mirror of
				https://github.com/esphome/esphome.git
				synced 2025-11-04 09:01:49 +00:00 
			
		
		
		
	Compare commits
	
		
			217 Commits
		
	
	
		
			2022.2.0
			...
			jesserockz
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					9268ef7665 | ||
| 
						 | 
					cd35ead890 | ||
| 
						 | 
					9dc804ee27 | ||
| 
						 | 
					a8ceeaa7b0 | ||
| 
						 | 
					7092f7663e | ||
| 
						 | 
					d9d2edeb08 | ||
| 
						 | 
					dda1ddcb26 | ||
| 
						 | 
					f0c890f160 | ||
| 
						 | 
					4f52d43347 | ||
| 
						 | 
					0ed7db979b | ||
| 
						 | 
					9c78049359 | ||
| 
						 | 
					7882105661 | ||
| 
						 | 
					c000e1d6dd | ||
| 
						 | 
					9b6b9c1fa2 | ||
| 
						 | 
					609a2ca592 | ||
| 
						 | 
					6dabf24bf3 | ||
| 
						 | 
					93e2506279 | ||
| 
						 | 
					f62d5d3b9d | ||
| 
						 | 
					0665acd190 | ||
| 
						 | 
					fea05e9d33 | ||
| 
						 | 
					7a03c7d56f | ||
| 
						 | 
					2dc2aec954 | ||
| 
						 | 
					39c6c2417a | ||
| 
						 | 
					03d5a0ec1d | ||
| 
						 | 
					1c873e0034 | ||
| 
						 | 
					bcb47c306c | ||
| 
						 | 
					01c4d3c225 | ||
| 
						 | 
					c2aaae4818 | ||
| 
						 | 
					3f678e218d | ||
| 
						 | 
					f8a1bd4e79 | ||
| 
						 | 
					40ad9f4911 | ||
| 
						 | 
					4116caff6a | ||
| 
						 | 
					0b69f72315 | ||
| 
						 | 
					c569f5ddcf | ||
| 
						 | 
					62f9e181e0 | ||
| 
						 | 
					235a97ea10 | ||
| 
						 | 
					e541ae400c | ||
| 
						 | 
					4822abde86 | ||
| 
						 | 
					b7e52812f8 | ||
| 
						 | 
					69118120d9 | ||
| 
						 | 
					7cba0c6fb0 | ||
| 
						 | 
					5fac67ce15 | ||
| 
						 | 
					98c733108e | ||
| 
						 | 
					782186e13d | ||
| 
						 | 
					4e1f6518e8 | ||
| 
						 | 
					53e0fe8e51 | ||
| 
						 | 
					0e547390da | ||
| 
						 | 
					86b52df839 | ||
| 
						 | 
					d685fdf54a | ||
| 
						 | 
					d9caab4108 | ||
| 
						 | 
					44b68f140e | ||
| 
						 | 
					3a3d97dfa7 | ||
| 
						 | 
					47898b527c | ||
| 
						 | 
					a35f36ad39 | ||
| 
						 | 
					d13a397f8e | ||
| 
						 | 
					df999723f8 | ||
| 
						 | 
					8236e840a7 | ||
| 
						 | 
					e5b3625f73 | ||
| 
						 | 
					2e4645310b | ||
| 
						 | 
					50a32b387e | ||
| 
						 | 
					2059283707 | ||
| 
						 | 
					8e3af515c9 | ||
| 
						 | 
					6f88f0ea3f | ||
| 
						 | 
					d2f37cf3f9 | ||
| 
						 | 
					7c30d6254e | ||
| 
						 | 
					64fb39a653 | ||
| 
						 | 
					91895aa70c | ||
| 
						 | 
					68dfaf238b | ||
| 
						 | 
					ebf13a0ba0 | ||
| 
						 | 
					2bff9937b7 | ||
| 
						 | 
					256395c28d | ||
| 
						 | 
					3346bc8bba | ||
| 
						 | 
					6fe22a7e62 | ||
| 
						 | 
					757b98748b | ||
| 
						 | 
					7a778f3f33 | ||
| 
						 | 
					9576d246ee | ||
| 
						 | 
					988d3ea8ba | ||
| 
						 | 
					0767b92b62 | ||
| 
						 | 
					2064abe16d | ||
| 
						 | 
					b605982f94 | ||
| 
						 | 
					93b628d9a8 | ||
| 
						 | 
					6bac551d9f | ||
| 
						 | 
					70a35656e4 | ||
| 
						 | 
					047c18eac0 | ||
| 
						 | 
					b4a86ce6cf | ||
| 
						 | 
					b778eed419 | ||
| 
						 | 
					8be704e591 | ||
| 
						 | 
					b622a8fa58 | ||
| 
						 | 
					a519e5c475 | ||
| 
						 | 
					d620b6dd5e | ||
| 
						 | 
					99335d986e | ||
| 
						 | 
					7895cd92cd | ||
| 
						 | 
					8b2c032da6 | ||
| 
						 | 
					da336247eb | ||
| 
						 | 
					dabd27d4be | ||
| 
						 | 
					fdda47db6e | ||
| 
						 | 
					efa6fd03e5 | ||
| 
						 | 
					9e3e34acf5 | ||
| 
						 | 
					a2d0c1bf18 | ||
| 
						 | 
					7663716ae8 | ||
| 
						 | 
					c2cacb3478 | ||
| 
						 | 
					84666b54b9 | ||
| 
						 | 
					2b91c23bf3 | ||
| 
						 | 
					3297267a16 | ||
| 
						 | 
					a9e653724c | ||
| 
						 | 
					5e79a1f500 | ||
| 
						 | 
					d4ff98680a | ||
| 
						 | 
					ba8d255cb4 | ||
| 
						 | 
					06f4ad922c | ||
| 
						 | 
					bff06e448b | ||
| 
						 | 
					d48ffa2913 | ||
| 
						 | 
					d97c3a7e01 | ||
| 
						 | 
					0b1161f7ef | ||
| 
						 | 
					061e1a471d | ||
| 
						 | 
					a39d874600 | ||
| 
						 | 
					de96376565 | ||
| 
						 | 
					c54c20ab3c | ||
| 
						 | 
					70fafa473b | ||
| 
						 | 
					2e436eae6b | ||
| 
						 | 
					fd7e861ff5 | ||
| 
						 | 
					792108686c | ||
| 
						 | 
					fa1b5117fd | ||
| 
						 | 
					b0bd9e0a34 | ||
| 
						 | 
					05dc97099a | ||
| 
						 | 
					9de61fcf58 | ||
| 
						 | 
					7f7175b184 | ||
| 
						 | 
					cf5c640ae4 | ||
| 
						 | 
					6b9371d105 | ||
| 
						 | 
					9a82057303 | ||
| 
						 | 
					48584e94c4 | ||
| 
						 | 
					d8024a5928 | ||
| 
						 | 
					2034ab4f6c | ||
| 
						 | 
					58b70b42dd | ||
| 
						 | 
					1496bc1b07 | ||
| 
						 | 
					bfbf88b2ea | ||
| 
						 | 
					e621b938e3 | ||
| 
						 | 
					0372d17a11 | ||
| 
						 | 
					4525588116 | ||
| 
						 | 
					68e957c147 | ||
| 
						 | 
					99f5ed1461 | ||
| 
						 | 
					59f67796dc | ||
| 
						 | 
					aafdfa933e | ||
| 
						 | 
					3208c8ed1e | ||
| 
						 | 
					6bf733e24e | ||
| 
						 | 
					65d3e8fbfc | ||
| 
						 | 
					a29d65d47c | ||
| 
						 | 
					0af1edefff | ||
| 
						 | 
					5b2457af0b | ||
| 
						 | 
					900b4f1af9 | ||
| 
						 | 
					4c22a98b0b | ||
| 
						 | 
					3b8ca80900 | ||
| 
						 | 
					dc6eff83ea | ||
| 
						 | 
					38ff66debd | ||
| 
						 | 
					1d2e0f74ea | ||
| 
						 | 
					bf60e40d0b | ||
| 
						 | 
					c9094ca537 | ||
| 
						 | 
					68b3fd6b8f | ||
| 
						 | 
					9323b3a248 | ||
| 
						 | 
					b55e9329d9 | ||
| 
						 | 
					a5b4105971 | ||
| 
						 | 
					d1feaa935d | ||
| 
						 | 
					6919930aaa | ||
| 
						 | 
					69633826bb | ||
| 
						 | 
					771162bfb1 | ||
| 
						 | 
					ba785e29e9 | ||
| 
						 | 
					2c7b104f4a | ||
| 
						 | 
					78951c197a | ||
| 
						 | 
					07c1cf7137 | ||
| 
						 | 
					d26141151a | ||
| 
						 | 
					f59dbe4a88 | ||
| 
						 | 
					8dae7f8225 | ||
| 
						 | 
					5811389891 | ||
| 
						 | 
					debcaf6fb7 | ||
| 
						 | 
					b8d10a62c2 | ||
| 
						 | 
					d2b209234f | ||
| 
						 | 
					34c9d8be50 | ||
| 
						 | 
					ae57ad0c81 | ||
| 
						 | 
					0c1520dd9c | ||
| 
						 | 
					d594f43ebd | ||
| 
						 | 
					125c693e3f | ||
| 
						 | 
					ad2f857e15 | ||
| 
						 | 
					e445d6aada | ||
| 
						 | 
					88fbb0ffbb | ||
| 
						 | 
					231908fe9f | ||
| 
						 | 
					f137cc10f4 | ||
| 
						 | 
					1a8f8adc2a | ||
| 
						 | 
					7a242bb4ed | ||
| 
						 | 
					3b8bb09ae3 | ||
| 
						 | 
					140db85d21 | ||
| 
						 | 
					ccce4b19e8 | ||
| 
						 | 
					8cb9be7560 | ||
| 
						 | 
					953f0569fb | ||
| 
						 | 
					34c229fd33 | ||
| 
						 | 
					958ad0d750 | ||
| 
						 | 
					36ddd9dd69 | ||
| 
						 | 
					38259c96c9 | ||
| 
						 | 
					c054fb8a2c | ||
| 
						 | 
					5a0b8328d8 | ||
| 
						 | 
					ffa19426d7 | ||
| 
						 | 
					c123804294 | ||
| 
						 | 
					4e24551b90 | ||
| 
						 | 
					51cb5da7f0 | ||
| 
						 | 
					41f84447cc | ||
| 
						 | 
					ce073a704b | ||
| 
						 | 
					113232ebb6 | ||
| 
						 | 
					a13a1225b7 | ||
| 
						 | 
					0ec84be5da | ||
| 
						 | 
					b1cefb7e3e | ||
| 
						 | 
					cc0c1c08b9 | ||
| 
						 | 
					3a67884451 | ||
| 
						 | 
					72e716cdf1 | ||
| 
						 | 
					40e06c9819 | ||
| 
						 | 
					ad6c5ff11d | ||
| 
						 | 
					335512e232 | ||
| 
						 | 
					2622e59b0b | ||
| 
						 | 
					35e6a13cd1 | ||
| 
						 | 
					b48490badc | 
							
								
								
									
										1
									
								
								.github/ISSUE_TEMPLATE/config.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/ISSUE_TEMPLATE/config.yml
									
									
									
									
										vendored
									
									
								
							@@ -9,4 +9,3 @@ contact_links:
 | 
			
		||||
  - name: Frequently Asked Question
 | 
			
		||||
    url: https://esphome.io/guides/faq.html
 | 
			
		||||
    about: Please view the FAQ for common questions and what to include in a bug report.
 | 
			
		||||
    
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								.github/PULL_REQUEST_TEMPLATE.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/PULL_REQUEST_TEMPLATE.md
									
									
									
									
										vendored
									
									
								
							@@ -1,4 +1,4 @@
 | 
			
		||||
# What does this implement/fix? 
 | 
			
		||||
# What does this implement/fix?
 | 
			
		||||
 | 
			
		||||
Quick description and explanation of changes
 | 
			
		||||
 | 
			
		||||
@@ -35,6 +35,6 @@ Quick description and explanation of changes
 | 
			
		||||
## Checklist:
 | 
			
		||||
  - [ ] The code change is tested and works locally.
 | 
			
		||||
  - [ ] Tests have been added to verify that the new code works (under `tests/` folder).
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
If user exposed functionality or configuration variables are added/changed:
 | 
			
		||||
  - [ ] Documentation added/updated in [esphome-docs](https://github.com/esphome/esphome-docs).
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							@@ -80,7 +80,7 @@ jobs:
 | 
			
		||||
        uses: actions/setup-python@v2
 | 
			
		||||
        id: python
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: '3.7'
 | 
			
		||||
          python-version: '3.8'
 | 
			
		||||
 | 
			
		||||
      - name: Cache virtualenv
 | 
			
		||||
        uses: actions/cache@v2
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							@@ -143,7 +143,7 @@ jobs:
 | 
			
		||||
    needs: [deploy-docker]
 | 
			
		||||
    steps:
 | 
			
		||||
      - env:
 | 
			
		||||
          TOKEN: ${{ secrets.DEPLOY_HASSIO_TOKEN }}
 | 
			
		||||
          TOKEN: ${{ secrets.DEPLOY_HA_ADDON_REPO_TOKEN }}
 | 
			
		||||
        run: |
 | 
			
		||||
          TAG="${GITHUB_REF#refs/tags/}"
 | 
			
		||||
          curl \
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -77,6 +77,7 @@ venv/
 | 
			
		||||
ENV/
 | 
			
		||||
env.bak/
 | 
			
		||||
venv.bak/
 | 
			
		||||
venv-*/
 | 
			
		||||
 | 
			
		||||
# mypy
 | 
			
		||||
.mypy_cache/
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@
 | 
			
		||||
# See https://pre-commit.com/hooks.html for more hooks
 | 
			
		||||
repos:
 | 
			
		||||
  - repo: https://github.com/ambv/black
 | 
			
		||||
    rev: 22.1.0
 | 
			
		||||
    rev: 22.3.0
 | 
			
		||||
    hooks:
 | 
			
		||||
    - id: black
 | 
			
		||||
      args:
 | 
			
		||||
@@ -25,3 +25,8 @@ repos:
 | 
			
		||||
          - --branch=dev
 | 
			
		||||
          - --branch=release
 | 
			
		||||
          - --branch=beta
 | 
			
		||||
  - repo: https://github.com/asottile/pyupgrade
 | 
			
		||||
    rev: v2.31.1
 | 
			
		||||
    hooks:
 | 
			
		||||
      - id: pyupgrade
 | 
			
		||||
        args: [--py38-plus]
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										28
									
								
								CODEOWNERS
									
									
									
									
									
								
							
							
						
						
									
										28
									
								
								CODEOWNERS
									
									
									
									
									
								
							@@ -19,6 +19,7 @@ esphome/components/airthings_wave_mini/* @ncareau
 | 
			
		||||
esphome/components/airthings_wave_plus/* @jeromelaban
 | 
			
		||||
esphome/components/am43/* @buxtronix
 | 
			
		||||
esphome/components/am43/cover/* @buxtronix
 | 
			
		||||
esphome/components/analog_threshold/* @ianchi
 | 
			
		||||
esphome/components/animation/* @syndlex
 | 
			
		||||
esphome/components/anova/* @buxtronix
 | 
			
		||||
esphome/components/api/* @OttoWinter
 | 
			
		||||
@@ -27,7 +28,10 @@ esphome/components/atc_mithermometer/* @ahpohl
 | 
			
		||||
esphome/components/b_parasite/* @rbaron
 | 
			
		||||
esphome/components/ballu/* @bazuchan
 | 
			
		||||
esphome/components/bang_bang/* @OttoWinter
 | 
			
		||||
esphome/components/bedjet/* @jhansche
 | 
			
		||||
esphome/components/bh1750/* @OttoWinter
 | 
			
		||||
esphome/components/binary_sensor/* @esphome/core
 | 
			
		||||
esphome/components/bl0939/* @ziceva
 | 
			
		||||
esphome/components/bl0940/* @tobias-
 | 
			
		||||
esphome/components/ble_client/* @buxtronix
 | 
			
		||||
esphome/components/bme680_bsec/* @trvrnrth
 | 
			
		||||
@@ -42,6 +46,7 @@ esphome/components/climate/* @esphome/core
 | 
			
		||||
esphome/components/climate_ir/* @glmnet
 | 
			
		||||
esphome/components/color_temperature/* @jesserockz
 | 
			
		||||
esphome/components/coolix/* @glmnet
 | 
			
		||||
esphome/components/copy/* @OttoWinter
 | 
			
		||||
esphome/components/cover/* @esphome/core
 | 
			
		||||
esphome/components/cs5460a/* @balrog-kun
 | 
			
		||||
esphome/components/cse7761/* @berfenger
 | 
			
		||||
@@ -50,11 +55,13 @@ esphome/components/current_based/* @djwmarcx
 | 
			
		||||
esphome/components/daly_bms/* @s1lvi0
 | 
			
		||||
esphome/components/dashboard_import/* @esphome/core
 | 
			
		||||
esphome/components/debug/* @OttoWinter
 | 
			
		||||
esphome/components/delonghi/* @grob6000
 | 
			
		||||
esphome/components/dfplayer/* @glmnet
 | 
			
		||||
esphome/components/dht/* @OttoWinter
 | 
			
		||||
esphome/components/ds1307/* @badbadc0ffee
 | 
			
		||||
esphome/components/dsmr/* @glmnet @zuidwijk
 | 
			
		||||
esphome/components/ektf2232/* @jesserockz
 | 
			
		||||
esphome/components/ens210/* @itn3rd77
 | 
			
		||||
esphome/components/esp32/* @esphome/core
 | 
			
		||||
esphome/components/esp32_ble/* @jesserockz
 | 
			
		||||
esphome/components/esp32_ble_server/* @jesserockz
 | 
			
		||||
@@ -77,7 +84,9 @@ esphome/components/hbridge/light/* @DotNetDann
 | 
			
		||||
esphome/components/heatpumpir/* @rob-deutsch
 | 
			
		||||
esphome/components/hitachi_ac424/* @sourabhjaiswal
 | 
			
		||||
esphome/components/homeassistant/* @OttoWinter
 | 
			
		||||
esphome/components/honeywellabp/* @RubyBailey
 | 
			
		||||
esphome/components/hrxl_maxsonar_wr/* @netmikey
 | 
			
		||||
esphome/components/hydreon_rgxx/* @functionpointer
 | 
			
		||||
esphome/components/i2c/* @esphome/core
 | 
			
		||||
esphome/components/improv_serial/* @esphome/core
 | 
			
		||||
esphome/components/ina260/* @MrEditor97
 | 
			
		||||
@@ -93,6 +102,7 @@ esphome/components/lilygo_t5_47/touchscreen/* @jesserockz
 | 
			
		||||
esphome/components/lock/* @esphome/core
 | 
			
		||||
esphome/components/logger/* @esphome/core
 | 
			
		||||
esphome/components/ltr390/* @sjtrny
 | 
			
		||||
esphome/components/max44009/* @berfenger
 | 
			
		||||
esphome/components/max7219digit/* @rspaargaren
 | 
			
		||||
esphome/components/max9611/* @mckaymatthew
 | 
			
		||||
esphome/components/mcp23008/* @jesserockz
 | 
			
		||||
@@ -104,6 +114,7 @@ esphome/components/mcp23x17_base/* @jesserockz
 | 
			
		||||
esphome/components/mcp23xxx_base/* @jesserockz
 | 
			
		||||
esphome/components/mcp2515/* @danielschramm @mvturnho
 | 
			
		||||
esphome/components/mcp3204/* @rsumner
 | 
			
		||||
esphome/components/mcp4728/* @berfenger
 | 
			
		||||
esphome/components/mcp47a1/* @jesserockz
 | 
			
		||||
esphome/components/mcp9808/* @k7hpn
 | 
			
		||||
esphome/components/md5/* @esphome/core
 | 
			
		||||
@@ -120,6 +131,9 @@ esphome/components/modbus_controller/select/* @martgras @stegm
 | 
			
		||||
esphome/components/modbus_controller/sensor/* @martgras
 | 
			
		||||
esphome/components/modbus_controller/switch/* @martgras
 | 
			
		||||
esphome/components/modbus_controller/text_sensor/* @martgras
 | 
			
		||||
esphome/components/mopeka_ble/* @spbrogan
 | 
			
		||||
esphome/components/mopeka_pro_check/* @spbrogan
 | 
			
		||||
esphome/components/mpu6886/* @fabaff
 | 
			
		||||
esphome/components/network/* @esphome/core
 | 
			
		||||
esphome/components/nextion/* @senexcrenshaw
 | 
			
		||||
esphome/components/nextion/binary_sensor/* @senexcrenshaw
 | 
			
		||||
@@ -140,8 +154,9 @@ esphome/components/pn532_spi/* @OttoWinter @jesserockz
 | 
			
		||||
esphome/components/power_supply/* @esphome/core
 | 
			
		||||
esphome/components/preferences/* @esphome/core
 | 
			
		||||
esphome/components/psram/* @esphome/core
 | 
			
		||||
esphome/components/pulse_meter/* @stevebaxter
 | 
			
		||||
esphome/components/pulse_meter/* @cstaahl @stevebaxter
 | 
			
		||||
esphome/components/pvvx_mithermometer/* @pasiz
 | 
			
		||||
esphome/components/qmp6988/* @andrewpc
 | 
			
		||||
esphome/components/qr_code/* @wjtje
 | 
			
		||||
esphome/components/radon_eye_ble/* @jeffeb3
 | 
			
		||||
esphome/components/radon_eye_rd200/* @jeffeb3
 | 
			
		||||
@@ -153,20 +168,27 @@ esphome/components/rf_bridge/* @jesserockz
 | 
			
		||||
esphome/components/rgbct/* @jesserockz
 | 
			
		||||
esphome/components/rtttl/* @glmnet
 | 
			
		||||
esphome/components/safe_mode/* @jsuanet @paulmonigatti
 | 
			
		||||
esphome/components/scd4x/* @sjtrny
 | 
			
		||||
esphome/components/scd4x/* @martgras @sjtrny
 | 
			
		||||
esphome/components/script/* @esphome/core
 | 
			
		||||
esphome/components/sdm_meter/* @jesserockz @polyfaces
 | 
			
		||||
esphome/components/sdp3x/* @Azimath
 | 
			
		||||
esphome/components/selec_meter/* @sourabhjaiswal
 | 
			
		||||
esphome/components/select/* @esphome/core
 | 
			
		||||
esphome/components/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/socket/* @esphome/core
 | 
			
		||||
esphome/components/sonoff_d1/* @anatoly-savchenkov
 | 
			
		||||
esphome/components/spi/* @esphome/core
 | 
			
		||||
esphome/components/sps30/* @martgras
 | 
			
		||||
esphome/components/ssd1322_base/* @kbx81
 | 
			
		||||
esphome/components/ssd1322_spi/* @kbx81
 | 
			
		||||
esphome/components/ssd1325_base/* @kbx81
 | 
			
		||||
@@ -201,6 +223,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
 | 
			
		||||
@@ -213,4 +236,5 @@ esphome/components/whirlpool/* @glmnet
 | 
			
		||||
esphome/components/xiaomi_lywsd03mmc/* @ahpohl
 | 
			
		||||
esphome/components/xiaomi_mhoc303/* @drug123
 | 
			
		||||
esphome/components/xiaomi_mhoc401/* @vevsvevs
 | 
			
		||||
esphome/components/xiaomi_rtcgq02lm/* @jesserockz
 | 
			
		||||
esphome/components/xpt2046/* @numo68
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,7 @@ For a detailed guide, please see https://esphome.io/guides/contributing.html#con
 | 
			
		||||
Things to note when contributing:
 | 
			
		||||
 | 
			
		||||
 - Please test your changes :)
 | 
			
		||||
 - If a new feature is added or an existing user-facing feature is changed, you should also 
 | 
			
		||||
 - If a new feature is added or an existing user-facing feature is changed, you should also
 | 
			
		||||
   update the [docs](https://github.com/esphome/esphome-docs). See [contributing to esphome-docs](https://esphome.io/guides/contributing.html#contributing-to-esphomedocs)
 | 
			
		||||
   for more information.
 | 
			
		||||
 - Please also update the tests in the `tests/` folder. You can do so by just adding a line in one of the YAML files
 | 
			
		||||
 
 | 
			
		||||
@@ -5,12 +5,14 @@
 | 
			
		||||
# One of "docker", "hassio"
 | 
			
		||||
ARG BASEIMGTYPE=docker
 | 
			
		||||
 | 
			
		||||
FROM ghcr.io/hassio-addons/debian-base/amd64:5.2.3 AS base-hassio-amd64
 | 
			
		||||
FROM ghcr.io/hassio-addons/debian-base/aarch64:5.2.3 AS base-hassio-arm64
 | 
			
		||||
FROM ghcr.io/hassio-addons/debian-base/armv7:5.2.3 AS base-hassio-armv7
 | 
			
		||||
FROM debian:bullseye-20220125-slim AS base-docker-amd64
 | 
			
		||||
FROM debian:bullseye-20220125-slim AS base-docker-arm64
 | 
			
		||||
FROM debian:bullseye-20220125-slim AS base-docker-armv7
 | 
			
		||||
# https://github.com/hassio-addons/addon-debian-base/releases
 | 
			
		||||
FROM ghcr.io/hassio-addons/debian-base/amd64:5.3.0 AS base-hassio-amd64
 | 
			
		||||
FROM ghcr.io/hassio-addons/debian-base/aarch64:5.3.0 AS base-hassio-arm64
 | 
			
		||||
FROM ghcr.io/hassio-addons/debian-base/armv7:5.3.0 AS base-hassio-armv7
 | 
			
		||||
# https://hub.docker.com/_/debian?tab=tags&page=1&name=bullseye
 | 
			
		||||
FROM debian:bullseye-20220328-slim AS base-docker-amd64
 | 
			
		||||
FROM debian:bullseye-20220328-slim AS base-docker-arm64
 | 
			
		||||
FROM debian:bullseye-20220328-slim AS base-docker-armv7
 | 
			
		||||
 | 
			
		||||
# Use TARGETARCH/TARGETVARIANT defined by docker
 | 
			
		||||
# https://docs.docker.com/engine/reference/builder/#automatic-platform-args-in-the-global-scope
 | 
			
		||||
@@ -21,13 +23,14 @@ RUN \
 | 
			
		||||
    # Use pinned versions so that we get updates with build caching
 | 
			
		||||
    && apt-get install -y --no-install-recommends \
 | 
			
		||||
        python3=3.9.2-3 \
 | 
			
		||||
        python3-pip=20.3.4-4 \
 | 
			
		||||
        python3-pip=20.3.4-4+deb11u1 \
 | 
			
		||||
        python3-setuptools=52.0.0-4 \
 | 
			
		||||
        python3-pil=8.1.2+dfsg-0.3+deb11u1 \
 | 
			
		||||
        python3-cryptography=3.3.2-1 \
 | 
			
		||||
        iputils-ping=3:20210202-1 \
 | 
			
		||||
        git=1:2.30.2-1 \
 | 
			
		||||
        curl=7.74.0-1.3+deb11u1 \
 | 
			
		||||
        openssh-client=1:8.4p1-5 \
 | 
			
		||||
    && rm -rf \
 | 
			
		||||
        /tmp/* \
 | 
			
		||||
        /var/{cache,log}/* \
 | 
			
		||||
@@ -43,7 +46,7 @@ RUN \
 | 
			
		||||
    # Ubuntu python3-pip is missing wheel
 | 
			
		||||
    pip3 install --no-cache-dir \
 | 
			
		||||
        wheel==0.37.1 \
 | 
			
		||||
        platformio==5.2.4 \
 | 
			
		||||
        platformio==5.2.5 \
 | 
			
		||||
    # Change some platformio settings
 | 
			
		||||
    && platformio settings set enable_telemetry No \
 | 
			
		||||
    && platformio settings set check_libraries_interval 1000000 \
 | 
			
		||||
@@ -52,16 +55,16 @@ RUN \
 | 
			
		||||
    && mkdir -p /piolibs
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# ======================= docker-type image =======================
 | 
			
		||||
FROM base AS docker
 | 
			
		||||
 | 
			
		||||
# First install requirements to leverage caching when requirements don't change
 | 
			
		||||
COPY requirements.txt requirements_optional.txt docker/platformio_install_deps.py platformio.ini /
 | 
			
		||||
RUN \
 | 
			
		||||
    pip3 install --no-cache-dir -r /requirements.txt -r /requirements_optional.txt \
 | 
			
		||||
    && /platformio_install_deps.py /platformio.ini
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# ======================= docker-type image =======================
 | 
			
		||||
FROM base AS docker
 | 
			
		||||
 | 
			
		||||
# Copy esphome and install
 | 
			
		||||
COPY . /esphome
 | 
			
		||||
RUN pip3 install --no-cache-dir --no-use-pep517 -e /esphome
 | 
			
		||||
@@ -93,7 +96,7 @@ RUN \
 | 
			
		||||
    apt-get update \
 | 
			
		||||
    # Use pinned versions so that we get updates with build caching
 | 
			
		||||
    && apt-get install -y --no-install-recommends \
 | 
			
		||||
        nginx=1.18.0-6.1 \
 | 
			
		||||
        nginx-light=1.18.0-6.1 \
 | 
			
		||||
    && rm -rf \
 | 
			
		||||
        /tmp/* \
 | 
			
		||||
        /var/{cache,log}/* \
 | 
			
		||||
@@ -104,12 +107,6 @@ ARG BUILD_VERSION=dev
 | 
			
		||||
# Copy root filesystem
 | 
			
		||||
COPY docker/ha-addon-rootfs/ /
 | 
			
		||||
 | 
			
		||||
# First install requirements to leverage caching when requirements don't change
 | 
			
		||||
COPY requirements.txt requirements_optional.txt docker/platformio_install_deps.py platformio.ini /
 | 
			
		||||
RUN \
 | 
			
		||||
    pip3 install --no-cache-dir -r /requirements.txt -r /requirements_optional.txt \
 | 
			
		||||
    && /platformio_install_deps.py /platformio.ini
 | 
			
		||||
 | 
			
		||||
# Copy esphome and install
 | 
			
		||||
COPY . /esphome
 | 
			
		||||
RUN pip3 install --no-cache-dir --no-use-pep517 -e /esphome
 | 
			
		||||
@@ -147,10 +144,8 @@ RUN \
 | 
			
		||||
        /var/{cache,log}/* \
 | 
			
		||||
        /var/lib/apt/lists/*
 | 
			
		||||
 | 
			
		||||
COPY requirements.txt requirements_optional.txt requirements_test.txt docker/platformio_install_deps.py platformio.ini /
 | 
			
		||||
RUN \
 | 
			
		||||
    pip3 install --no-cache-dir -r /requirements.txt -r /requirements_optional.txt -r /requirements_test.txt \
 | 
			
		||||
    && /platformio_install_deps.py /platformio.ini
 | 
			
		||||
COPY requirements_test.txt /
 | 
			
		||||
RUN pip3 install --no-cache-dir -r /requirements_test.txt
 | 
			
		||||
 | 
			
		||||
VOLUME ["/esphome"]
 | 
			
		||||
WORKDIR /esphome
 | 
			
		||||
 
 | 
			
		||||
@@ -7,12 +7,12 @@
 | 
			
		||||
# Check SSL requirements, if enabled
 | 
			
		||||
if bashio::config.true 'ssl'; then
 | 
			
		||||
    if ! bashio::config.has_value 'certfile'; then
 | 
			
		||||
        bashio::fatal 'SSL is enabled, but no certfile was specified.'
 | 
			
		||||
        bashio::log.fatal 'SSL is enabled, but no certfile was specified.'
 | 
			
		||||
        bashio::exit.nok
 | 
			
		||||
    fi
 | 
			
		||||
 | 
			
		||||
    if ! bashio::config.has_value 'keyfile'; then
 | 
			
		||||
        bashio::fatal 'SSL is enabled, but no keyfile was specified'
 | 
			
		||||
        bashio::log.fatal 'SSL is enabled, but no keyfile was specified'
 | 
			
		||||
        bashio::exit.nok
 | 
			
		||||
    fi
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@ import argparse
 | 
			
		||||
import functools
 | 
			
		||||
import logging
 | 
			
		||||
import os
 | 
			
		||||
import re
 | 
			
		||||
import sys
 | 
			
		||||
from datetime import datetime
 | 
			
		||||
 | 
			
		||||
@@ -9,15 +10,18 @@ from esphome import const, writer, yaml_util
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.config import iter_components, read_config, strip_default_ids
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    ALLOWED_NAME_CHARS,
 | 
			
		||||
    CONF_BAUD_RATE,
 | 
			
		||||
    CONF_BROKER,
 | 
			
		||||
    CONF_DEASSERT_RTS_DTR,
 | 
			
		||||
    CONF_LOGGER,
 | 
			
		||||
    CONF_NAME,
 | 
			
		||||
    CONF_OTA,
 | 
			
		||||
    CONF_PASSWORD,
 | 
			
		||||
    CONF_PORT,
 | 
			
		||||
    CONF_ESPHOME,
 | 
			
		||||
    CONF_PLATFORMIO_OPTIONS,
 | 
			
		||||
    CONF_SUBSTITUTIONS,
 | 
			
		||||
    SECRETS_FILES,
 | 
			
		||||
)
 | 
			
		||||
from esphome.core import CORE, EsphomeError, coroutine
 | 
			
		||||
@@ -481,6 +485,98 @@ def command_idedata(args, config):
 | 
			
		||||
    return 0
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def command_rename(args, config):
 | 
			
		||||
    for c in args.name:
 | 
			
		||||
        if c not in ALLOWED_NAME_CHARS:
 | 
			
		||||
            print(
 | 
			
		||||
                color(
 | 
			
		||||
                    Fore.BOLD_RED,
 | 
			
		||||
                    f"'{c}' is an invalid character for names. Valid characters are: "
 | 
			
		||||
                    f"{ALLOWED_NAME_CHARS} (lowercase, no spaces)",
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
            return 1
 | 
			
		||||
    # Load existing yaml file
 | 
			
		||||
    with open(CORE.config_path, mode="r+", encoding="utf-8") as raw_file:
 | 
			
		||||
        raw_contents = raw_file.read()
 | 
			
		||||
 | 
			
		||||
    yaml = yaml_util.load_yaml(CORE.config_path)
 | 
			
		||||
    if CONF_ESPHOME not in yaml or CONF_NAME not in yaml[CONF_ESPHOME]:
 | 
			
		||||
        print(
 | 
			
		||||
            color(Fore.BOLD_RED, "Complex YAML files cannot be automatically renamed.")
 | 
			
		||||
        )
 | 
			
		||||
        return 1
 | 
			
		||||
    old_name = yaml[CONF_ESPHOME][CONF_NAME]
 | 
			
		||||
    match = re.match(r"^\$\{?([a-zA-Z0-9_]+)\}?$", old_name)
 | 
			
		||||
    if match is None:
 | 
			
		||||
        new_raw = re.sub(
 | 
			
		||||
            rf"name:\s+[\"']?{old_name}[\"']?",
 | 
			
		||||
            f'name: "{args.name}"',
 | 
			
		||||
            raw_contents,
 | 
			
		||||
        )
 | 
			
		||||
    else:
 | 
			
		||||
        old_name = yaml[CONF_SUBSTITUTIONS][match.group(1)]
 | 
			
		||||
        if (
 | 
			
		||||
            len(
 | 
			
		||||
                re.findall(
 | 
			
		||||
                    rf"^\s+{match.group(1)}:\s+[\"']?{old_name}[\"']?",
 | 
			
		||||
                    raw_contents,
 | 
			
		||||
                    flags=re.MULTILINE,
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
            > 1
 | 
			
		||||
        ):
 | 
			
		||||
            print(color(Fore.BOLD_RED, "Too many matches in YAML to safely rename"))
 | 
			
		||||
            return 1
 | 
			
		||||
 | 
			
		||||
        new_raw = re.sub(
 | 
			
		||||
            rf"^(\s+{match.group(1)}):\s+[\"']?{old_name}[\"']?",
 | 
			
		||||
            f'\\1: "{args.name}"',
 | 
			
		||||
            raw_contents,
 | 
			
		||||
            flags=re.MULTILINE,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    new_path = os.path.join(CORE.config_dir, args.name + ".yaml")
 | 
			
		||||
    print(
 | 
			
		||||
        f"Updating {color(Fore.CYAN, CORE.config_path)} to {color(Fore.CYAN, new_path)}"
 | 
			
		||||
    )
 | 
			
		||||
    print()
 | 
			
		||||
 | 
			
		||||
    with open(new_path, mode="w", encoding="utf-8") as new_file:
 | 
			
		||||
        new_file.write(new_raw)
 | 
			
		||||
 | 
			
		||||
    rc = run_external_process("esphome", "config", new_path)
 | 
			
		||||
    if rc != 0:
 | 
			
		||||
        print(color(Fore.BOLD_RED, "Rename failed. Reverting changes."))
 | 
			
		||||
        os.remove(new_path)
 | 
			
		||||
        return 1
 | 
			
		||||
 | 
			
		||||
    cli_args = [
 | 
			
		||||
        "run",
 | 
			
		||||
        new_path,
 | 
			
		||||
        "--no-logs",
 | 
			
		||||
        "--device",
 | 
			
		||||
        CORE.address,
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    if args.dashboard:
 | 
			
		||||
        cli_args.insert(0, "--dashboard")
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        rc = run_external_process("esphome", *cli_args)
 | 
			
		||||
    except KeyboardInterrupt:
 | 
			
		||||
        rc = 1
 | 
			
		||||
    if rc != 0:
 | 
			
		||||
        os.remove(new_path)
 | 
			
		||||
        return 1
 | 
			
		||||
 | 
			
		||||
    os.remove(CORE.config_path)
 | 
			
		||||
 | 
			
		||||
    print(color(Fore.BOLD_GREEN, "SUCCESS"))
 | 
			
		||||
    print()
 | 
			
		||||
    return 0
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
PRE_CONFIG_ACTIONS = {
 | 
			
		||||
    "wizard": command_wizard,
 | 
			
		||||
    "version": command_version,
 | 
			
		||||
@@ -499,6 +595,7 @@ POST_CONFIG_ACTIONS = {
 | 
			
		||||
    "mqtt-fingerprint": command_mqtt_fingerprint,
 | 
			
		||||
    "clean": command_clean,
 | 
			
		||||
    "idedata": command_idedata,
 | 
			
		||||
    "rename": command_rename,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -681,6 +778,15 @@ def parse_args(argv):
 | 
			
		||||
        "configuration", help="Your YAML configuration file(s).", nargs=1
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    parser_rename = subparsers.add_parser(
 | 
			
		||||
        "rename",
 | 
			
		||||
        help="Rename a device in YAML, compile the binary and upload it.",
 | 
			
		||||
    )
 | 
			
		||||
    parser_rename.add_argument(
 | 
			
		||||
        "configuration", help="Your YAML configuration file.", nargs=1
 | 
			
		||||
    )
 | 
			
		||||
    parser_rename.add_argument("name", help="The new name for the device.", type=str)
 | 
			
		||||
 | 
			
		||||
    # Keep backward compatibility with the old command line format of
 | 
			
		||||
    # esphome <config> <command>.
 | 
			
		||||
    #
 | 
			
		||||
@@ -778,10 +884,10 @@ def run_esphome(argv):
 | 
			
		||||
        _LOGGER.warning("Please instead use:")
 | 
			
		||||
        _LOGGER.warning("   esphome %s", " ".join(args.deprecated_argv_suggestion))
 | 
			
		||||
 | 
			
		||||
    if sys.version_info < (3, 7, 0):
 | 
			
		||||
    if sys.version_info < (3, 8, 0):
 | 
			
		||||
        _LOGGER.error(
 | 
			
		||||
            "You're running ESPHome with Python <3.7. ESPHome is no longer compatible "
 | 
			
		||||
            "with this Python version. Please reinstall ESPHome with Python 3.7+"
 | 
			
		||||
            "You're running ESPHome with Python <3.8. ESPHome is no longer compatible "
 | 
			
		||||
            "with this Python version. Please reinstall ESPHome with Python 3.8+"
 | 
			
		||||
        )
 | 
			
		||||
        return 1
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -262,21 +262,16 @@ async def repeat_action_to_code(config, action_id, template_arg, args):
 | 
			
		||||
    return var
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def validate_wait_until(value):
 | 
			
		||||
    schema = cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
            cv.Required(CONF_CONDITION): validate_potentially_and_condition,
 | 
			
		||||
            cv.Optional(CONF_TIMEOUT): cv.templatable(
 | 
			
		||||
                cv.positive_time_period_milliseconds
 | 
			
		||||
            ),
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
    if isinstance(value, dict) and CONF_CONDITION in value:
 | 
			
		||||
        return schema(value)
 | 
			
		||||
    return validate_wait_until({CONF_CONDITION: value})
 | 
			
		||||
_validate_wait_until = cv.maybe_simple_value(
 | 
			
		||||
    {
 | 
			
		||||
        cv.Required(CONF_CONDITION): validate_potentially_and_condition,
 | 
			
		||||
        cv.Optional(CONF_TIMEOUT): cv.templatable(cv.positive_time_period_milliseconds),
 | 
			
		||||
    },
 | 
			
		||||
    key=CONF_CONDITION,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register_action("wait_until", WaitUntilAction, validate_wait_until)
 | 
			
		||||
@register_action("wait_until", WaitUntilAction, _validate_wait_until)
 | 
			
		||||
async def wait_until_action_to_code(config, action_id, template_arg, args):
 | 
			
		||||
    conditions = await build_condition(config[CONF_CONDITION], template_arg, args)
 | 
			
		||||
    var = cg.new_Pvariable(action_id, template_arg, conditions)
 | 
			
		||||
 
 | 
			
		||||
@@ -64,6 +64,7 @@ from esphome.cpp_types import (  # noqa
 | 
			
		||||
    uint64,
 | 
			
		||||
    int32,
 | 
			
		||||
    int64,
 | 
			
		||||
    size_t,
 | 
			
		||||
    const_char_ptr,
 | 
			
		||||
    NAN,
 | 
			
		||||
    esphome_ns,
 | 
			
		||||
 
 | 
			
		||||
@@ -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%
 | 
			
		||||
 
 | 
			
		||||
@@ -51,8 +51,8 @@ void ADCSensor::setup() {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // adc_gpio_init doesn't exist on ESP32-C3 or ESP32-H2
 | 
			
		||||
#if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32H2)
 | 
			
		||||
  // adc_gpio_init 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
 | 
			
		||||
 
 | 
			
		||||
@@ -133,6 +133,7 @@ ADCSensor = adc_ns.class_(
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.All(
 | 
			
		||||
    sensor.sensor_schema(
 | 
			
		||||
        ADCSensor,
 | 
			
		||||
        unit_of_measurement=UNIT_VOLT,
 | 
			
		||||
        accuracy_decimals=2,
 | 
			
		||||
        device_class=DEVICE_CLASS_VOLTAGE,
 | 
			
		||||
@@ -140,7 +141,6 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
    )
 | 
			
		||||
    .extend(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(ADCSensor),
 | 
			
		||||
            cv.Required(CONF_PIN): validate_adc_pin,
 | 
			
		||||
            cv.Optional(CONF_RAW, default=False): cv.boolean,
 | 
			
		||||
            cv.SplitDefault(CONF_ATTENUATION, esp32="0db"): cv.All(
 | 
			
		||||
 
 | 
			
		||||
@@ -52,6 +52,7 @@ ADS1115Sensor = ads1115_ns.class_(
 | 
			
		||||
CONF_ADS1115_ID = "ads1115_id"
 | 
			
		||||
CONFIG_SCHEMA = (
 | 
			
		||||
    sensor.sensor_schema(
 | 
			
		||||
        ADS1115Sensor,
 | 
			
		||||
        unit_of_measurement=UNIT_VOLT,
 | 
			
		||||
        accuracy_decimals=3,
 | 
			
		||||
        device_class=DEVICE_CLASS_VOLTAGE,
 | 
			
		||||
@@ -59,7 +60,6 @@ CONFIG_SCHEMA = (
 | 
			
		||||
    )
 | 
			
		||||
    .extend(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(ADS1115Sensor),
 | 
			
		||||
            cv.GenerateID(CONF_ADS1115_ID): cv.use_id(ADS1115Component),
 | 
			
		||||
            cv.Required(CONF_MULTIPLEXER): cv.enum(MUX, upper=True, space="_"),
 | 
			
		||||
            cv.Required(CONF_GAIN): validate_gain,
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								esphome/components/analog_threshold/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								esphome/components/analog_threshold/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
CODEOWNERS = ["@ianchi"]
 | 
			
		||||
@@ -0,0 +1,40 @@
 | 
			
		||||
#include "analog_threshold_binary_sensor.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace analog_threshold {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "analog_threshold.binary_sensor";
 | 
			
		||||
 | 
			
		||||
void AnalogThresholdBinarySensor::setup() {
 | 
			
		||||
  float sensor_value = this->sensor_->get_state();
 | 
			
		||||
 | 
			
		||||
  // TRUE state is defined to be when sensor is >= threshold
 | 
			
		||||
  // so when undefined sensor value initialize to FALSE
 | 
			
		||||
  if (std::isnan(sensor_value)) {
 | 
			
		||||
    this->publish_initial_state(false);
 | 
			
		||||
  } else {
 | 
			
		||||
    this->publish_initial_state(sensor_value >= (this->lower_threshold_ + this->upper_threshold_) / 2.0f);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void AnalogThresholdBinarySensor::set_sensor(sensor::Sensor *analog_sensor) {
 | 
			
		||||
  this->sensor_ = analog_sensor;
 | 
			
		||||
 | 
			
		||||
  this->sensor_->add_on_state_callback([this](float sensor_value) {
 | 
			
		||||
    // if there is an invalid sensor reading, ignore the change and keep the current state
 | 
			
		||||
    if (!std::isnan(sensor_value)) {
 | 
			
		||||
      this->publish_state(sensor_value >= (this->state ? this->lower_threshold_ : this->upper_threshold_));
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void AnalogThresholdBinarySensor::dump_config() {
 | 
			
		||||
  LOG_BINARY_SENSOR("", "Analog Threshold Binary Sensor", this);
 | 
			
		||||
  LOG_SENSOR("  ", "Sensor", this->sensor_);
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Upper threshold: %.11f", this->upper_threshold_);
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Lower threshold: %.11f", this->lower_threshold_);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace analog_threshold
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
@@ -0,0 +1,29 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/components/binary_sensor/binary_sensor.h"
 | 
			
		||||
#include "esphome/components/sensor/sensor.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace analog_threshold {
 | 
			
		||||
 | 
			
		||||
class AnalogThresholdBinarySensor : public Component, public binary_sensor::BinarySensor {
 | 
			
		||||
 public:
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
  void setup() override;
 | 
			
		||||
 | 
			
		||||
  float get_setup_priority() const override { return setup_priority::DATA; }
 | 
			
		||||
 | 
			
		||||
  void set_sensor(sensor::Sensor *analog_sensor);
 | 
			
		||||
  void set_upper_threshold(float threshold) { this->upper_threshold_ = threshold; }
 | 
			
		||||
  void set_lower_threshold(float threshold) { this->lower_threshold_ = threshold; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  sensor::Sensor *sensor_{nullptr};
 | 
			
		||||
 | 
			
		||||
  float upper_threshold_;
 | 
			
		||||
  float lower_threshold_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace analog_threshold
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										44
									
								
								esphome/components/analog_threshold/binary_sensor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								esphome/components/analog_threshold/binary_sensor.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,44 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.components import binary_sensor, sensor
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_SENSOR_ID,
 | 
			
		||||
    CONF_THRESHOLD,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
analog_threshold_ns = cg.esphome_ns.namespace("analog_threshold")
 | 
			
		||||
 | 
			
		||||
AnalogThresholdBinarySensor = analog_threshold_ns.class_(
 | 
			
		||||
    "AnalogThresholdBinarySensor", binary_sensor.BinarySensor, cg.Component
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CONF_UPPER = "upper"
 | 
			
		||||
CONF_LOWER = "lower"
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend(
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(): cv.declare_id(AnalogThresholdBinarySensor),
 | 
			
		||||
        cv.Required(CONF_SENSOR_ID): cv.use_id(sensor.Sensor),
 | 
			
		||||
        cv.Required(CONF_THRESHOLD): cv.Any(
 | 
			
		||||
            cv.float_,
 | 
			
		||||
            cv.Schema(
 | 
			
		||||
                {cv.Required(CONF_UPPER): cv.float_, cv.Required(CONF_LOWER): cv.float_}
 | 
			
		||||
            ),
 | 
			
		||||
        ),
 | 
			
		||||
    }
 | 
			
		||||
).extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = await binary_sensor.new_binary_sensor(config)
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
 | 
			
		||||
    sens = await cg.get_variable(config[CONF_SENSOR_ID])
 | 
			
		||||
    cg.add(var.set_sensor(sens))
 | 
			
		||||
 | 
			
		||||
    if isinstance(config[CONF_THRESHOLD], float):
 | 
			
		||||
        cg.add(var.set_upper_threshold(config[CONF_THRESHOLD]))
 | 
			
		||||
        cg.add(var.set_lower_threshold(config[CONF_THRESHOLD]))
 | 
			
		||||
    else:
 | 
			
		||||
        cg.add(var.set_upper_threshold(config[CONF_THRESHOLD][CONF_UPPER]))
 | 
			
		||||
        cg.add(var.set_lower_threshold(config[CONF_THRESHOLD][CONF_LOWER]))
 | 
			
		||||
@@ -92,6 +92,29 @@ async def to_code(config):
 | 
			
		||||
                data[pos] = pix[2]
 | 
			
		||||
                pos += 1
 | 
			
		||||
 | 
			
		||||
    elif config[CONF_TYPE] == "RGB565":
 | 
			
		||||
        data = [0 for _ in range(height * width * 2 * frames)]
 | 
			
		||||
        pos = 0
 | 
			
		||||
        for frameIndex in range(frames):
 | 
			
		||||
            image.seek(frameIndex)
 | 
			
		||||
            frame = image.convert("RGB")
 | 
			
		||||
            if CONF_RESIZE in config:
 | 
			
		||||
                frame = frame.resize([width, height])
 | 
			
		||||
            pixels = list(frame.getdata())
 | 
			
		||||
            if len(pixels) != height * width:
 | 
			
		||||
                raise core.EsphomeError(
 | 
			
		||||
                    f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height*width})"
 | 
			
		||||
                )
 | 
			
		||||
            for pix in pixels:
 | 
			
		||||
                R = pix[0] >> 3
 | 
			
		||||
                G = pix[1] >> 2
 | 
			
		||||
                B = pix[2] >> 3
 | 
			
		||||
                rgb = (R << 11) | (G << 5) | B
 | 
			
		||||
                data[pos] = rgb >> 8
 | 
			
		||||
                pos += 1
 | 
			
		||||
                data[pos] = rgb & 255
 | 
			
		||||
                pos += 1
 | 
			
		||||
 | 
			
		||||
    elif config[CONF_TYPE] == "BINARY":
 | 
			
		||||
        width8 = ((width + 7) // 8) * 8
 | 
			
		||||
        data = [0 for _ in range((height * width8 // 8) * frames)]
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.components import binary_sensor
 | 
			
		||||
from esphome.const import CONF_DIRECTION, CONF_DEVICE_CLASS, DEVICE_CLASS_MOVING
 | 
			
		||||
from esphome.const import CONF_DIRECTION, DEVICE_CLASS_MOVING
 | 
			
		||||
from . import APDS9960, CONF_APDS9960_ID
 | 
			
		||||
 | 
			
		||||
DEPENDENCIES = ["apds9960"]
 | 
			
		||||
@@ -13,13 +13,12 @@ DIRECTIONS = {
 | 
			
		||||
    "RIGHT": "set_right_direction",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend(
 | 
			
		||||
CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(
 | 
			
		||||
    device_class=DEVICE_CLASS_MOVING
 | 
			
		||||
).extend(
 | 
			
		||||
    {
 | 
			
		||||
        cv.Required(CONF_DIRECTION): cv.one_of(*DIRECTIONS, upper=True),
 | 
			
		||||
        cv.GenerateID(CONF_APDS9960_ID): cv.use_id(APDS9960),
 | 
			
		||||
        cv.Optional(
 | 
			
		||||
            CONF_DEVICE_CLASS, default=DEVICE_CLASS_MOVING
 | 
			
		||||
        ): binary_sensor.device_class,
 | 
			
		||||
        cv.Required(CONF_DIRECTION): cv.one_of(*DIRECTIONS, upper=True),
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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 {
 | 
			
		||||
@@ -23,7 +20,7 @@ static const char *const TAG = "api.connection";
 | 
			
		||||
static const int ESP32_CAMERA_STOP_STREAM = 5000;
 | 
			
		||||
 | 
			
		||||
APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *parent)
 | 
			
		||||
    : parent_(parent), initial_state_iterator_(parent, this), list_entities_iterator_(parent, this) {
 | 
			
		||||
    : parent_(parent), initial_state_iterator_(this), list_entities_iterator_(this) {
 | 
			
		||||
  this->proto_write_buffer_.reserve(64);
 | 
			
		||||
 | 
			
		||||
#if defined(USE_API_PLAINTEXT)
 | 
			
		||||
@@ -105,6 +102,7 @@ void APIConnection::loop() {
 | 
			
		||||
      ESP_LOGW(TAG, "%s didn't respond to ping request in time. Disconnecting...", this->client_info_.c_str());
 | 
			
		||||
    }
 | 
			
		||||
  } else if (now - this->last_traffic_ > keepalive) {
 | 
			
		||||
    ESP_LOGVV(TAG, "Sending keepalive PING...");
 | 
			
		||||
    this->sent_ping_ = true;
 | 
			
		||||
    this->send_ping_request(PingRequest());
 | 
			
		||||
  }
 | 
			
		||||
@@ -252,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;
 | 
			
		||||
@@ -267,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);
 | 
			
		||||
@@ -294,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);
 | 
			
		||||
@@ -304,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
 | 
			
		||||
@@ -908,7 +897,7 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint32_t message_type)
 | 
			
		||||
    }
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
  this->last_traffic_ = millis();
 | 
			
		||||
  // Do not set last_traffic_ on send
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
void APIConnection::on_unauthenticated_access() {
 | 
			
		||||
 
 | 
			
		||||
@@ -255,7 +255,7 @@ void APIServer::on_number_update(number::Number *obj, float state) {
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_SELECT
 | 
			
		||||
void APIServer::on_select_update(select::Select *obj, const std::string &state) {
 | 
			
		||||
void APIServer::on_select_update(select::Select *obj, const std::string &state, size_t index) {
 | 
			
		||||
  if (obj->is_internal())
 | 
			
		||||
    return;
 | 
			
		||||
  for (auto &c : this->clients_)
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,6 @@
 | 
			
		||||
#include "esphome/components/socket/socket.h"
 | 
			
		||||
#include "api_pb2.h"
 | 
			
		||||
#include "api_pb2_service.h"
 | 
			
		||||
#include "util.h"
 | 
			
		||||
#include "list_entities.h"
 | 
			
		||||
#include "subscribe_state.h"
 | 
			
		||||
#include "user_services.h"
 | 
			
		||||
@@ -65,7 +64,7 @@ class APIServer : public Component, public Controller {
 | 
			
		||||
  void on_number_update(number::Number *obj, float state) override;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SELECT
 | 
			
		||||
  void on_select_update(select::Select *obj, const std::string &state) override;
 | 
			
		||||
  void on_select_update(select::Select *obj, const std::string &state, size_t index) override;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_LOCK
 | 
			
		||||
  void on_lock_update(lock::Lock *obj) override;
 | 
			
		||||
 
 | 
			
		||||
@@ -21,7 +21,6 @@ async def async_run_logs(config, address):
 | 
			
		||||
    if CONF_ENCRYPTION in conf:
 | 
			
		||||
        noise_psk = conf[CONF_ENCRYPTION][CONF_KEY]
 | 
			
		||||
    _LOGGER.info("Starting log output from %s using esphome API", address)
 | 
			
		||||
    zc = zeroconf.Zeroconf()
 | 
			
		||||
    cli = APIClient(
 | 
			
		||||
        address,
 | 
			
		||||
        port,
 | 
			
		||||
 
 | 
			
		||||
@@ -40,8 +40,7 @@ bool ListEntitiesIterator::on_lock(lock::Lock *a_lock) { return this->client_->s
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
bool ListEntitiesIterator::on_end() { return this->client_->send_list_info_done(); }
 | 
			
		||||
ListEntitiesIterator::ListEntitiesIterator(APIServer *server, APIConnection *client)
 | 
			
		||||
    : ComponentIterator(server), client_(client) {}
 | 
			
		||||
ListEntitiesIterator::ListEntitiesIterator(APIConnection *client) : client_(client) {}
 | 
			
		||||
bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) {
 | 
			
		||||
  auto resp = service->encode_list_service_response();
 | 
			
		||||
  return this->client_->send_list_entities_services_response(resp);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/core/component_iterator.h"
 | 
			
		||||
#include "esphome/core/defines.h"
 | 
			
		||||
#include "util.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace api {
 | 
			
		||||
@@ -11,7 +11,7 @@ class APIConnection;
 | 
			
		||||
 | 
			
		||||
class ListEntitiesIterator : public ComponentIterator {
 | 
			
		||||
 public:
 | 
			
		||||
  ListEntitiesIterator(APIServer *server, APIConnection *client);
 | 
			
		||||
  ListEntitiesIterator(APIConnection *client);
 | 
			
		||||
#ifdef USE_BINARY_SENSOR
 | 
			
		||||
  bool on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) override;
 | 
			
		||||
#endif
 | 
			
		||||
@@ -60,5 +60,3 @@ class ListEntitiesIterator : public ComponentIterator {
 | 
			
		||||
 | 
			
		||||
}  // namespace api
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#include "api_server.h"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,4 @@
 | 
			
		||||
#include "proto.h"
 | 
			
		||||
#include "util.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
 
 | 
			
		||||
@@ -195,6 +195,20 @@ class ProtoWriteBuffer {
 | 
			
		||||
    this->write((value >> 16) & 0xFF);
 | 
			
		||||
    this->write((value >> 24) & 0xFF);
 | 
			
		||||
  }
 | 
			
		||||
  void encode_fixed64(uint32_t field_id, uint64_t value, bool force = false) {
 | 
			
		||||
    if (value == 0 && !force)
 | 
			
		||||
      return;
 | 
			
		||||
 | 
			
		||||
    this->encode_field_raw(field_id, 5);
 | 
			
		||||
    this->write((value >> 0) & 0xFF);
 | 
			
		||||
    this->write((value >> 8) & 0xFF);
 | 
			
		||||
    this->write((value >> 16) & 0xFF);
 | 
			
		||||
    this->write((value >> 24) & 0xFF);
 | 
			
		||||
    this->write((value >> 32) & 0xFF);
 | 
			
		||||
    this->write((value >> 40) & 0xFF);
 | 
			
		||||
    this->write((value >> 48) & 0xFF);
 | 
			
		||||
    this->write((value >> 56) & 0xFF);
 | 
			
		||||
  }
 | 
			
		||||
  template<typename T> void encode_enum(uint32_t field_id, T value, bool force = false) {
 | 
			
		||||
    this->encode_uint32(field_id, static_cast<uint32_t>(value), force);
 | 
			
		||||
  }
 | 
			
		||||
@@ -229,6 +243,15 @@ class ProtoWriteBuffer {
 | 
			
		||||
    }
 | 
			
		||||
    this->encode_uint32(field_id, uvalue, force);
 | 
			
		||||
  }
 | 
			
		||||
  void encode_sint64(uint32_t field_id, int64_t value, bool force = false) {
 | 
			
		||||
    uint64_t uvalue;
 | 
			
		||||
    if (value < 0) {
 | 
			
		||||
      uvalue = ~(value << 1);
 | 
			
		||||
    } else {
 | 
			
		||||
      uvalue = value << 1;
 | 
			
		||||
    }
 | 
			
		||||
    this->encode_uint64(field_id, uvalue, force);
 | 
			
		||||
  }
 | 
			
		||||
  template<class C> void encode_message(uint32_t field_id, const C &value, bool force = false) {
 | 
			
		||||
    this->encode_field_raw(field_id, 2);
 | 
			
		||||
    size_t begin = this->buffer_->size();
 | 
			
		||||
 
 | 
			
		||||
@@ -50,8 +50,7 @@ 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
 | 
			
		||||
InitialStateIterator::InitialStateIterator(APIServer *server, APIConnection *client)
 | 
			
		||||
    : ComponentIterator(server), client_(client) {}
 | 
			
		||||
InitialStateIterator::InitialStateIterator(APIConnection *client) : client_(client) {}
 | 
			
		||||
 | 
			
		||||
}  // namespace api
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,9 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/core/component_iterator.h"
 | 
			
		||||
#include "esphome/core/controller.h"
 | 
			
		||||
#include "esphome/core/defines.h"
 | 
			
		||||
#include "util.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace api {
 | 
			
		||||
@@ -12,7 +12,7 @@ class APIConnection;
 | 
			
		||||
 | 
			
		||||
class InitialStateIterator : public ComponentIterator {
 | 
			
		||||
 public:
 | 
			
		||||
  InitialStateIterator(APIServer *server, APIConnection *client);
 | 
			
		||||
  InitialStateIterator(APIConnection *client);
 | 
			
		||||
#ifdef USE_BINARY_SENSOR
 | 
			
		||||
  bool on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) override;
 | 
			
		||||
#endif
 | 
			
		||||
@@ -55,5 +55,3 @@ class InitialStateIterator : public ComponentIterator {
 | 
			
		||||
 | 
			
		||||
}  // namespace api
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#include "api_server.h"
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,7 @@ from . import AS3935, CONF_AS3935_ID
 | 
			
		||||
 | 
			
		||||
DEPENDENCIES = ["as3935"]
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend(
 | 
			
		||||
CONFIG_SCHEMA = binary_sensor.binary_sensor_schema().extend(
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(CONF_AS3935_ID): cv.use_id(AS3935),
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,6 @@ from esphome.components import sensor
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_DISTANCE,
 | 
			
		||||
    CONF_LIGHTNING_ENERGY,
 | 
			
		||||
    STATE_CLASS_NONE,
 | 
			
		||||
    UNIT_KILOMETER,
 | 
			
		||||
    ICON_SIGNAL_DISTANCE_VARIANT,
 | 
			
		||||
    ICON_FLASH,
 | 
			
		||||
@@ -20,12 +19,10 @@ CONFIG_SCHEMA = cv.Schema(
 | 
			
		||||
            unit_of_measurement=UNIT_KILOMETER,
 | 
			
		||||
            icon=ICON_SIGNAL_DISTANCE_VARIANT,
 | 
			
		||||
            accuracy_decimals=1,
 | 
			
		||||
            state_class=STATE_CLASS_NONE,
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional(CONF_LIGHTNING_ENERGY): sensor.sensor_schema(
 | 
			
		||||
            icon=ICON_FLASH,
 | 
			
		||||
            accuracy_decimals=1,
 | 
			
		||||
            state_class=STATE_CLASS_NONE,
 | 
			
		||||
        ),
 | 
			
		||||
    }
 | 
			
		||||
).extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
 
 | 
			
		||||
@@ -38,7 +38,7 @@ bool BParasite::parse_device(const esp32_ble_tracker::ESPBTDevice &device) {
 | 
			
		||||
  const auto &data = service_data.data;
 | 
			
		||||
 | 
			
		||||
  const uint8_t protocol_version = data[0] >> 4;
 | 
			
		||||
  if (protocol_version != 1) {
 | 
			
		||||
  if (protocol_version != 1 && protocol_version != 2) {
 | 
			
		||||
    ESP_LOGE(TAG, "Unsupported protocol version: %u", protocol_version);
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
@@ -57,9 +57,15 @@ bool BParasite::parse_device(const esp32_ble_tracker::ESPBTDevice &device) {
 | 
			
		||||
  uint16_t battery_millivolt = data[2] << 8 | data[3];
 | 
			
		||||
  float battery_voltage = battery_millivolt / 1000.0f;
 | 
			
		||||
 | 
			
		||||
  // Temperature in 1000 * Celsius.
 | 
			
		||||
  uint16_t temp_millicelcius = data[4] << 8 | data[5];
 | 
			
		||||
  float temp_celcius = temp_millicelcius / 1000.0f;
 | 
			
		||||
  // Temperature in 1000 * Celsius (protocol v1) or 100 * Celsius (protocol v2).
 | 
			
		||||
  float temp_celsius;
 | 
			
		||||
  if (protocol_version == 1) {
 | 
			
		||||
    uint16_t temp_millicelsius = data[4] << 8 | data[5];
 | 
			
		||||
    temp_celsius = temp_millicelsius / 1000.0f;
 | 
			
		||||
  } else {
 | 
			
		||||
    int16_t temp_centicelsius = data[4] << 8 | data[5];
 | 
			
		||||
    temp_celsius = temp_centicelsius / 100.0f;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Relative air humidity in the range [0, 2^16).
 | 
			
		||||
  uint16_t humidity = data[6] << 8 | data[7];
 | 
			
		||||
@@ -76,7 +82,7 @@ bool BParasite::parse_device(const esp32_ble_tracker::ESPBTDevice &device) {
 | 
			
		||||
    battery_voltage_->publish_state(battery_voltage);
 | 
			
		||||
  }
 | 
			
		||||
  if (temperature_ != nullptr) {
 | 
			
		||||
    temperature_->publish_state(temp_celcius);
 | 
			
		||||
    temperature_->publish_state(temp_celsius);
 | 
			
		||||
  }
 | 
			
		||||
  if (humidity_ != nullptr) {
 | 
			
		||||
    humidity_->publish_state(humidity_percent);
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								esphome/components/bedjet/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								esphome/components/bedjet/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
CODEOWNERS = ["@jhansche"]
 | 
			
		||||
							
								
								
									
										644
									
								
								esphome/components/bedjet/bedjet.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										644
									
								
								esphome/components/bedjet/bedjet.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,644 @@
 | 
			
		||||
#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
 | 
			
		||||
							
								
								
									
										124
									
								
								esphome/components/bedjet/bedjet.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								esphome/components/bedjet/bedjet.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,124 @@
 | 
			
		||||
#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
 | 
			
		||||
 | 
			
		||||
#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 {
 | 
			
		||||
 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; }
 | 
			
		||||
 | 
			
		||||
  /** Attempts to check for and apply firmware updates. */
 | 
			
		||||
  void upgrade_firmware();
 | 
			
		||||
 | 
			
		||||
  climate::ClimateTraits traits() override {
 | 
			
		||||
    auto traits = climate::ClimateTraits();
 | 
			
		||||
    traits.set_supports_action(true);
 | 
			
		||||
    traits.set_supports_current_temperature(true);
 | 
			
		||||
    traits.set_supported_modes({
 | 
			
		||||
        climate::CLIMATE_MODE_OFF,
 | 
			
		||||
        climate::CLIMATE_MODE_HEAT,
 | 
			
		||||
        // climate::CLIMATE_MODE_TURBO // Not supported by Climate: see presets instead
 | 
			
		||||
        climate::CLIMATE_MODE_FAN_ONLY,
 | 
			
		||||
        climate::CLIMATE_MODE_DRY,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // It would be better if we had a slider for the fan modes.
 | 
			
		||||
    traits.set_supported_custom_fan_modes(BEDJET_FAN_STEP_NAMES_SET);
 | 
			
		||||
    traits.set_supported_presets({
 | 
			
		||||
        // If we support NONE, then have to decide what happens if the user switches to it (turn off?)
 | 
			
		||||
        // climate::CLIMATE_PRESET_NONE,
 | 
			
		||||
        // Climate doesn't have a "TURBO" mode, but we can use the BOOST preset instead.
 | 
			
		||||
        climate::CLIMATE_PRESET_BOOST,
 | 
			
		||||
    });
 | 
			
		||||
    traits.set_supported_custom_presets({
 | 
			
		||||
        // We could fetch biodata from bedjet and set these names that way.
 | 
			
		||||
        // But then we have to invert the lookup in order to send the right preset.
 | 
			
		||||
        // For now, we can leave them as M1-3 to match the remote buttons.
 | 
			
		||||
        // EXT HT added to match remote button.
 | 
			
		||||
        "EXT HT",
 | 
			
		||||
        "M1",
 | 
			
		||||
        "M2",
 | 
			
		||||
        "M3",
 | 
			
		||||
    });
 | 
			
		||||
    traits.set_visual_min_temperature(19.0);
 | 
			
		||||
    traits.set_visual_max_temperature(43.0);
 | 
			
		||||
    traits.set_visual_temperature_step(1.0);
 | 
			
		||||
    return traits;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void control(const climate::ClimateCall &call) override;
 | 
			
		||||
 | 
			
		||||
#ifdef USE_TIME
 | 
			
		||||
  void setup_time_();
 | 
			
		||||
  void send_local_time_();
 | 
			
		||||
  optional<time::RealTimeClock *> time_id_{};
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  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_();
 | 
			
		||||
 | 
			
		||||
  bool is_valid_() {
 | 
			
		||||
    // FIXME: find a better way to check this?
 | 
			
		||||
    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
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
							
								
								
									
										123
									
								
								esphome/components/bedjet/bedjet_base.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								esphome/components/bedjet/bedjet_base.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,123 @@
 | 
			
		||||
#include "bedjet_base.h"
 | 
			
		||||
#include <cstdio>
 | 
			
		||||
#include <cstring>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace bedjet {
 | 
			
		||||
 | 
			
		||||
/// Converts a BedJet temp step into degrees Fahrenheit.
 | 
			
		||||
float bedjet_temp_to_f(const uint8_t temp) {
 | 
			
		||||
  // BedJet temp is "C*2"; to get F, multiply by 0.9 (half 1.8) and add 32.
 | 
			
		||||
  return 0.9f * temp + 32.0f;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** Cleans up the packet before sending. */
 | 
			
		||||
BedjetPacket *BedjetCodec::clean_packet_() {
 | 
			
		||||
  // So far no commands require more than 2 bytes of data.
 | 
			
		||||
  assert(this->packet_.data_length <= 2);
 | 
			
		||||
  for (int i = this->packet_.data_length; i < 2; i++) {
 | 
			
		||||
    this->packet_.data[i] = '\0';
 | 
			
		||||
  }
 | 
			
		||||
  ESP_LOGV(TAG, "Created packet: %02X, %02X %02X", this->packet_.command, this->packet_.data[0], this->packet_.data[1]);
 | 
			
		||||
  return &this->packet_;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** Returns a BedjetPacket that will initiate a BedjetButton press. */
 | 
			
		||||
BedjetPacket *BedjetCodec::get_button_request(BedjetButton button) {
 | 
			
		||||
  this->packet_.command = CMD_BUTTON;
 | 
			
		||||
  this->packet_.data_length = 1;
 | 
			
		||||
  this->packet_.data[0] = button;
 | 
			
		||||
  return this->clean_packet_();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** Returns a BedjetPacket that will set the device's target `temperature`. */
 | 
			
		||||
BedjetPacket *BedjetCodec::get_set_target_temp_request(float temperature) {
 | 
			
		||||
  this->packet_.command = CMD_SET_TEMP;
 | 
			
		||||
  this->packet_.data_length = 1;
 | 
			
		||||
  this->packet_.data[0] = temperature * 2;
 | 
			
		||||
  return this->clean_packet_();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** Returns a BedjetPacket that will set the device's target fan speed. */
 | 
			
		||||
BedjetPacket *BedjetCodec::get_set_fan_speed_request(const uint8_t fan_step) {
 | 
			
		||||
  this->packet_.command = CMD_SET_FAN;
 | 
			
		||||
  this->packet_.data_length = 1;
 | 
			
		||||
  this->packet_.data[0] = fan_step;
 | 
			
		||||
  return this->clean_packet_();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 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_.data_length = 2;
 | 
			
		||||
  this->packet_.data[0] = hour;
 | 
			
		||||
  this->packet_.data[1] = minute;
 | 
			
		||||
  return this->clean_packet_();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** 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]);
 | 
			
		||||
  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));
 | 
			
		||||
  } 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);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** Decodes the incoming status packet received on the BEDJET_STATUS_UUID.
 | 
			
		||||
 *
 | 
			
		||||
 * @return `true` if the packet was decoded and represents a "partial" packet; `false` otherwise.
 | 
			
		||||
 */
 | 
			
		||||
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
 | 
			
		||||
    memcpy(&this->buf_, data, 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) {
 | 
			
		||||
      // and save it for the update() loop
 | 
			
		||||
      this->status_packet_ = this->buf_;
 | 
			
		||||
      return this->buf_.is_partial == 1;
 | 
			
		||||
    } else {
 | 
			
		||||
      // 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]);
 | 
			
		||||
 | 
			
		||||
    if (this->has_status()) {
 | 
			
		||||
      this->status_packet_->ambient_temp_step = data[6];
 | 
			
		||||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    // TODO: log a warning if we detect that we connected to a non-V3 device.
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace bedjet
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										159
									
								
								esphome/components/bedjet/bedjet_base.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										159
									
								
								esphome/components/bedjet/bedjet_base.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,159 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
#include "bedjet_const.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace bedjet {
 | 
			
		||||
 | 
			
		||||
struct BedjetPacket {
 | 
			
		||||
  uint8_t data_length;
 | 
			
		||||
  BedjetCommand command;
 | 
			
		||||
  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
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
enum BedjetPacketType : uint8_t {
 | 
			
		||||
  PACKET_TYPE_STATUS = 0x1,
 | 
			
		||||
  PACKET_TYPE_DEBUG = 0x2,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/** 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.
 | 
			
		||||
  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.
 | 
			
		||||
  BedjetPacketType packet_type : 8;  ///< Typically BedjetPacketType::PACKET_TYPE_STATUS for BedJet V3 status packet.
 | 
			
		||||
 | 
			
		||||
  // [4]
 | 
			
		||||
  uint8_t time_remaining_hrs : 8;   ///< Hours remaining in program runtime
 | 
			
		||||
  uint8_t time_remaining_mins : 8;  ///< Minutes remaining in program runtime
 | 
			
		||||
  uint8_t time_remaining_secs : 8;  ///< Seconds remaining in program runtime
 | 
			
		||||
 | 
			
		||||
  // [7]
 | 
			
		||||
  uint8_t actual_temp_step : 8;  ///< Actual temp of the air blown by the BedJet fan; value represents `2 *
 | 
			
		||||
                                 ///< degrees_celsius`. See #bedjet_temp_to_c and #bedjet_temp_to_f
 | 
			
		||||
  uint8_t target_temp_step : 8;  ///< Target temp that the BedJet will try to heat to. See #actual_temp_step.
 | 
			
		||||
 | 
			
		||||
  // [9]
 | 
			
		||||
  BedjetMode mode : 8;  ///< BedJet operating mode.
 | 
			
		||||
 | 
			
		||||
  // [10]
 | 
			
		||||
  uint8_t fan_step : 8;  ///< BedJet fan speed; value is in the 0-19 range, representing 5% increments (5%-100%): `5 + 5
 | 
			
		||||
                         ///< * fan_step`
 | 
			
		||||
  uint8_t max_hrs : 8;   ///< Max hours of mode runtime
 | 
			
		||||
  uint8_t max_mins : 8;  ///< Max minutes of mode runtime
 | 
			
		||||
  uint8_t min_temp_step : 8;  ///< Min temp allowed in mode. See #actual_temp_step.
 | 
			
		||||
  uint8_t max_temp_step : 8;  ///< Max temp allowed in mode. See #actual_temp_step.
 | 
			
		||||
 | 
			
		||||
  // [15-16]
 | 
			
		||||
  uint16_t turbo_time : 16;  ///< Time remaining in BedjetMode::MODE_TURBO.
 | 
			
		||||
 | 
			
		||||
  // [17]
 | 
			
		||||
  uint8_t ambient_temp_step : 8;  ///< Current ambient air temp. This is the coldest air the BedJet can blow. See
 | 
			
		||||
                                  ///< #actual_temp_step.
 | 
			
		||||
  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
 | 
			
		||||
 | 
			
		||||
  // [26]
 | 
			
		||||
  //   0x18(24) = "Connection test has completed OK"
 | 
			
		||||
  //   0x1a(26) = "Firmware update is not needed"
 | 
			
		||||
  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
 | 
			
		||||
 | 
			
		||||
} __attribute__((packed));
 | 
			
		||||
 | 
			
		||||
/** This class is responsible for encoding command packets and decoding status packets.
 | 
			
		||||
 *
 | 
			
		||||
 * Status Packets
 | 
			
		||||
 * ==============
 | 
			
		||||
 * The BedJet protocol depends on registering for notifications on the esphome::BedJet::BEDJET_SERVICE_UUID
 | 
			
		||||
 * characteristic. If the BedJet is on, it will send rapid updates as notifications. If it is off,
 | 
			
		||||
 * it generally will not notify of any status.
 | 
			
		||||
 *
 | 
			
		||||
 * As the BedJet V3's BedjetStatusPacket exceeds the buffer size allowed for BLE notification packets,
 | 
			
		||||
 * the notification packet will contain `BedjetStatusPacket::is_partial == 1`. When that happens, an additional
 | 
			
		||||
 * read of the esphome::BedJet::BEDJET_SERVICE_UUID characteristic will contain the second portion of the
 | 
			
		||||
 * full status packet.
 | 
			
		||||
 *
 | 
			
		||||
 * Command Packets
 | 
			
		||||
 * ===============
 | 
			
		||||
 * This class supports encoding a number of BedjetPacket commands:
 | 
			
		||||
 * - Button press
 | 
			
		||||
 *   This simulates a press of one of the BedjetButton values.
 | 
			
		||||
 *   - BedjetPacket#command = BedjetCommand::CMD_BUTTON
 | 
			
		||||
 *   - BedjetPacket#data [0] contains the BedjetButton value
 | 
			
		||||
 * - Set target temp
 | 
			
		||||
 *   This sets the BedJet's target temp to a concrete temperature value.
 | 
			
		||||
 *   - BedjetPacket#command = BedjetCommand::CMD_SET_TEMP
 | 
			
		||||
 *   - BedjetPacket#data [0] contains the BedJet temp value; see BedjetStatusPacket#actual_temp_step
 | 
			
		||||
 * - Set fan speed
 | 
			
		||||
 *   This sets the BedJet fan speed.
 | 
			
		||||
 *   - BedjetPacket#command = BedjetCommand::CMD_SET_FAN
 | 
			
		||||
 *   - BedjetPacket#data [0] contains the BedJet fan step in the range 0-19.
 | 
			
		||||
 * - 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#data [0] is hours, [1] is minutes
 | 
			
		||||
 */
 | 
			
		||||
class BedjetCodec {
 | 
			
		||||
 public:
 | 
			
		||||
  BedjetPacket *get_button_request(BedjetButton button);
 | 
			
		||||
  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);
 | 
			
		||||
 | 
			
		||||
  bool decode_notify(const uint8_t *data, uint16_t length);
 | 
			
		||||
  void decode_extra(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(); }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  BedjetPacket *clean_packet_();
 | 
			
		||||
 | 
			
		||||
  uint8_t last_buffer_size_ = 0;
 | 
			
		||||
 | 
			
		||||
  BedjetPacket packet_;
 | 
			
		||||
 | 
			
		||||
  optional<BedjetStatusPacket> status_packet_;
 | 
			
		||||
  BedjetStatusPacket buf_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace bedjet
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										78
									
								
								esphome/components/bedjet/bedjet_const.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								esphome/components/bedjet/bedjet_const.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,78 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include <set>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace bedjet {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "bedjet";
 | 
			
		||||
 | 
			
		||||
enum BedjetMode : uint8_t {
 | 
			
		||||
  /// BedJet is Off
 | 
			
		||||
  MODE_STANDBY = 0,
 | 
			
		||||
  /// BedJet is in Heat mode (limited to 4 hours)
 | 
			
		||||
  MODE_HEAT = 1,
 | 
			
		||||
  /// BedJet is in Turbo mode (high heat, limited time)
 | 
			
		||||
  MODE_TURBO = 2,
 | 
			
		||||
  /// BedJet is in Extended Heat mode (limited to 10 hours)
 | 
			
		||||
  MODE_EXTHT = 3,
 | 
			
		||||
  /// BedJet is in Cool mode (actually "Fan only" mode)
 | 
			
		||||
  MODE_COOL = 4,
 | 
			
		||||
  /// BedJet is in Dry mode (high speed, no heat)
 | 
			
		||||
  MODE_DRY = 5,
 | 
			
		||||
  /// BedJet is in "wait" mode, a step during a biorhythm program
 | 
			
		||||
  MODE_WAIT = 6,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
enum BedjetButton : uint8_t {
 | 
			
		||||
  /// Turn BedJet off
 | 
			
		||||
  BTN_OFF = 0x1,
 | 
			
		||||
  /// Enter Cool mode (fan only)
 | 
			
		||||
  BTN_COOL = 0x2,
 | 
			
		||||
  /// Enter Heat mode (limited to 4 hours)
 | 
			
		||||
  BTN_HEAT = 0x3,
 | 
			
		||||
  /// Enter Turbo mode (high heat, limited to 10 minutes)
 | 
			
		||||
  BTN_TURBO = 0x4,
 | 
			
		||||
  /// Enter Dry mode (high speed, no heat)
 | 
			
		||||
  BTN_DRY = 0x5,
 | 
			
		||||
  /// Enter Extended Heat mode (limited to 10 hours)
 | 
			
		||||
  BTN_EXTHT = 0x6,
 | 
			
		||||
 | 
			
		||||
  /// Start the M1 biorhythm/preset program
 | 
			
		||||
  BTN_M1 = 0x20,
 | 
			
		||||
  /// Start the M2 biorhythm/preset program
 | 
			
		||||
  BTN_M2 = 0x21,
 | 
			
		||||
  /// Start the M3 biorhythm/preset program
 | 
			
		||||
  BTN_M3 = 0x22,
 | 
			
		||||
 | 
			
		||||
  /* These are "MAGIC" buttons */
 | 
			
		||||
 | 
			
		||||
  /// Turn debug mode on/off
 | 
			
		||||
  MAGIC_DEBUG_ON = 0x40,
 | 
			
		||||
  MAGIC_DEBUG_OFF = 0x41,
 | 
			
		||||
  /// Perform a connection test.
 | 
			
		||||
  MAGIC_CONNTEST = 0x42,
 | 
			
		||||
  /// Request a firmware update. This will also restart the Bedjet.
 | 
			
		||||
  MAGIC_UPDATE = 0x43,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
enum BedjetCommand : uint8_t {
 | 
			
		||||
  CMD_BUTTON = 0x1,
 | 
			
		||||
  CMD_SET_TEMP = 0x3,
 | 
			
		||||
  CMD_STATUS = 0x6,
 | 
			
		||||
  CMD_SET_FAN = 0x7,
 | 
			
		||||
  CMD_SET_TIME = 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%" \
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
static const char *const BEDJET_FAN_STEP_NAMES[20] = BEDJET_FAN_STEP_NAMES_;
 | 
			
		||||
static const std::string BEDJET_FAN_STEP_NAME_STRINGS[20] = BEDJET_FAN_STEP_NAMES_;
 | 
			
		||||
static const std::set<std::string> BEDJET_FAN_STEP_NAMES_SET BEDJET_FAN_STEP_NAMES_;
 | 
			
		||||
 | 
			
		||||
}  // namespace bedjet
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										42
									
								
								esphome/components/bedjet/climate.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								esphome/components/bedjet/climate.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,42 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.components import climate, ble_client, time
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_RECEIVE_TIMEOUT,
 | 
			
		||||
    CONF_TIME_ID,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CODEOWNERS = ["@jhansche"]
 | 
			
		||||
DEPENDENCIES = ["ble_client"]
 | 
			
		||||
 | 
			
		||||
bedjet_ns = cg.esphome_ns.namespace("bedjet")
 | 
			
		||||
Bedjet = bedjet_ns.class_(
 | 
			
		||||
    "Bedjet", climate.Climate, ble_client.BLEClientNode, cg.PollingComponent
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
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,
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
    .extend(ble_client.BLE_CLIENT_SCHEMA)
 | 
			
		||||
    .extend(cv.polling_component_schema("30s"))
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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]))
 | 
			
		||||
@@ -9,18 +9,109 @@ static const char *const TAG = "bh1750.sensor";
 | 
			
		||||
static const uint8_t BH1750_COMMAND_POWER_ON = 0b00000001;
 | 
			
		||||
static const uint8_t BH1750_COMMAND_MT_REG_HI = 0b01000000;  // last 3 bits
 | 
			
		||||
static const uint8_t BH1750_COMMAND_MT_REG_LO = 0b01100000;  // last 5 bits
 | 
			
		||||
static const uint8_t BH1750_COMMAND_ONE_TIME_L = 0b00100011;
 | 
			
		||||
static const uint8_t BH1750_COMMAND_ONE_TIME_H = 0b00100000;
 | 
			
		||||
static const uint8_t BH1750_COMMAND_ONE_TIME_H2 = 0b00100001;
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
bh1750 properties:
 | 
			
		||||
 | 
			
		||||
L-resolution mode:
 | 
			
		||||
- resolution 4lx (@ mtreg=69)
 | 
			
		||||
- measurement time: typ=16ms, max=24ms, scaled by MTreg value divided by 69
 | 
			
		||||
- formula: counts / 1.2 * (69 / MTreg) lx
 | 
			
		||||
H-resolution mode:
 | 
			
		||||
- resolution 1lx (@ mtreg=69)
 | 
			
		||||
- measurement time: typ=120ms, max=180ms, scaled by MTreg value divided by 69
 | 
			
		||||
- formula: counts / 1.2 * (69 / MTreg) lx
 | 
			
		||||
H-resolution mode2:
 | 
			
		||||
- resolution 0.5lx (@ mtreg=69)
 | 
			
		||||
- measurement time: typ=120ms, max=180ms, scaled by MTreg value divided by 69
 | 
			
		||||
- formula: counts / 1.2 * (69 / MTreg) / 2 lx
 | 
			
		||||
 | 
			
		||||
MTreg:
 | 
			
		||||
- min=31, default=69, max=254
 | 
			
		||||
 | 
			
		||||
-> only reason to use l-resolution is faster, but offers no higher range
 | 
			
		||||
-> below ~7000lx, makes sense to use H-resolution2 @ MTreg=254
 | 
			
		||||
-> try to maximize MTreg to get lowest noise level
 | 
			
		||||
*/
 | 
			
		||||
 | 
			
		||||
void BH1750Sensor::setup() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Setting up BH1750 '%s'...", this->name_.c_str());
 | 
			
		||||
  if (!this->write_bytes(BH1750_COMMAND_POWER_ON, nullptr, 0)) {
 | 
			
		||||
  uint8_t turn_on = BH1750_COMMAND_POWER_ON;
 | 
			
		||||
  if (this->write(&turn_on, 1) != i2c::ERROR_OK) {
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
  uint8_t mtreg_hi = (this->measurement_duration_ >> 5) & 0b111;
 | 
			
		||||
  uint8_t mtreg_lo = (this->measurement_duration_ >> 0) & 0b11111;
 | 
			
		||||
  this->write_bytes(BH1750_COMMAND_MT_REG_HI | mtreg_hi, nullptr, 0);
 | 
			
		||||
  this->write_bytes(BH1750_COMMAND_MT_REG_LO | mtreg_lo, nullptr, 0);
 | 
			
		||||
void BH1750Sensor::read_lx_(BH1750Mode mode, uint8_t mtreg, const std::function<void(float)> &f) {
 | 
			
		||||
  // turn on (after one-shot sensor automatically powers down)
 | 
			
		||||
  uint8_t turn_on = BH1750_COMMAND_POWER_ON;
 | 
			
		||||
  if (this->write(&turn_on, 1) != i2c::ERROR_OK) {
 | 
			
		||||
    ESP_LOGW(TAG, "Turning on BH1750 failed");
 | 
			
		||||
    f(NAN);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (active_mtreg_ != mtreg) {
 | 
			
		||||
    // set mtreg
 | 
			
		||||
    uint8_t mtreg_hi = BH1750_COMMAND_MT_REG_HI | ((mtreg >> 5) & 0b111);
 | 
			
		||||
    uint8_t mtreg_lo = BH1750_COMMAND_MT_REG_LO | ((mtreg >> 0) & 0b11111);
 | 
			
		||||
    if (this->write(&mtreg_hi, 1) != i2c::ERROR_OK || this->write(&mtreg_lo, 1) != i2c::ERROR_OK) {
 | 
			
		||||
      ESP_LOGW(TAG, "Setting measurement time for BH1750 failed");
 | 
			
		||||
      active_mtreg_ = 0;
 | 
			
		||||
      f(NAN);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    active_mtreg_ = mtreg;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  uint8_t cmd;
 | 
			
		||||
  uint16_t meas_time;
 | 
			
		||||
  switch (mode) {
 | 
			
		||||
    case BH1750_MODE_L:
 | 
			
		||||
      cmd = BH1750_COMMAND_ONE_TIME_L;
 | 
			
		||||
      meas_time = 24 * mtreg / 69;
 | 
			
		||||
      break;
 | 
			
		||||
    case BH1750_MODE_H:
 | 
			
		||||
      cmd = BH1750_COMMAND_ONE_TIME_H;
 | 
			
		||||
      meas_time = 180 * mtreg / 69;
 | 
			
		||||
      break;
 | 
			
		||||
    case BH1750_MODE_H2:
 | 
			
		||||
      cmd = BH1750_COMMAND_ONE_TIME_H2;
 | 
			
		||||
      meas_time = 180 * mtreg / 69;
 | 
			
		||||
      break;
 | 
			
		||||
    default:
 | 
			
		||||
      f(NAN);
 | 
			
		||||
      return;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->write(&cmd, 1) != i2c::ERROR_OK) {
 | 
			
		||||
    ESP_LOGW(TAG, "Starting measurement for BH1750 failed");
 | 
			
		||||
    f(NAN);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // probably not needed, but adjust for rounding
 | 
			
		||||
  meas_time++;
 | 
			
		||||
 | 
			
		||||
  this->set_timeout("read", meas_time, [this, mode, mtreg, f]() {
 | 
			
		||||
    uint16_t raw_value;
 | 
			
		||||
    if (this->read(reinterpret_cast<uint8_t *>(&raw_value), 2) != i2c::ERROR_OK) {
 | 
			
		||||
      ESP_LOGW(TAG, "Reading BH1750 data failed");
 | 
			
		||||
      f(NAN);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    raw_value = i2c::i2ctohs(raw_value);
 | 
			
		||||
 | 
			
		||||
    float lx = float(raw_value) / 1.2f;
 | 
			
		||||
    lx *= 69.0f / mtreg;
 | 
			
		||||
    if (mode == BH1750_MODE_H2)
 | 
			
		||||
      lx /= 2.0f;
 | 
			
		||||
 | 
			
		||||
    f(lx);
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BH1750Sensor::dump_config() {
 | 
			
		||||
@@ -30,64 +121,49 @@ void BH1750Sensor::dump_config() {
 | 
			
		||||
    ESP_LOGE(TAG, "Communication with BH1750 failed!");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const char *resolution_s;
 | 
			
		||||
  switch (this->resolution_) {
 | 
			
		||||
    case BH1750_RESOLUTION_0P5_LX:
 | 
			
		||||
      resolution_s = "0.5";
 | 
			
		||||
      break;
 | 
			
		||||
    case BH1750_RESOLUTION_1P0_LX:
 | 
			
		||||
      resolution_s = "1";
 | 
			
		||||
      break;
 | 
			
		||||
    case BH1750_RESOLUTION_4P0_LX:
 | 
			
		||||
      resolution_s = "4";
 | 
			
		||||
      break;
 | 
			
		||||
    default:
 | 
			
		||||
      resolution_s = "Unknown";
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Resolution: %s", resolution_s);
 | 
			
		||||
  LOG_UPDATE_INTERVAL(this);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BH1750Sensor::update() {
 | 
			
		||||
  if (!this->write_bytes(this->resolution_, nullptr, 0))
 | 
			
		||||
    return;
 | 
			
		||||
  // first do a quick measurement in L-mode with full range
 | 
			
		||||
  // to find right range
 | 
			
		||||
  this->read_lx_(BH1750_MODE_L, 31, [this](float val) {
 | 
			
		||||
    if (std::isnan(val)) {
 | 
			
		||||
      this->status_set_warning();
 | 
			
		||||
      this->publish_state(NAN);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
  uint32_t wait = 0;
 | 
			
		||||
  // use max conversion times
 | 
			
		||||
  switch (this->resolution_) {
 | 
			
		||||
    case BH1750_RESOLUTION_0P5_LX:
 | 
			
		||||
    case BH1750_RESOLUTION_1P0_LX:
 | 
			
		||||
      wait = 180;
 | 
			
		||||
      break;
 | 
			
		||||
    case BH1750_RESOLUTION_4P0_LX:
 | 
			
		||||
      wait = 24;
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
    BH1750Mode use_mode;
 | 
			
		||||
    uint8_t use_mtreg;
 | 
			
		||||
    if (val <= 7000) {
 | 
			
		||||
      use_mode = BH1750_MODE_H2;
 | 
			
		||||
      use_mtreg = 254;
 | 
			
		||||
    } else {
 | 
			
		||||
      use_mode = BH1750_MODE_H;
 | 
			
		||||
      // lx = counts / 1.2 * (69 / mtreg)
 | 
			
		||||
      // -> mtreg = counts / 1.2 * (69 / lx)
 | 
			
		||||
      // calculate for counts=50000 (allow some range to not saturate, but maximize mtreg)
 | 
			
		||||
      // -> mtreg = 50000*(10/12)*(69/lx)
 | 
			
		||||
      int ideal_mtreg = 50000 * 10 * 69 / (12 * (int) val);
 | 
			
		||||
      use_mtreg = std::min(254, std::max(31, ideal_mtreg));
 | 
			
		||||
    }
 | 
			
		||||
    ESP_LOGV(TAG, "L result: %f -> Calculated mode=%d, mtreg=%d", val, (int) use_mode, use_mtreg);
 | 
			
		||||
 | 
			
		||||
  this->set_timeout("illuminance", wait, [this]() { this->read_data_(); });
 | 
			
		||||
    this->read_lx_(use_mode, use_mtreg, [this](float val) {
 | 
			
		||||
      if (std::isnan(val)) {
 | 
			
		||||
        this->status_set_warning();
 | 
			
		||||
        this->publish_state(NAN);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      ESP_LOGD(TAG, "'%s': Got illuminance=%.1flx", this->get_name().c_str(), val);
 | 
			
		||||
      this->status_clear_warning();
 | 
			
		||||
      this->publish_state(val);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
float BH1750Sensor::get_setup_priority() const { return setup_priority::DATA; }
 | 
			
		||||
void BH1750Sensor::read_data_() {
 | 
			
		||||
  uint16_t raw_value;
 | 
			
		||||
  if (this->read(reinterpret_cast<uint8_t *>(&raw_value), 2) != i2c::ERROR_OK) {
 | 
			
		||||
    this->status_set_warning();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  raw_value = i2c::i2ctohs(raw_value);
 | 
			
		||||
 | 
			
		||||
  float lx = float(raw_value) / 1.2f;
 | 
			
		||||
  lx *= 69.0f / this->measurement_duration_;
 | 
			
		||||
  if (this->resolution_ == BH1750_RESOLUTION_0P5_LX) {
 | 
			
		||||
    lx /= 2.0f;
 | 
			
		||||
  }
 | 
			
		||||
  ESP_LOGD(TAG, "'%s': Got illuminance=%.1flx", this->get_name().c_str(), lx);
 | 
			
		||||
  this->publish_state(lx);
 | 
			
		||||
  this->status_clear_warning();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BH1750Sensor::set_resolution(BH1750Resolution resolution) { this->resolution_ = resolution; }
 | 
			
		||||
 | 
			
		||||
}  // namespace bh1750
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 
 | 
			
		||||
@@ -7,29 +7,15 @@
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace bh1750 {
 | 
			
		||||
 | 
			
		||||
/// Enum listing all resolutions that can be used with the BH1750
 | 
			
		||||
enum BH1750Resolution {
 | 
			
		||||
  BH1750_RESOLUTION_4P0_LX = 0b00100011,  // one-time low resolution mode
 | 
			
		||||
  BH1750_RESOLUTION_1P0_LX = 0b00100000,  // one-time high resolution mode 1
 | 
			
		||||
  BH1750_RESOLUTION_0P5_LX = 0b00100001,  // one-time high resolution mode 2
 | 
			
		||||
enum BH1750Mode {
 | 
			
		||||
  BH1750_MODE_L,
 | 
			
		||||
  BH1750_MODE_H,
 | 
			
		||||
  BH1750_MODE_H2,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/// This class implements support for the i2c-based BH1750 ambient light sensor.
 | 
			
		||||
class BH1750Sensor : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice {
 | 
			
		||||
 public:
 | 
			
		||||
  /** Set the resolution of this sensor.
 | 
			
		||||
   *
 | 
			
		||||
   * Possible values are:
 | 
			
		||||
   *
 | 
			
		||||
   *  - `BH1750_RESOLUTION_4P0_LX`
 | 
			
		||||
   *  - `BH1750_RESOLUTION_1P0_LX`
 | 
			
		||||
   *  - `BH1750_RESOLUTION_0P5_LX` (default)
 | 
			
		||||
   *
 | 
			
		||||
   * @param resolution The new resolution of the sensor.
 | 
			
		||||
   */
 | 
			
		||||
  void set_resolution(BH1750Resolution resolution);
 | 
			
		||||
  void set_measurement_duration(uint8_t measurement_duration) { measurement_duration_ = measurement_duration; }
 | 
			
		||||
 | 
			
		||||
  // ========== INTERNAL METHODS ==========
 | 
			
		||||
  // (In most use cases you won't need these)
 | 
			
		||||
  void setup() override;
 | 
			
		||||
@@ -38,10 +24,9 @@ class BH1750Sensor : public sensor::Sensor, public PollingComponent, public i2c:
 | 
			
		||||
  float get_setup_priority() const override;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void read_data_();
 | 
			
		||||
  void read_lx_(BH1750Mode mode, uint8_t mtreg, const std::function<void(float)> &f);
 | 
			
		||||
 | 
			
		||||
  BH1750Resolution resolution_{BH1750_RESOLUTION_0P5_LX};
 | 
			
		||||
  uint8_t measurement_duration_;
 | 
			
		||||
  uint8_t active_mtreg_{0};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace bh1750
 | 
			
		||||
 
 | 
			
		||||
@@ -2,31 +2,23 @@ import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.components import i2c, sensor
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_RESOLUTION,
 | 
			
		||||
    DEVICE_CLASS_ILLUMINANCE,
 | 
			
		||||
    STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    UNIT_LUX,
 | 
			
		||||
    CONF_MEASUREMENT_DURATION,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
DEPENDENCIES = ["i2c"]
 | 
			
		||||
CODEOWNERS = ["@OttoWinter"]
 | 
			
		||||
 | 
			
		||||
bh1750_ns = cg.esphome_ns.namespace("bh1750")
 | 
			
		||||
BH1750Resolution = bh1750_ns.enum("BH1750Resolution")
 | 
			
		||||
BH1750_RESOLUTIONS = {
 | 
			
		||||
    4.0: BH1750Resolution.BH1750_RESOLUTION_4P0_LX,
 | 
			
		||||
    1.0: BH1750Resolution.BH1750_RESOLUTION_1P0_LX,
 | 
			
		||||
    0.5: BH1750Resolution.BH1750_RESOLUTION_0P5_LX,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
BH1750Sensor = bh1750_ns.class_(
 | 
			
		||||
    "BH1750Sensor", sensor.Sensor, cg.PollingComponent, i2c.I2CDevice
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CONF_MEASUREMENT_TIME = "measurement_time"
 | 
			
		||||
CONFIG_SCHEMA = (
 | 
			
		||||
    sensor.sensor_schema(
 | 
			
		||||
        BH1750Sensor,
 | 
			
		||||
        unit_of_measurement=UNIT_LUX,
 | 
			
		||||
        accuracy_decimals=1,
 | 
			
		||||
        device_class=DEVICE_CLASS_ILLUMINANCE,
 | 
			
		||||
@@ -34,15 +26,11 @@ CONFIG_SCHEMA = (
 | 
			
		||||
    )
 | 
			
		||||
    .extend(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(BH1750Sensor),
 | 
			
		||||
            cv.Optional(CONF_RESOLUTION, default=0.5): cv.enum(
 | 
			
		||||
                BH1750_RESOLUTIONS, float=True
 | 
			
		||||
            cv.Optional("resolution"): cv.invalid(
 | 
			
		||||
                "The 'resolution' option has been removed. The optimal value is now dynamically calculated."
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_MEASUREMENT_DURATION, default=69): cv.int_range(
 | 
			
		||||
                min=31, max=254
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_MEASUREMENT_TIME): cv.invalid(
 | 
			
		||||
                "The 'measurement_time' option has been replaced with 'measurement_duration' in 1.18.0"
 | 
			
		||||
            cv.Optional("measurement_duration"): cv.invalid(
 | 
			
		||||
                "The 'measurement_duration' option has been removed. The optimal value is now dynamically calculated."
 | 
			
		||||
            ),
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
@@ -52,10 +40,6 @@ CONFIG_SCHEMA = (
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    var = await sensor.new_sensor(config)
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    await sensor.register_sensor(var, config)
 | 
			
		||||
    await i2c.register_i2c_device(var, config)
 | 
			
		||||
 | 
			
		||||
    cg.add(var.set_resolution(config[CONF_RESOLUTION]))
 | 
			
		||||
    cg.add(var.set_measurement_duration(config[CONF_MEASUREMENT_DURATION]))
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.cpp_generator import MockObjClass
 | 
			
		||||
from esphome.cpp_helpers import setup_entity
 | 
			
		||||
from esphome import automation, core
 | 
			
		||||
from esphome.automation import Condition, maybe_simple_id
 | 
			
		||||
@@ -7,7 +8,9 @@ from esphome.components import mqtt
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_DELAY,
 | 
			
		||||
    CONF_DEVICE_CLASS,
 | 
			
		||||
    CONF_ENTITY_CATEGORY,
 | 
			
		||||
    CONF_FILTERS,
 | 
			
		||||
    CONF_ICON,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_INVALID_COOLDOWN,
 | 
			
		||||
    CONF_INVERTED,
 | 
			
		||||
@@ -22,7 +25,6 @@ from esphome.const import (
 | 
			
		||||
    CONF_STATE,
 | 
			
		||||
    CONF_TIMING,
 | 
			
		||||
    CONF_TRIGGER_ID,
 | 
			
		||||
    CONF_NAME,
 | 
			
		||||
    CONF_MQTT_ID,
 | 
			
		||||
    DEVICE_CLASS_EMPTY,
 | 
			
		||||
    DEVICE_CLASS_BATTERY,
 | 
			
		||||
@@ -315,7 +317,7 @@ def validate_multi_click_timing(value):
 | 
			
		||||
    return timings
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_")
 | 
			
		||||
validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
BINARY_SENSOR_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMPONENT_SCHEMA).extend(
 | 
			
		||||
@@ -324,7 +326,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_DEVICE_CLASS): device_class,
 | 
			
		||||
        cv.Optional(CONF_DEVICE_CLASS): validate_device_class,
 | 
			
		||||
        cv.Optional(CONF_FILTERS): validate_filters,
 | 
			
		||||
        cv.Optional(CONF_ON_PRESS): automation.validate_automation(
 | 
			
		||||
            {
 | 
			
		||||
@@ -377,6 +379,39 @@ BINARY_SENSOR_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMPONENT_SCHEMA).ex
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
_UNDEF = object()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def binary_sensor_schema(
 | 
			
		||||
    class_: MockObjClass = _UNDEF,
 | 
			
		||||
    *,
 | 
			
		||||
    icon: str = _UNDEF,
 | 
			
		||||
    entity_category: str = _UNDEF,
 | 
			
		||||
    device_class: str = _UNDEF,
 | 
			
		||||
) -> cv.Schema:
 | 
			
		||||
    schema = BINARY_SENSOR_SCHEMA
 | 
			
		||||
    if class_ is not _UNDEF:
 | 
			
		||||
        schema = schema.extend({cv.GenerateID(): cv.declare_id(class_)})
 | 
			
		||||
    if icon is not _UNDEF:
 | 
			
		||||
        schema = schema.extend({cv.Optional(CONF_ICON, default=icon): cv.icon})
 | 
			
		||||
    if entity_category is not _UNDEF:
 | 
			
		||||
        schema = schema.extend(
 | 
			
		||||
            {
 | 
			
		||||
                cv.Optional(
 | 
			
		||||
                    CONF_ENTITY_CATEGORY, default=entity_category
 | 
			
		||||
                ): cv.entity_category
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
    if device_class is not _UNDEF:
 | 
			
		||||
        schema = schema.extend(
 | 
			
		||||
            {
 | 
			
		||||
                cv.Optional(
 | 
			
		||||
                    CONF_DEVICE_CLASS, default=device_class
 | 
			
		||||
                ): validate_device_class
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
    return schema
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def setup_binary_sensor_core_(var, config):
 | 
			
		||||
    await setup_entity(var, config)
 | 
			
		||||
@@ -443,7 +478,7 @@ async def register_binary_sensor(var, config):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def new_binary_sensor(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID], config[CONF_NAME])
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await register_binary_sensor(var, config)
 | 
			
		||||
    return var
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -42,8 +42,7 @@ void BinarySensor::send_state_internal(bool state, bool is_initial) {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
std::string BinarySensor::device_class() { return ""; }
 | 
			
		||||
BinarySensor::BinarySensor(const std::string &name) : EntityBase(name), state(false) {}
 | 
			
		||||
BinarySensor::BinarySensor() : BinarySensor("") {}
 | 
			
		||||
BinarySensor::BinarySensor() : state(false) {}
 | 
			
		||||
void BinarySensor::set_device_class(const std::string &device_class) { this->device_class_ = device_class; }
 | 
			
		||||
std::string BinarySensor::get_device_class() {
 | 
			
		||||
  if (this->device_class_.has_value())
 | 
			
		||||
 
 | 
			
		||||
@@ -26,11 +26,6 @@ namespace binary_sensor {
 | 
			
		||||
class BinarySensor : public EntityBase {
 | 
			
		||||
 public:
 | 
			
		||||
  explicit BinarySensor();
 | 
			
		||||
  /** Construct a binary sensor with the specified name
 | 
			
		||||
   *
 | 
			
		||||
   * @param name Name of this binary sensor.
 | 
			
		||||
   */
 | 
			
		||||
  explicit BinarySensor(const std::string &name);
 | 
			
		||||
 | 
			
		||||
  /** Add a callback to be notified of state changes.
 | 
			
		||||
   *
 | 
			
		||||
 
 | 
			
		||||
@@ -3,14 +3,12 @@ import esphome.config_validation as cv
 | 
			
		||||
 | 
			
		||||
from esphome.components import sensor, binary_sensor
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_CHANNELS,
 | 
			
		||||
    CONF_VALUE,
 | 
			
		||||
    CONF_TYPE,
 | 
			
		||||
    ICON_CHECK_CIRCLE_OUTLINE,
 | 
			
		||||
    CONF_BINARY_SENSOR,
 | 
			
		||||
    CONF_GROUP,
 | 
			
		||||
    STATE_CLASS_NONE,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
DEPENDENCIES = ["binary_sensor"]
 | 
			
		||||
@@ -33,12 +31,11 @@ entry = {
 | 
			
		||||
CONFIG_SCHEMA = cv.typed_schema(
 | 
			
		||||
    {
 | 
			
		||||
        CONF_GROUP: sensor.sensor_schema(
 | 
			
		||||
            BinarySensorMap,
 | 
			
		||||
            icon=ICON_CHECK_CIRCLE_OUTLINE,
 | 
			
		||||
            accuracy_decimals=0,
 | 
			
		||||
            state_class=STATE_CLASS_NONE,
 | 
			
		||||
        ).extend(
 | 
			
		||||
            {
 | 
			
		||||
                cv.GenerateID(): cv.declare_id(BinarySensorMap),
 | 
			
		||||
                cv.Required(CONF_CHANNELS): cv.All(
 | 
			
		||||
                    cv.ensure_list(entry), cv.Length(min=1)
 | 
			
		||||
                ),
 | 
			
		||||
@@ -50,9 +47,8 @@ CONFIG_SCHEMA = cv.typed_schema(
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    var = await sensor.new_sensor(config)
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    await sensor.register_sensor(var, config)
 | 
			
		||||
 | 
			
		||||
    constant = SENSOR_MAP_TYPES[config[CONF_TYPE]]
 | 
			
		||||
    cg.add(var.set_sensor_type(constant))
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								esphome/components/bl0939/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								esphome/components/bl0939/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
CODEOWNERS = ["@ziceva"]
 | 
			
		||||
							
								
								
									
										144
									
								
								esphome/components/bl0939/bl0939.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										144
									
								
								esphome/components/bl0939/bl0939.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,144 @@
 | 
			
		||||
#include "bl0939.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
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)
 | 
			
		||||
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;
 | 
			
		||||
 | 
			
		||||
static const uint8_t BL0939_WRITE_COMMAND = 0xA5;  // 0xA{A4,A3,A2,A1}
 | 
			
		||||
static const uint8_t BL0939_REG_IA_FAST_RMS_CTRL = 0x10;
 | 
			
		||||
static const uint8_t BL0939_REG_IB_FAST_RMS_CTRL = 0x1E;
 | 
			
		||||
static const uint8_t BL0939_REG_MODE = 0x18;
 | 
			
		||||
static const uint8_t BL0939_REG_SOFT_RESET = 0x19;
 | 
			
		||||
static const uint8_t BL0939_REG_USR_WRPROT = 0x1A;
 | 
			
		||||
static const uint8_t BL0939_REG_TPS_CTRL = 0x1B;
 | 
			
		||||
 | 
			
		||||
const uint8_t BL0939_INIT[6][6] = {
 | 
			
		||||
    // Reset to default
 | 
			
		||||
    {BL0939_WRITE_COMMAND, BL0939_REG_SOFT_RESET, 0x5A, 0x5A, 0x5A, 0x33},
 | 
			
		||||
    // Enable User Operation Write
 | 
			
		||||
    {BL0939_WRITE_COMMAND, BL0939_REG_USR_WRPROT, 0x55, 0x00, 0x00, 0xEB},
 | 
			
		||||
    // 0x0100 = CF_UNABLE energy pulse, AC_FREQ_SEL 50Hz, RMS_UPDATE_SEL 800mS
 | 
			
		||||
    {BL0939_WRITE_COMMAND, BL0939_REG_MODE, 0x00, 0x10, 0x00, 0x32},
 | 
			
		||||
    // 0x47FF = Over-current and leakage alarm on, Automatic temperature measurement, Interval 100mS
 | 
			
		||||
    {BL0939_WRITE_COMMAND, BL0939_REG_TPS_CTRL, 0xFF, 0x47, 0x00, 0xF9},
 | 
			
		||||
    // 0x181C = Half cycle, Fast RMS threshold 6172
 | 
			
		||||
    {BL0939_WRITE_COMMAND, BL0939_REG_IA_FAST_RMS_CTRL, 0x1C, 0x18, 0x00, 0x16},
 | 
			
		||||
    // 0x181C = Half cycle, Fast RMS threshold 6172
 | 
			
		||||
    {BL0939_WRITE_COMMAND, BL0939_REG_IB_FAST_RMS_CTRL, 0x1C, 0x18, 0x00, 0x08}};
 | 
			
		||||
 | 
			
		||||
void BL0939::loop() {
 | 
			
		||||
  DataPacket buffer;
 | 
			
		||||
  if (!this->available()) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (read_array((uint8_t *) &buffer, sizeof(buffer))) {
 | 
			
		||||
    if (validate_checksum(&buffer)) {
 | 
			
		||||
      received_package_(&buffer);
 | 
			
		||||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    ESP_LOGW(TAG, "Junk on wire. Throwing away partial message");
 | 
			
		||||
    while (read() >= 0)
 | 
			
		||||
      ;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool BL0939::validate_checksum(const DataPacket *data) {
 | 
			
		||||
  uint8_t checksum = BL0939_READ_COMMAND;
 | 
			
		||||
  // Whole package but checksum
 | 
			
		||||
  for (uint32_t i = 0; i < sizeof(data->raw) - 1; i++) {
 | 
			
		||||
    checksum += data->raw[i];
 | 
			
		||||
  }
 | 
			
		||||
  checksum ^= 0xFF;
 | 
			
		||||
  if (checksum != data->checksum) {
 | 
			
		||||
    ESP_LOGW(TAG, "BL0939 invalid checksum! 0x%02X != 0x%02X", checksum, data->checksum);
 | 
			
		||||
  }
 | 
			
		||||
  return checksum == data->checksum;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BL0939::update() {
 | 
			
		||||
  this->flush();
 | 
			
		||||
  this->write_byte(BL0939_READ_COMMAND);
 | 
			
		||||
  this->write_byte(BL0939_FULL_PACKET);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BL0939::setup() {
 | 
			
		||||
  for (auto *i : BL0939_INIT) {
 | 
			
		||||
    this->write_array(i, 6);
 | 
			
		||||
    delay(1);
 | 
			
		||||
  }
 | 
			
		||||
  this->flush();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BL0939::received_package_(const DataPacket *data) const {
 | 
			
		||||
  // Bad header
 | 
			
		||||
  if (data->frame_header != BL0939_PACKET_HEADER) {
 | 
			
		||||
    ESP_LOGI("bl0939", "Invalid data. Header mismatch: %d", data->frame_header);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  float v_rms = (float) to_uint32_t(data->v_rms) / voltage_reference_;
 | 
			
		||||
  float ia_rms = (float) to_uint32_t(data->ia_rms) / current_reference_;
 | 
			
		||||
  float ib_rms = (float) to_uint32_t(data->ib_rms) / current_reference_;
 | 
			
		||||
  float a_watt = (float) to_int32_t(data->a_watt) / power_reference_;
 | 
			
		||||
  float b_watt = (float) to_int32_t(data->b_watt) / power_reference_;
 | 
			
		||||
  int32_t cfa_cnt = to_int32_t(data->cfa_cnt);
 | 
			
		||||
  int32_t cfb_cnt = to_int32_t(data->cfb_cnt);
 | 
			
		||||
  float a_energy_consumption = (float) cfa_cnt / energy_reference_;
 | 
			
		||||
  float b_energy_consumption = (float) cfb_cnt / energy_reference_;
 | 
			
		||||
  float total_energy_consumption = a_energy_consumption + b_energy_consumption;
 | 
			
		||||
 | 
			
		||||
  if (voltage_sensor_ != nullptr) {
 | 
			
		||||
    voltage_sensor_->publish_state(v_rms);
 | 
			
		||||
  }
 | 
			
		||||
  if (current_sensor_1_ != nullptr) {
 | 
			
		||||
    current_sensor_1_->publish_state(ia_rms);
 | 
			
		||||
  }
 | 
			
		||||
  if (current_sensor_2_ != nullptr) {
 | 
			
		||||
    current_sensor_2_->publish_state(ib_rms);
 | 
			
		||||
  }
 | 
			
		||||
  if (power_sensor_1_ != nullptr) {
 | 
			
		||||
    power_sensor_1_->publish_state(a_watt);
 | 
			
		||||
  }
 | 
			
		||||
  if (power_sensor_2_ != nullptr) {
 | 
			
		||||
    power_sensor_2_->publish_state(b_watt);
 | 
			
		||||
  }
 | 
			
		||||
  if (energy_sensor_1_ != nullptr) {
 | 
			
		||||
    energy_sensor_1_->publish_state(a_energy_consumption);
 | 
			
		||||
  }
 | 
			
		||||
  if (energy_sensor_2_ != nullptr) {
 | 
			
		||||
    energy_sensor_2_->publish_state(b_energy_consumption);
 | 
			
		||||
  }
 | 
			
		||||
  if (energy_sensor_sum_ != nullptr) {
 | 
			
		||||
    energy_sensor_sum_->publish_state(total_energy_consumption);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ESP_LOGV("bl0939", "BL0939: U %fV, I1 %fA, I2 %fA, P1 %fW, P2 %fW, CntA %d, CntB %d, ∫P1 %fkWh, ∫P2 %fkWh", v_rms,
 | 
			
		||||
           ia_rms, ib_rms, a_watt, b_watt, cfa_cnt, cfb_cnt, a_energy_consumption, b_energy_consumption);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BL0939::dump_config() {  // NOLINT(readability-function-cognitive-complexity)
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "BL0939:");
 | 
			
		||||
  LOG_SENSOR("", "Voltage", this->voltage_sensor_);
 | 
			
		||||
  LOG_SENSOR("", "Current 1", this->current_sensor_1_);
 | 
			
		||||
  LOG_SENSOR("", "Current 2", this->current_sensor_2_);
 | 
			
		||||
  LOG_SENSOR("", "Power 1", this->power_sensor_1_);
 | 
			
		||||
  LOG_SENSOR("", "Power 2", this->power_sensor_2_);
 | 
			
		||||
  LOG_SENSOR("", "Energy 1", this->energy_sensor_1_);
 | 
			
		||||
  LOG_SENSOR("", "Energy 2", this->energy_sensor_2_);
 | 
			
		||||
  LOG_SENSOR("", "Energy sum", this->energy_sensor_sum_);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
uint32_t BL0939::to_uint32_t(ube24_t input) { return input.h << 16 | input.m << 8 | input.l; }
 | 
			
		||||
 | 
			
		||||
int32_t BL0939::to_int32_t(sbe24_t input) { return input.h << 16 | input.m << 8 | input.l; }
 | 
			
		||||
 | 
			
		||||
}  // namespace bl0939
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										107
									
								
								esphome/components/bl0939/bl0939.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								esphome/components/bl0939/bl0939.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,107 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/components/uart/uart.h"
 | 
			
		||||
#include "esphome/components/sensor/sensor.h"
 | 
			
		||||
 | 
			
		||||
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)
 | 
			
		||||
// 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)
 | 
			
		||||
static const float BL0939_IREF = 324004 * 1 / 1.218;
 | 
			
		||||
static const float BL0939_UREF = 79931 * 0.51 * 1000 / (1.218 * (5 * 390 + 0.51));
 | 
			
		||||
static const float BL0939_PREF = 4046 * 1 * 0.51 * 1000 / (1.218 * 1.218 * (5 * 390 + 0.51));
 | 
			
		||||
static const float BL0939_EREF = 3.6e6 * 4046 * 1 * 0.51 * 1000 / (1638.4 * 256 * 1.218 * 1.218 * (5 * 390 + 0.51));
 | 
			
		||||
 | 
			
		||||
struct ube24_t {  // NOLINT(readability-identifier-naming,altera-struct-pack-align)
 | 
			
		||||
  uint8_t l;
 | 
			
		||||
  uint8_t m;
 | 
			
		||||
  uint8_t h;
 | 
			
		||||
} __attribute__((packed));
 | 
			
		||||
 | 
			
		||||
struct ube16_t {  // NOLINT(readability-identifier-naming,altera-struct-pack-align)
 | 
			
		||||
  uint8_t l;
 | 
			
		||||
  uint8_t h;
 | 
			
		||||
} __attribute__((packed));
 | 
			
		||||
 | 
			
		||||
struct sbe24_t {  // NOLINT(readability-identifier-naming,altera-struct-pack-align)
 | 
			
		||||
  uint8_t l;
 | 
			
		||||
  uint8_t m;
 | 
			
		||||
  int8_t h;
 | 
			
		||||
} __attribute__((packed));
 | 
			
		||||
 | 
			
		||||
// Caveat: All these values are big endian (low - middle - high)
 | 
			
		||||
 | 
			
		||||
union DataPacket {  // NOLINT(altera-struct-pack-align)
 | 
			
		||||
  uint8_t raw[35];
 | 
			
		||||
  struct {
 | 
			
		||||
    uint8_t frame_header;  // 0x55 according to docs
 | 
			
		||||
    ube24_t ia_fast_rms;
 | 
			
		||||
    ube24_t ia_rms;
 | 
			
		||||
    ube24_t ib_rms;
 | 
			
		||||
    ube24_t v_rms;
 | 
			
		||||
    ube24_t ib_fast_rms;
 | 
			
		||||
    sbe24_t a_watt;
 | 
			
		||||
    sbe24_t b_watt;
 | 
			
		||||
    sbe24_t cfa_cnt;
 | 
			
		||||
    sbe24_t cfb_cnt;
 | 
			
		||||
    ube16_t tps1;
 | 
			
		||||
    uint8_t RESERVED1;  // value of 0x00
 | 
			
		||||
    ube16_t tps2;
 | 
			
		||||
    uint8_t RESERVED2;  // value of 0x00
 | 
			
		||||
    uint8_t checksum;   // checksum
 | 
			
		||||
  };
 | 
			
		||||
} __attribute__((packed));
 | 
			
		||||
 | 
			
		||||
class BL0939 : public PollingComponent, public uart::UARTDevice {
 | 
			
		||||
 public:
 | 
			
		||||
  void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; }
 | 
			
		||||
  void set_current_sensor_1(sensor::Sensor *current_sensor_1) { current_sensor_1_ = current_sensor_1; }
 | 
			
		||||
  void set_current_sensor_2(sensor::Sensor *current_sensor_2) { current_sensor_2_ = current_sensor_2; }
 | 
			
		||||
  void set_power_sensor_1(sensor::Sensor *power_sensor_1) { power_sensor_1_ = power_sensor_1; }
 | 
			
		||||
  void set_power_sensor_2(sensor::Sensor *power_sensor_2) { power_sensor_2_ = power_sensor_2; }
 | 
			
		||||
  void set_energy_sensor_1(sensor::Sensor *energy_sensor_1) { energy_sensor_1_ = energy_sensor_1; }
 | 
			
		||||
  void set_energy_sensor_2(sensor::Sensor *energy_sensor_2) { energy_sensor_2_ = energy_sensor_2; }
 | 
			
		||||
  void set_energy_sensor_sum(sensor::Sensor *energy_sensor_sum) { energy_sensor_sum_ = energy_sensor_sum; }
 | 
			
		||||
 | 
			
		||||
  void loop() override;
 | 
			
		||||
 | 
			
		||||
  void update() override;
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  sensor::Sensor *voltage_sensor_;
 | 
			
		||||
  sensor::Sensor *current_sensor_1_;
 | 
			
		||||
  sensor::Sensor *current_sensor_2_;
 | 
			
		||||
  // NB This may be negative as the circuits is seemingly able to measure
 | 
			
		||||
  // power in both directions
 | 
			
		||||
  sensor::Sensor *power_sensor_1_;
 | 
			
		||||
  sensor::Sensor *power_sensor_2_;
 | 
			
		||||
  sensor::Sensor *energy_sensor_1_;
 | 
			
		||||
  sensor::Sensor *energy_sensor_2_;
 | 
			
		||||
  sensor::Sensor *energy_sensor_sum_;
 | 
			
		||||
 | 
			
		||||
  // Divide by this to turn into Watt
 | 
			
		||||
  float power_reference_ = BL0939_PREF;
 | 
			
		||||
  // Divide by this to turn into Volt
 | 
			
		||||
  float voltage_reference_ = BL0939_UREF;
 | 
			
		||||
  // Divide by this to turn into Ampere
 | 
			
		||||
  float current_reference_ = BL0939_IREF;
 | 
			
		||||
  // Divide by this to turn into kWh
 | 
			
		||||
  float energy_reference_ = BL0939_EREF;
 | 
			
		||||
 | 
			
		||||
  static uint32_t to_uint32_t(ube24_t input);
 | 
			
		||||
 | 
			
		||||
  static int32_t to_int32_t(sbe24_t input);
 | 
			
		||||
 | 
			
		||||
  static bool validate_checksum(const DataPacket *data);
 | 
			
		||||
 | 
			
		||||
  void received_package_(const DataPacket *data) const;
 | 
			
		||||
};
 | 
			
		||||
}  // namespace bl0939
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										123
									
								
								esphome/components/bl0939/sensor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								esphome/components/bl0939/sensor.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,123 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.components import sensor, uart
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_VOLTAGE,
 | 
			
		||||
    DEVICE_CLASS_CURRENT,
 | 
			
		||||
    DEVICE_CLASS_ENERGY,
 | 
			
		||||
    DEVICE_CLASS_POWER,
 | 
			
		||||
    DEVICE_CLASS_VOLTAGE,
 | 
			
		||||
    STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    UNIT_AMPERE,
 | 
			
		||||
    UNIT_KILOWATT_HOURS,
 | 
			
		||||
    UNIT_VOLT,
 | 
			
		||||
    UNIT_WATT,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
DEPENDENCIES = ["uart"]
 | 
			
		||||
 | 
			
		||||
CONF_CURRENT_1 = "current_1"
 | 
			
		||||
CONF_CURRENT_2 = "current_2"
 | 
			
		||||
CONF_ACTIVE_POWER_1 = "active_power_1"
 | 
			
		||||
CONF_ACTIVE_POWER_2 = "active_power_2"
 | 
			
		||||
CONF_ENERGY_1 = "energy_1"
 | 
			
		||||
CONF_ENERGY_2 = "energy_2"
 | 
			
		||||
CONF_ENERGY_TOTAL = "energy_total"
 | 
			
		||||
 | 
			
		||||
bl0939_ns = cg.esphome_ns.namespace("bl0939")
 | 
			
		||||
BL0939 = bl0939_ns.class_("BL0939", cg.PollingComponent, uart.UARTDevice)
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = (
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(BL0939),
 | 
			
		||||
            cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_VOLT,
 | 
			
		||||
                accuracy_decimals=1,
 | 
			
		||||
                device_class=DEVICE_CLASS_VOLTAGE,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_CURRENT_1): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_AMPERE,
 | 
			
		||||
                accuracy_decimals=2,
 | 
			
		||||
                device_class=DEVICE_CLASS_CURRENT,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_CURRENT_2): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_AMPERE,
 | 
			
		||||
                accuracy_decimals=2,
 | 
			
		||||
                device_class=DEVICE_CLASS_CURRENT,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_ACTIVE_POWER_1): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_WATT,
 | 
			
		||||
                accuracy_decimals=0,
 | 
			
		||||
                device_class=DEVICE_CLASS_POWER,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_ACTIVE_POWER_2): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_WATT,
 | 
			
		||||
                accuracy_decimals=0,
 | 
			
		||||
                device_class=DEVICE_CLASS_POWER,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_ENERGY_1): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_KILOWATT_HOURS,
 | 
			
		||||
                accuracy_decimals=3,
 | 
			
		||||
                device_class=DEVICE_CLASS_ENERGY,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_ENERGY_2): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_KILOWATT_HOURS,
 | 
			
		||||
                accuracy_decimals=3,
 | 
			
		||||
                device_class=DEVICE_CLASS_ENERGY,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_ENERGY_TOTAL): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_KILOWATT_HOURS,
 | 
			
		||||
                accuracy_decimals=3,
 | 
			
		||||
                device_class=DEVICE_CLASS_ENERGY,
 | 
			
		||||
            ),
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
    .extend(cv.polling_component_schema("60s"))
 | 
			
		||||
    .extend(uart.UART_DEVICE_SCHEMA)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    await uart.register_uart_device(var, config)
 | 
			
		||||
 | 
			
		||||
    if CONF_VOLTAGE in config:
 | 
			
		||||
        conf = config[CONF_VOLTAGE]
 | 
			
		||||
        sens = await sensor.new_sensor(conf)
 | 
			
		||||
        cg.add(var.set_voltage_sensor(sens))
 | 
			
		||||
    if CONF_CURRENT_1 in config:
 | 
			
		||||
        conf = config[CONF_CURRENT_1]
 | 
			
		||||
        sens = await sensor.new_sensor(conf)
 | 
			
		||||
        cg.add(var.set_current_sensor_1(sens))
 | 
			
		||||
    if CONF_CURRENT_2 in config:
 | 
			
		||||
        conf = config[CONF_CURRENT_2]
 | 
			
		||||
        sens = await sensor.new_sensor(conf)
 | 
			
		||||
        cg.add(var.set_current_sensor_2(sens))
 | 
			
		||||
    if CONF_ACTIVE_POWER_1 in config:
 | 
			
		||||
        conf = config[CONF_ACTIVE_POWER_1]
 | 
			
		||||
        sens = await sensor.new_sensor(conf)
 | 
			
		||||
        cg.add(var.set_power_sensor_1(sens))
 | 
			
		||||
    if CONF_ACTIVE_POWER_2 in config:
 | 
			
		||||
        conf = config[CONF_ACTIVE_POWER_2]
 | 
			
		||||
        sens = await sensor.new_sensor(conf)
 | 
			
		||||
        cg.add(var.set_power_sensor_2(sens))
 | 
			
		||||
    if CONF_ENERGY_1 in config:
 | 
			
		||||
        conf = config[CONF_ENERGY_1]
 | 
			
		||||
        sens = await sensor.new_sensor(conf)
 | 
			
		||||
        cg.add(var.set_energy_sensor_1(sens))
 | 
			
		||||
    if CONF_ENERGY_2 in config:
 | 
			
		||||
        conf = config[CONF_ENERGY_2]
 | 
			
		||||
        sens = await sensor.new_sensor(conf)
 | 
			
		||||
        cg.add(var.set_energy_sensor_2(sens))
 | 
			
		||||
    if CONF_ENERGY_TOTAL in config:
 | 
			
		||||
        conf = config[CONF_ENERGY_TOTAL]
 | 
			
		||||
        sens = await sensor.new_sensor(conf)
 | 
			
		||||
        cg.add(var.set_energy_sensor_sum(sens))
 | 
			
		||||
@@ -12,9 +12,7 @@ from esphome.const import (
 | 
			
		||||
    DEVICE_CLASS_POWER,
 | 
			
		||||
    DEVICE_CLASS_VOLTAGE,
 | 
			
		||||
    DEVICE_CLASS_TEMPERATURE,
 | 
			
		||||
    ICON_EMPTY,
 | 
			
		||||
    STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    STATE_CLASS_NONE,
 | 
			
		||||
    UNIT_AMPERE,
 | 
			
		||||
    UNIT_CELSIUS,
 | 
			
		||||
    UNIT_KILOWATT_HOURS,
 | 
			
		||||
@@ -35,38 +33,39 @@ CONFIG_SCHEMA = (
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(BL0940),
 | 
			
		||||
            cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(
 | 
			
		||||
                UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT
 | 
			
		||||
                unit_of_measurement=UNIT_VOLT,
 | 
			
		||||
                accuracy_decimals=1,
 | 
			
		||||
                device_class=DEVICE_CLASS_VOLTAGE,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_CURRENT): sensor.sensor_schema(
 | 
			
		||||
                UNIT_AMPERE,
 | 
			
		||||
                ICON_EMPTY,
 | 
			
		||||
                2,
 | 
			
		||||
                DEVICE_CLASS_CURRENT,
 | 
			
		||||
                STATE_CLASS_MEASUREMENT,
 | 
			
		||||
                unit_of_measurement=UNIT_AMPERE,
 | 
			
		||||
                accuracy_decimals=2,
 | 
			
		||||
                device_class=DEVICE_CLASS_CURRENT,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_POWER): sensor.sensor_schema(
 | 
			
		||||
                UNIT_WATT, ICON_EMPTY, 0, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT
 | 
			
		||||
                unit_of_measurement=UNIT_WATT,
 | 
			
		||||
                accuracy_decimals=0,
 | 
			
		||||
                device_class=DEVICE_CLASS_POWER,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_ENERGY): sensor.sensor_schema(
 | 
			
		||||
                UNIT_KILOWATT_HOURS,
 | 
			
		||||
                ICON_EMPTY,
 | 
			
		||||
                0,
 | 
			
		||||
                DEVICE_CLASS_ENERGY,
 | 
			
		||||
                STATE_CLASS_NONE,
 | 
			
		||||
                unit_of_measurement=UNIT_KILOWATT_HOURS,
 | 
			
		||||
                accuracy_decimals=0,
 | 
			
		||||
                device_class=DEVICE_CLASS_ENERGY,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_INTERNAL_TEMPERATURE): sensor.sensor_schema(
 | 
			
		||||
                UNIT_CELSIUS,
 | 
			
		||||
                ICON_EMPTY,
 | 
			
		||||
                0,
 | 
			
		||||
                DEVICE_CLASS_TEMPERATURE,
 | 
			
		||||
                STATE_CLASS_NONE,
 | 
			
		||||
                unit_of_measurement=UNIT_CELSIUS,
 | 
			
		||||
                accuracy_decimals=0,
 | 
			
		||||
                device_class=DEVICE_CLASS_TEMPERATURE,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_EXTERNAL_TEMPERATURE): sensor.sensor_schema(
 | 
			
		||||
                UNIT_CELSIUS,
 | 
			
		||||
                ICON_EMPTY,
 | 
			
		||||
                0,
 | 
			
		||||
                DEVICE_CLASS_TEMPERATURE,
 | 
			
		||||
                STATE_CLASS_NONE,
 | 
			
		||||
                unit_of_measurement=UNIT_CELSIUS,
 | 
			
		||||
                accuracy_decimals=0,
 | 
			
		||||
                device_class=DEVICE_CLASS_TEMPERATURE,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ),
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
 
 | 
			
		||||
@@ -118,16 +118,21 @@ void BLEClient::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t es
 | 
			
		||||
        this->set_states_(espbt::ClientState::IDLE);
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
      this->conn_id = param->open.conn_id;
 | 
			
		||||
      auto ret = esp_ble_gattc_send_mtu_req(this->gattc_if, param->open.conn_id);
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    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;
 | 
			
		||||
      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=%d", ret);
 | 
			
		||||
        ESP_LOGW(TAG, "esp_ble_gattc_send_mtu_req failed, status=%x", ret);
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case ESP_GATTC_CFG_MTU_EVT: {
 | 
			
		||||
      if (param->cfg_mtu.status != ESP_GATT_OK) {
 | 
			
		||||
        ESP_LOGW(TAG, "cfg_mtu to %s failed, status %d", this->address_str().c_str(), param->cfg_mtu.status);
 | 
			
		||||
        ESP_LOGW(TAG, "cfg_mtu to %s failed, mtu %d, status %d", this->address_str().c_str(), param->cfg_mtu.mtu,
 | 
			
		||||
                 param->cfg_mtu.status);
 | 
			
		||||
        this->set_states_(espbt::ClientState::IDLE);
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
@@ -139,7 +144,7 @@ void BLEClient::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t es
 | 
			
		||||
      if (memcmp(param->disconnect.remote_bda, this->remote_bda, 6) != 0) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      ESP_LOGV(TAG, "[%s] ESP_GATTC_DISCONNECT_EVT", this->address_str().c_str());
 | 
			
		||||
      ESP_LOGV(TAG, "[%s] ESP_GATTC_DISCONNECT_EVT, reason %d", this->address_str().c_str(), param->disconnect.reason);
 | 
			
		||||
      for (auto &svc : this->services_)
 | 
			
		||||
        delete svc;  // NOLINT(cppcoreguidelines-owning-memory)
 | 
			
		||||
      this->services_.clear();
 | 
			
		||||
@@ -201,6 +206,32 @@ void BLEClient::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t es
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BLEClient::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
 | 
			
		||||
  switch (event) {
 | 
			
		||||
    // This event is sent by the server when it requests security
 | 
			
		||||
    case ESP_GAP_BLE_SEC_REQ_EVT:
 | 
			
		||||
      ESP_LOGV(TAG, "ESP_GAP_BLE_SEC_REQ_EVT %x", event);
 | 
			
		||||
      esp_ble_gap_security_rsp(param->ble_security.ble_req.bd_addr, true);
 | 
			
		||||
      break;
 | 
			
		||||
    // This event is sent once authentication has completed
 | 
			
		||||
    case ESP_GAP_BLE_AUTH_CMPL_EVT:
 | 
			
		||||
      esp_bd_addr_t bd_addr;
 | 
			
		||||
      memcpy(bd_addr, param->ble_security.auth_cmpl.bd_addr, sizeof(esp_bd_addr_t));
 | 
			
		||||
      ESP_LOGI(TAG, "auth complete. remote BD_ADDR: %s", format_hex(bd_addr, 6).c_str());
 | 
			
		||||
      if (!param->ble_security.auth_cmpl.success) {
 | 
			
		||||
        ESP_LOGE(TAG, "auth fail reason = 0x%x", param->ble_security.auth_cmpl.fail_reason);
 | 
			
		||||
      } else {
 | 
			
		||||
        ESP_LOGV(TAG, "auth success. address type = %d auth mode = %d", param->ble_security.auth_cmpl.addr_type,
 | 
			
		||||
                 param->ble_security.auth_cmpl.auth_mode);
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
    // There are other events we'll want to implement at some point to support things like pass key
 | 
			
		||||
    // https://github.com/espressif/esp-idf/blob/cba69dd088344ed9d26739f04736ae7a37541b3a/examples/bluetooth/bluedroid/ble/gatt_security_client/tutorial/Gatt_Security_Client_Example_Walkthrough.md
 | 
			
		||||
    default:
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Parse GATT values into a float for a sensor.
 | 
			
		||||
// Ref: https://www.bluetooth.com/specifications/assigned-numbers/format-types/
 | 
			
		||||
float BLEClient::parse_char_value(uint8_t *value, uint16_t length) {
 | 
			
		||||
 
 | 
			
		||||
@@ -11,6 +11,7 @@
 | 
			
		||||
#include <esp_gap_ble_api.h>
 | 
			
		||||
#include <esp_gattc_api.h>
 | 
			
		||||
#include <esp_bt_defs.h>
 | 
			
		||||
#include <esp_gatt_common_api.h>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace ble_client {
 | 
			
		||||
@@ -86,6 +87,7 @@ class BLEClient : public espbt::ESPBTClient, public Component {
 | 
			
		||||
 | 
			
		||||
  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 gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override;
 | 
			
		||||
  bool parse_device(const espbt::ESPBTDevice &device) override;
 | 
			
		||||
  void on_scan_end() override {}
 | 
			
		||||
  void connect() override;
 | 
			
		||||
 
 | 
			
		||||
@@ -2,9 +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_ID,
 | 
			
		||||
    CONF_LAMBDA,
 | 
			
		||||
    STATE_CLASS_NONE,
 | 
			
		||||
    CONF_TRIGGER_ID,
 | 
			
		||||
    CONF_SERVICE_UUID,
 | 
			
		||||
)
 | 
			
		||||
@@ -31,12 +29,11 @@ BLESensorNotifyTrigger = ble_client_ns.class_(
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.All(
 | 
			
		||||
    sensor.sensor_schema(
 | 
			
		||||
        BLESensor,
 | 
			
		||||
        accuracy_decimals=0,
 | 
			
		||||
        state_class=STATE_CLASS_NONE,
 | 
			
		||||
    )
 | 
			
		||||
    .extend(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(BLESensor),
 | 
			
		||||
            cv.Required(CONF_SERVICE_UUID): esp32_ble_tracker.bt_uuid,
 | 
			
		||||
            cv.Required(CONF_CHARACTERISTIC_UUID): esp32_ble_tracker.bt_uuid,
 | 
			
		||||
            cv.Optional(CONF_DESCRIPTOR_UUID): esp32_ble_tracker.bt_uuid,
 | 
			
		||||
@@ -57,7 +54,7 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    var = await sensor.new_sensor(config)
 | 
			
		||||
    if len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid16_format):
 | 
			
		||||
        cg.add(
 | 
			
		||||
            var.set_service_uuid16(esp32_ble_tracker.as_hex(config[CONF_SERVICE_UUID]))
 | 
			
		||||
@@ -124,7 +121,6 @@ async def to_code(config):
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    await ble_client.register_ble_node(var, config)
 | 
			
		||||
    cg.add(var.set_enable_notify(config[CONF_NOTIFY]))
 | 
			
		||||
    await sensor.register_sensor(var, config)
 | 
			
		||||
    for conf in config.get(CONF_ON_NOTIFY, []):
 | 
			
		||||
        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
 | 
			
		||||
        await ble_client.register_ble_node(trigger, config)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										121
									
								
								esphome/components/ble_client/text_sensor/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								esphome/components/ble_client/text_sensor/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,121 @@
 | 
			
		||||
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_ID,
 | 
			
		||||
    CONF_TRIGGER_ID,
 | 
			
		||||
    CONF_SERVICE_UUID,
 | 
			
		||||
)
 | 
			
		||||
from esphome import automation
 | 
			
		||||
from .. import ble_client_ns
 | 
			
		||||
 | 
			
		||||
DEPENDENCIES = ["ble_client"]
 | 
			
		||||
 | 
			
		||||
CONF_CHARACTERISTIC_UUID = "characteristic_uuid"
 | 
			
		||||
CONF_DESCRIPTOR_UUID = "descriptor_uuid"
 | 
			
		||||
 | 
			
		||||
CONF_NOTIFY = "notify"
 | 
			
		||||
CONF_ON_NOTIFY = "on_notify"
 | 
			
		||||
 | 
			
		||||
adv_data_t = cg.std_vector.template(cg.uint8)
 | 
			
		||||
adv_data_t_const_ref = adv_data_t.operator("ref").operator("const")
 | 
			
		||||
 | 
			
		||||
BLETextSensor = ble_client_ns.class_(
 | 
			
		||||
    "BLETextSensor",
 | 
			
		||||
    text_sensor.TextSensor,
 | 
			
		||||
    cg.PollingComponent,
 | 
			
		||||
    ble_client.BLEClientNode,
 | 
			
		||||
)
 | 
			
		||||
BLETextSensorNotifyTrigger = ble_client_ns.class_(
 | 
			
		||||
    "BLETextSensorNotifyTrigger", automation.Trigger.template(cg.std_string)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.All(
 | 
			
		||||
    text_sensor.TEXT_SENSOR_SCHEMA.extend(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(BLETextSensor),
 | 
			
		||||
            cv.Required(CONF_SERVICE_UUID): esp32_ble_tracker.bt_uuid,
 | 
			
		||||
            cv.Required(CONF_CHARACTERISTIC_UUID): esp32_ble_tracker.bt_uuid,
 | 
			
		||||
            cv.Optional(CONF_DESCRIPTOR_UUID): esp32_ble_tracker.bt_uuid,
 | 
			
		||||
            cv.Optional(CONF_NOTIFY, default=False): cv.boolean,
 | 
			
		||||
            cv.Optional(CONF_ON_NOTIFY): automation.validate_automation(
 | 
			
		||||
                {
 | 
			
		||||
                    cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
 | 
			
		||||
                        BLETextSensorNotifyTrigger
 | 
			
		||||
                    ),
 | 
			
		||||
                }
 | 
			
		||||
            ),
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
    .extend(cv.polling_component_schema("60s"))
 | 
			
		||||
    .extend(ble_client.BLE_CLIENT_SCHEMA)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    if len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid16_format):
 | 
			
		||||
        cg.add(
 | 
			
		||||
            var.set_service_uuid16(esp32_ble_tracker.as_hex(config[CONF_SERVICE_UUID]))
 | 
			
		||||
        )
 | 
			
		||||
    elif len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid32_format):
 | 
			
		||||
        cg.add(
 | 
			
		||||
            var.set_service_uuid32(esp32_ble_tracker.as_hex(config[CONF_SERVICE_UUID]))
 | 
			
		||||
        )
 | 
			
		||||
    elif len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid128_format):
 | 
			
		||||
        uuid128 = esp32_ble_tracker.as_reversed_hex_array(config[CONF_SERVICE_UUID])
 | 
			
		||||
        cg.add(var.set_service_uuid128(uuid128))
 | 
			
		||||
 | 
			
		||||
    if len(config[CONF_CHARACTERISTIC_UUID]) == len(esp32_ble_tracker.bt_uuid16_format):
 | 
			
		||||
        cg.add(
 | 
			
		||||
            var.set_char_uuid16(
 | 
			
		||||
                esp32_ble_tracker.as_hex(config[CONF_CHARACTERISTIC_UUID])
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
    elif len(config[CONF_CHARACTERISTIC_UUID]) == len(
 | 
			
		||||
        esp32_ble_tracker.bt_uuid32_format
 | 
			
		||||
    ):
 | 
			
		||||
        cg.add(
 | 
			
		||||
            var.set_char_uuid32(
 | 
			
		||||
                esp32_ble_tracker.as_hex(config[CONF_CHARACTERISTIC_UUID])
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
    elif len(config[CONF_CHARACTERISTIC_UUID]) == len(
 | 
			
		||||
        esp32_ble_tracker.bt_uuid128_format
 | 
			
		||||
    ):
 | 
			
		||||
        uuid128 = esp32_ble_tracker.as_reversed_hex_array(
 | 
			
		||||
            config[CONF_CHARACTERISTIC_UUID]
 | 
			
		||||
        )
 | 
			
		||||
        cg.add(var.set_char_uuid128(uuid128))
 | 
			
		||||
 | 
			
		||||
    if CONF_DESCRIPTOR_UUID in config:
 | 
			
		||||
        if len(config[CONF_DESCRIPTOR_UUID]) == len(esp32_ble_tracker.bt_uuid16_format):
 | 
			
		||||
            cg.add(
 | 
			
		||||
                var.set_descr_uuid16(
 | 
			
		||||
                    esp32_ble_tracker.as_hex(config[CONF_DESCRIPTOR_UUID])
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
        elif len(config[CONF_DESCRIPTOR_UUID]) == len(
 | 
			
		||||
            esp32_ble_tracker.bt_uuid32_format
 | 
			
		||||
        ):
 | 
			
		||||
            cg.add(
 | 
			
		||||
                var.set_descr_uuid32(
 | 
			
		||||
                    esp32_ble_tracker.as_hex(config[CONF_DESCRIPTOR_UUID])
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
        elif len(config[CONF_DESCRIPTOR_UUID]) == len(
 | 
			
		||||
            esp32_ble_tracker.bt_uuid128_format
 | 
			
		||||
        ):
 | 
			
		||||
            uuid128 = esp32_ble_tracker.as_reversed_hex_array(
 | 
			
		||||
                config[CONF_DESCRIPTOR_UUID]
 | 
			
		||||
            )
 | 
			
		||||
            cg.add(var.set_descr_uuid128(uuid128))
 | 
			
		||||
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    await ble_client.register_ble_node(var, config)
 | 
			
		||||
    cg.add(var.set_enable_notify(config[CONF_NOTIFY]))
 | 
			
		||||
    await text_sensor.register_text_sensor(var, config)
 | 
			
		||||
    for conf in config.get(CONF_ON_NOTIFY, []):
 | 
			
		||||
        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
 | 
			
		||||
        await ble_client.register_ble_node(trigger, config)
 | 
			
		||||
        await automation.build_automation(trigger, [(cg.std_string, "x")], conf)
 | 
			
		||||
							
								
								
									
										38
									
								
								esphome/components/ble_client/text_sensor/automation.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								esphome/components/ble_client/text_sensor/automation.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,38 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/automation.h"
 | 
			
		||||
#include "esphome/components/ble_client/text_sensor/ble_text_sensor.h"
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace ble_client {
 | 
			
		||||
 | 
			
		||||
class BLETextSensorNotifyTrigger : public Trigger<std::string>, public BLETextSensor {
 | 
			
		||||
 public:
 | 
			
		||||
  explicit BLETextSensorNotifyTrigger(BLETextSensor *sensor) { sensor_ = sensor; }
 | 
			
		||||
  void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
 | 
			
		||||
                           esp_ble_gattc_cb_param_t *param) override {
 | 
			
		||||
    switch (event) {
 | 
			
		||||
      case ESP_GATTC_SEARCH_CMPL_EVT: {
 | 
			
		||||
        this->sensor_->node_state = espbt::ClientState::ESTABLISHED;
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
      case ESP_GATTC_NOTIFY_EVT: {
 | 
			
		||||
        if (param->notify.conn_id != this->sensor_->parent()->conn_id || param->notify.handle != this->sensor_->handle)
 | 
			
		||||
          break;
 | 
			
		||||
        this->trigger(this->sensor_->parse_data(param->notify.value, param->notify.value_len));
 | 
			
		||||
      }
 | 
			
		||||
      default:
 | 
			
		||||
        break;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  BLETextSensor *sensor_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace ble_client
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
							
								
								
									
										137
									
								
								esphome/components/ble_client/text_sensor/ble_text_sensor.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										137
									
								
								esphome/components/ble_client/text_sensor/ble_text_sensor.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,137 @@
 | 
			
		||||
#include "ble_text_sensor.h"
 | 
			
		||||
 | 
			
		||||
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
 | 
			
		||||
#include "esphome/core/application.h"
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace ble_client {
 | 
			
		||||
 | 
			
		||||
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() {
 | 
			
		||||
  LOG_TEXT_SENSOR("", "BLE Text Sensor", this);
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  MAC address        : %s", this->parent()->address_str().c_str());
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Service UUID       : %s", this->service_uuid_.to_string().c_str());
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Characteristic UUID: %s", this->char_uuid_.to_string().c_str());
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Descriptor UUID    : %s", this->descr_uuid_.to_string().c_str());
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Notifications      : %s", YESNO(this->notify_));
 | 
			
		||||
  LOG_UPDATE_INTERVAL(this);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BLETextSensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
 | 
			
		||||
                                        esp_ble_gattc_cb_param_t *param) {
 | 
			
		||||
  switch (event) {
 | 
			
		||||
    case ESP_GATTC_OPEN_EVT: {
 | 
			
		||||
      if (param->open.status == ESP_GATT_OK) {
 | 
			
		||||
        ESP_LOGI(TAG, "[%s] Connected successfully!", this->get_name().c_str());
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case ESP_GATTC_DISCONNECT_EVT: {
 | 
			
		||||
      ESP_LOGW(TAG, "[%s] Disconnected!", this->get_name().c_str());
 | 
			
		||||
      this->status_set_warning();
 | 
			
		||||
      this->publish_state(EMPTY);
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case ESP_GATTC_SEARCH_CMPL_EVT: {
 | 
			
		||||
      this->handle = 0;
 | 
			
		||||
      auto *chr = this->parent()->get_characteristic(this->service_uuid_, this->char_uuid_);
 | 
			
		||||
      if (chr == nullptr) {
 | 
			
		||||
        this->status_set_warning();
 | 
			
		||||
        this->publish_state(EMPTY);
 | 
			
		||||
        ESP_LOGW(TAG, "No sensor characteristic found at service %s char %s", this->service_uuid_.to_string().c_str(),
 | 
			
		||||
                 this->char_uuid_.to_string().c_str());
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
      this->handle = chr->handle;
 | 
			
		||||
      if (this->descr_uuid_.get_uuid().len > 0) {
 | 
			
		||||
        auto *descr = chr->get_descriptor(this->descr_uuid_);
 | 
			
		||||
        if (descr == nullptr) {
 | 
			
		||||
          this->status_set_warning();
 | 
			
		||||
          this->publish_state(EMPTY);
 | 
			
		||||
          ESP_LOGW(TAG, "No sensor descriptor found at service %s char %s descr %s",
 | 
			
		||||
                   this->service_uuid_.to_string().c_str(), this->char_uuid_.to_string().c_str(),
 | 
			
		||||
                   this->descr_uuid_.to_string().c_str());
 | 
			
		||||
          break;
 | 
			
		||||
        }
 | 
			
		||||
        this->handle = descr->handle;
 | 
			
		||||
      }
 | 
			
		||||
      if (this->notify_) {
 | 
			
		||||
        auto status =
 | 
			
		||||
            esp_ble_gattc_register_for_notify(this->parent()->gattc_if, this->parent()->remote_bda, chr->handle);
 | 
			
		||||
        if (status) {
 | 
			
		||||
          ESP_LOGW(TAG, "esp_ble_gattc_register_for_notify failed, status=%d", status);
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        this->node_state = espbt::ClientState::ESTABLISHED;
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case ESP_GATTC_READ_CHAR_EVT: {
 | 
			
		||||
      if (param->read.conn_id != this->parent()->conn_id)
 | 
			
		||||
        break;
 | 
			
		||||
      if (param->read.status != ESP_GATT_OK) {
 | 
			
		||||
        ESP_LOGW(TAG, "Error reading char at handle %d, status=%d", param->read.handle, param->read.status);
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
      if (param->read.handle == this->handle) {
 | 
			
		||||
        this->status_clear_warning();
 | 
			
		||||
        this->publish_state(this->parse_data(param->read.value, param->read.value_len));
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case ESP_GATTC_NOTIFY_EVT: {
 | 
			
		||||
      if (param->notify.conn_id != this->parent()->conn_id || param->notify.handle != this->handle)
 | 
			
		||||
        break;
 | 
			
		||||
      ESP_LOGV(TAG, "[%s] ESP_GATTC_NOTIFY_EVT: handle=0x%x, value=0x%x", this->get_name().c_str(),
 | 
			
		||||
               param->notify.handle, param->notify.value[0]);
 | 
			
		||||
      this->publish_state(this->parse_data(param->notify.value, param->notify.value_len));
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
 | 
			
		||||
      this->node_state = espbt::ClientState::ESTABLISHED;
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    default:
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
std::string BLETextSensor::parse_data(uint8_t *value, uint16_t value_len) {
 | 
			
		||||
  std::string text(value, value + value_len);
 | 
			
		||||
  return text;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BLETextSensor::update() {
 | 
			
		||||
  if (this->node_state != espbt::ClientState::ESTABLISHED) {
 | 
			
		||||
    ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->get_name().c_str());
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->handle == 0) {
 | 
			
		||||
    ESP_LOGW(TAG, "[%s] Cannot poll, no service or characteristic found", this->get_name().c_str());
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  auto status =
 | 
			
		||||
      esp_ble_gattc_read_char(this->parent()->gattc_if, this->parent()->conn_id, this->handle, ESP_GATT_AUTH_REQ_NONE);
 | 
			
		||||
  if (status) {
 | 
			
		||||
    this->status_set_warning();
 | 
			
		||||
    this->publish_state(EMPTY);
 | 
			
		||||
    ESP_LOGW(TAG, "[%s] Error sending read request for sensor, status=%d", this->get_name().c_str(), status);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace ble_client
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
#endif
 | 
			
		||||
							
								
								
									
										47
									
								
								esphome/components/ble_client/text_sensor/ble_text_sensor.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								esphome/components/ble_client/text_sensor/ble_text_sensor.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,47 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/components/ble_client/ble_client.h"
 | 
			
		||||
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
 | 
			
		||||
#include "esphome/components/text_sensor/text_sensor.h"
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
#include <esp_gattc_api.h>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace ble_client {
 | 
			
		||||
 | 
			
		||||
namespace espbt = esphome::esp32_ble_tracker;
 | 
			
		||||
 | 
			
		||||
class BLETextSensor : public text_sensor::TextSensor, public PollingComponent, public BLEClientNode {
 | 
			
		||||
 public:
 | 
			
		||||
  void loop() override;
 | 
			
		||||
  void update() override;
 | 
			
		||||
  void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
 | 
			
		||||
                           esp_ble_gattc_cb_param_t *param) override;
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
  float get_setup_priority() const override { return setup_priority::DATA; }
 | 
			
		||||
  void set_service_uuid16(uint16_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); }
 | 
			
		||||
  void set_service_uuid32(uint32_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); }
 | 
			
		||||
  void set_service_uuid128(uint8_t *uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_raw(uuid); }
 | 
			
		||||
  void set_char_uuid16(uint16_t uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); }
 | 
			
		||||
  void set_char_uuid32(uint32_t uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); }
 | 
			
		||||
  void set_char_uuid128(uint8_t *uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_raw(uuid); }
 | 
			
		||||
  void set_descr_uuid16(uint16_t uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); }
 | 
			
		||||
  void set_descr_uuid32(uint32_t uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); }
 | 
			
		||||
  void set_descr_uuid128(uint8_t *uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_raw(uuid); }
 | 
			
		||||
  void set_enable_notify(bool notify) { this->notify_ = notify; }
 | 
			
		||||
  std::string parse_data(uint8_t *value, uint16_t value_len);
 | 
			
		||||
  uint16_t handle;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  uint32_t hash_base() override;
 | 
			
		||||
  bool notify_;
 | 
			
		||||
  espbt::ESPBTUUID service_uuid_;
 | 
			
		||||
  espbt::ESPBTUUID char_uuid_;
 | 
			
		||||
  espbt::ESPBTUUID descr_uuid_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace ble_client
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
#endif
 | 
			
		||||
@@ -7,7 +7,6 @@ from esphome.const import (
 | 
			
		||||
    CONF_IBEACON_MAJOR,
 | 
			
		||||
    CONF_IBEACON_MINOR,
 | 
			
		||||
    CONF_IBEACON_UUID,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
DEPENDENCIES = ["esp32_ble_tracker"]
 | 
			
		||||
@@ -30,9 +29,9 @@ def _validate(config):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.All(
 | 
			
		||||
    binary_sensor.BINARY_SENSOR_SCHEMA.extend(
 | 
			
		||||
    binary_sensor.binary_sensor_schema(BLEPresenceDevice)
 | 
			
		||||
    .extend(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(BLEPresenceDevice),
 | 
			
		||||
            cv.Optional(CONF_MAC_ADDRESS): cv.mac_address,
 | 
			
		||||
            cv.Optional(CONF_SERVICE_UUID): esp32_ble_tracker.bt_uuid,
 | 
			
		||||
            cv.Optional(CONF_IBEACON_MAJOR): cv.uint16_t,
 | 
			
		||||
@@ -48,10 +47,9 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    var = await binary_sensor.new_binary_sensor(config)
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    await esp32_ble_tracker.register_ble_device(var, config)
 | 
			
		||||
    await binary_sensor.register_binary_sensor(var, config)
 | 
			
		||||
 | 
			
		||||
    if CONF_MAC_ADDRESS in config:
 | 
			
		||||
        cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,6 @@ from esphome.components import sensor, esp32_ble_tracker
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_SERVICE_UUID,
 | 
			
		||||
    CONF_MAC_ADDRESS,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    DEVICE_CLASS_SIGNAL_STRENGTH,
 | 
			
		||||
    STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    UNIT_DECIBEL,
 | 
			
		||||
@@ -19,6 +18,7 @@ BLERSSISensor = ble_rssi_ns.class_(
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.All(
 | 
			
		||||
    sensor.sensor_schema(
 | 
			
		||||
        BLERSSISensor,
 | 
			
		||||
        unit_of_measurement=UNIT_DECIBEL,
 | 
			
		||||
        accuracy_decimals=0,
 | 
			
		||||
        device_class=DEVICE_CLASS_SIGNAL_STRENGTH,
 | 
			
		||||
@@ -26,7 +26,6 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
    )
 | 
			
		||||
    .extend(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(BLERSSISensor),
 | 
			
		||||
            cv.Optional(CONF_MAC_ADDRESS): cv.mac_address,
 | 
			
		||||
            cv.Optional(CONF_SERVICE_UUID): esp32_ble_tracker.bt_uuid,
 | 
			
		||||
        }
 | 
			
		||||
@@ -38,10 +37,9 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    var = await sensor.new_sensor(config)
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    await esp32_ble_tracker.register_ble_device(var, config)
 | 
			
		||||
    await sensor.register_sensor(var, config)
 | 
			
		||||
 | 
			
		||||
    if CONF_MAC_ADDRESS in config:
 | 
			
		||||
        cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
 | 
			
		||||
 
 | 
			
		||||
@@ -13,7 +13,7 @@ BLEScanner = ble_scanner_ns.class_(
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.All(
 | 
			
		||||
    text_sensor.text_sensor_schema(klass=BLEScanner)
 | 
			
		||||
    text_sensor.text_sensor_schema(BLEScanner)
 | 
			
		||||
    .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA)
 | 
			
		||||
    .extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
@@ -81,6 +81,11 @@ static const char *iir_filter_to_str(BME280IIRFilter filter) {
 | 
			
		||||
void BME280Component::setup() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Setting up BME280...");
 | 
			
		||||
  uint8_t chip_id = 0;
 | 
			
		||||
 | 
			
		||||
  // Mark as not failed before initializing. Some devices will turn off sensors to save on batteries
 | 
			
		||||
  // and when they come back on, the COMPONENT_STATE_FAILED bit must be unset on the component.
 | 
			
		||||
  this->component_state_ &= ~COMPONENT_STATE_FAILED;
 | 
			
		||||
 | 
			
		||||
  if (!this->read_byte(BME280_REGISTER_CHIPID, &chip_id)) {
 | 
			
		||||
    this->error_code_ = COMMUNICATION_FAILED;
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
 
 | 
			
		||||
@@ -169,6 +169,14 @@ void BME680BSECComponent::loop() {
 | 
			
		||||
  } else {
 | 
			
		||||
    this->status_clear_warning();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Process a single action from the queue. These are primarily sensor state publishes
 | 
			
		||||
  // that in totality take too long to send in a single call.
 | 
			
		||||
  if (this->queue_.size()) {
 | 
			
		||||
    auto action = std::move(this->queue_.front());
 | 
			
		||||
    this->queue_.pop();
 | 
			
		||||
    action();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BME680BSECComponent::run_() {
 | 
			
		||||
@@ -306,37 +314,39 @@ void BME680BSECComponent::read_(int64_t trigger_time_ns, bsec_bme_settings_t bme
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BME680BSECComponent::publish_(const bsec_output_t *outputs, uint8_t num_outputs) {
 | 
			
		||||
  ESP_LOGV(TAG, "Publishing sensor states");
 | 
			
		||||
  ESP_LOGV(TAG, "Queuing sensor state publish actions");
 | 
			
		||||
  for (uint8_t i = 0; i < num_outputs; i++) {
 | 
			
		||||
    float signal = outputs[i].signal;
 | 
			
		||||
    switch (outputs[i].sensor_id) {
 | 
			
		||||
      case BSEC_OUTPUT_IAQ:
 | 
			
		||||
      case BSEC_OUTPUT_STATIC_IAQ:
 | 
			
		||||
        uint8_t accuracy;
 | 
			
		||||
        accuracy = outputs[i].accuracy;
 | 
			
		||||
        this->publish_sensor_state_(this->iaq_sensor_, outputs[i].signal);
 | 
			
		||||
        this->publish_sensor_state_(this->iaq_accuracy_text_sensor_, IAQ_ACCURACY_STATES[accuracy]);
 | 
			
		||||
        this->publish_sensor_state_(this->iaq_accuracy_sensor_, accuracy, true);
 | 
			
		||||
      case BSEC_OUTPUT_STATIC_IAQ: {
 | 
			
		||||
        uint8_t accuracy = outputs[i].accuracy;
 | 
			
		||||
        this->queue_push_([this, signal]() { this->publish_sensor_(this->iaq_sensor_, signal); });
 | 
			
		||||
        this->queue_push_([this, accuracy]() {
 | 
			
		||||
          this->publish_sensor_(this->iaq_accuracy_text_sensor_, IAQ_ACCURACY_STATES[accuracy]);
 | 
			
		||||
        });
 | 
			
		||||
        this->queue_push_([this, accuracy]() { this->publish_sensor_(this->iaq_accuracy_sensor_, accuracy, true); });
 | 
			
		||||
 | 
			
		||||
        // Queue up an opportunity to save state
 | 
			
		||||
        this->defer("save_state", [this, accuracy]() { this->save_state_(accuracy); });
 | 
			
		||||
        break;
 | 
			
		||||
        this->queue_push_([this, accuracy]() { this->save_state_(accuracy); });
 | 
			
		||||
      } break;
 | 
			
		||||
      case BSEC_OUTPUT_CO2_EQUIVALENT:
 | 
			
		||||
        this->publish_sensor_state_(this->co2_equivalent_sensor_, outputs[i].signal);
 | 
			
		||||
        this->queue_push_([this, signal]() { this->publish_sensor_(this->co2_equivalent_sensor_, signal); });
 | 
			
		||||
        break;
 | 
			
		||||
      case BSEC_OUTPUT_BREATH_VOC_EQUIVALENT:
 | 
			
		||||
        this->publish_sensor_state_(this->breath_voc_equivalent_sensor_, outputs[i].signal);
 | 
			
		||||
        this->queue_push_([this, signal]() { this->publish_sensor_(this->breath_voc_equivalent_sensor_, signal); });
 | 
			
		||||
        break;
 | 
			
		||||
      case BSEC_OUTPUT_RAW_PRESSURE:
 | 
			
		||||
        this->publish_sensor_state_(this->pressure_sensor_, outputs[i].signal / 100.0f);
 | 
			
		||||
        this->queue_push_([this, signal]() { this->publish_sensor_(this->pressure_sensor_, signal / 100.0f); });
 | 
			
		||||
        break;
 | 
			
		||||
      case BSEC_OUTPUT_RAW_GAS:
 | 
			
		||||
        this->publish_sensor_state_(this->gas_resistance_sensor_, outputs[i].signal);
 | 
			
		||||
        this->queue_push_([this, signal]() { this->publish_sensor_(this->gas_resistance_sensor_, signal); });
 | 
			
		||||
        break;
 | 
			
		||||
      case BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE:
 | 
			
		||||
        this->publish_sensor_state_(this->temperature_sensor_, outputs[i].signal);
 | 
			
		||||
        this->queue_push_([this, signal]() { this->publish_sensor_(this->temperature_sensor_, signal); });
 | 
			
		||||
        break;
 | 
			
		||||
      case BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY:
 | 
			
		||||
        this->publish_sensor_state_(this->humidity_sensor_, outputs[i].signal);
 | 
			
		||||
        this->queue_push_([this, signal]() { this->publish_sensor_(this->humidity_sensor_, signal); });
 | 
			
		||||
        break;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@@ -352,14 +362,14 @@ int64_t BME680BSECComponent::get_time_ns_() {
 | 
			
		||||
  return (time_ms + ((int64_t) this->millis_overflow_counter_ << 32)) * INT64_C(1000000);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BME680BSECComponent::publish_sensor_state_(sensor::Sensor *sensor, float value, bool change_only) {
 | 
			
		||||
void BME680BSECComponent::publish_sensor_(sensor::Sensor *sensor, float value, bool change_only) {
 | 
			
		||||
  if (!sensor || (change_only && sensor->has_state() && sensor->state == value)) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  sensor->publish_state(value);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BME680BSECComponent::publish_sensor_state_(text_sensor::TextSensor *sensor, const std::string &value) {
 | 
			
		||||
void BME680BSECComponent::publish_sensor_(text_sensor::TextSensor *sensor, const std::string &value) {
 | 
			
		||||
  if (!sensor || (sensor->has_state() && sensor->state == value)) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -70,12 +70,14 @@ class BME680BSECComponent : public Component, public i2c::I2CDevice {
 | 
			
		||||
  void publish_(const bsec_output_t *outputs, uint8_t num_outputs);
 | 
			
		||||
  int64_t get_time_ns_();
 | 
			
		||||
 | 
			
		||||
  void publish_sensor_state_(sensor::Sensor *sensor, float value, bool change_only = false);
 | 
			
		||||
  void publish_sensor_state_(text_sensor::TextSensor *sensor, const std::string &value);
 | 
			
		||||
  void publish_sensor_(sensor::Sensor *sensor, float value, bool change_only = false);
 | 
			
		||||
  void publish_sensor_(text_sensor::TextSensor *sensor, const std::string &value);
 | 
			
		||||
 | 
			
		||||
  void load_state_();
 | 
			
		||||
  void save_state_(uint8_t accuracy);
 | 
			
		||||
 | 
			
		||||
  void queue_push_(std::function<void()> &&f) { this->queue_.push(std::move(f)); }
 | 
			
		||||
 | 
			
		||||
  struct bme680_dev bme680_;
 | 
			
		||||
  bsec_library_return_t bsec_status_{BSEC_OK};
 | 
			
		||||
  int8_t bme680_status_{BME680_OK};
 | 
			
		||||
@@ -84,6 +86,8 @@ class BME680BSECComponent : public Component, public i2c::I2CDevice {
 | 
			
		||||
  uint32_t millis_overflow_counter_{0};
 | 
			
		||||
  int64_t next_call_ns_{0};
 | 
			
		||||
 | 
			
		||||
  std::queue<std::function<void()>> queue_;
 | 
			
		||||
 | 
			
		||||
  ESPPreferenceObject bsec_state_;
 | 
			
		||||
  uint32_t state_save_interval_ms_{21600000};  // 6 hours - 4 times a day
 | 
			
		||||
  uint32_t last_state_save_ms_ = 0;
 | 
			
		||||
 
 | 
			
		||||
@@ -18,11 +18,7 @@ void Button::add_on_press_callback(std::function<void()> &&callback) { this->pre
 | 
			
		||||
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() {
 | 
			
		||||
  if (this->device_class_.has_value())
 | 
			
		||||
    return *this->device_class_;
 | 
			
		||||
  return "";
 | 
			
		||||
}
 | 
			
		||||
std::string Button::get_device_class() { return this->device_class_; }
 | 
			
		||||
 | 
			
		||||
}  // namespace button
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 
 | 
			
		||||
@@ -45,12 +45,12 @@ class Button : public EntityBase {
 | 
			
		||||
 protected:
 | 
			
		||||
  /** You should implement this virtual method if you want to create your own button.
 | 
			
		||||
   */
 | 
			
		||||
  virtual void press_action(){};
 | 
			
		||||
  virtual void press_action() = 0;
 | 
			
		||||
 | 
			
		||||
  uint32_t hash_base() override;
 | 
			
		||||
 | 
			
		||||
  CallbackManager<void()> press_callback_{};
 | 
			
		||||
  optional<std::string> device_class_{};
 | 
			
		||||
  std::string device_class_{};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace button
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,9 @@ CODEOWNERS = ["@mvturnho", "@danielschramm"]
 | 
			
		||||
IS_PLATFORM_COMPONENT = True
 | 
			
		||||
 | 
			
		||||
CONF_CAN_ID = "can_id"
 | 
			
		||||
CONF_CAN_ID_MASK = "can_id_mask"
 | 
			
		||||
CONF_USE_EXTENDED_ID = "use_extended_id"
 | 
			
		||||
CONF_REMOTE_TRANSMISSION_REQUEST = "remote_transmission_request"
 | 
			
		||||
CONF_CANBUS_ID = "canbus_id"
 | 
			
		||||
CONF_BIT_RATE = "bit_rate"
 | 
			
		||||
CONF_ON_FRAME = "on_frame"
 | 
			
		||||
@@ -38,7 +40,7 @@ canbus_ns = cg.esphome_ns.namespace("canbus")
 | 
			
		||||
CanbusComponent = canbus_ns.class_("CanbusComponent", cg.Component)
 | 
			
		||||
CanbusTrigger = canbus_ns.class_(
 | 
			
		||||
    "CanbusTrigger",
 | 
			
		||||
    automation.Trigger.template(cg.std_vector.template(cg.uint8)),
 | 
			
		||||
    automation.Trigger.template(cg.std_vector.template(cg.uint8), cg.uint32),
 | 
			
		||||
    cg.Component,
 | 
			
		||||
)
 | 
			
		||||
CanSpeed = canbus_ns.enum("CAN_SPEED")
 | 
			
		||||
@@ -72,7 +74,11 @@ CANBUS_SCHEMA = cv.Schema(
 | 
			
		||||
            {
 | 
			
		||||
                cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CanbusTrigger),
 | 
			
		||||
                cv.Required(CONF_CAN_ID): cv.int_range(min=0, max=0x1FFFFFFF),
 | 
			
		||||
                cv.Optional(CONF_CAN_ID_MASK, default=0x1FFFFFFF): cv.int_range(
 | 
			
		||||
                    min=0, max=0x1FFFFFFF
 | 
			
		||||
                ),
 | 
			
		||||
                cv.Optional(CONF_USE_EXTENDED_ID, default=False): cv.boolean,
 | 
			
		||||
                cv.Optional(CONF_REMOTE_TRANSMISSION_REQUEST): cv.boolean,
 | 
			
		||||
            },
 | 
			
		||||
            validate_id,
 | 
			
		||||
        ),
 | 
			
		||||
@@ -90,11 +96,26 @@ async def setup_canbus_core_(var, config):
 | 
			
		||||
 | 
			
		||||
    for conf in config.get(CONF_ON_FRAME, []):
 | 
			
		||||
        can_id = conf[CONF_CAN_ID]
 | 
			
		||||
        can_id_mask = conf[CONF_CAN_ID_MASK]
 | 
			
		||||
        ext_id = conf[CONF_USE_EXTENDED_ID]
 | 
			
		||||
        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var, can_id, ext_id)
 | 
			
		||||
        trigger = cg.new_Pvariable(
 | 
			
		||||
            conf[CONF_TRIGGER_ID], var, can_id, can_id_mask, ext_id
 | 
			
		||||
        )
 | 
			
		||||
        if CONF_REMOTE_TRANSMISSION_REQUEST in conf:
 | 
			
		||||
            cg.add(
 | 
			
		||||
                trigger.set_remote_transmission_request(
 | 
			
		||||
                    conf[CONF_REMOTE_TRANSMISSION_REQUEST]
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
        await cg.register_component(trigger, conf)
 | 
			
		||||
        await automation.build_automation(
 | 
			
		||||
            trigger, [(cg.std_vector.template(cg.uint8), "x")], conf
 | 
			
		||||
            trigger,
 | 
			
		||||
            [
 | 
			
		||||
                (cg.std_vector.template(cg.uint8), "x"),
 | 
			
		||||
                (cg.uint32, "can_id"),
 | 
			
		||||
                (cg.bool_, "remote_transmission_request"),
 | 
			
		||||
            ],
 | 
			
		||||
            conf,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -113,6 +134,7 @@ async def register_canbus(var, config):
 | 
			
		||||
            cv.GenerateID(CONF_CANBUS_ID): cv.use_id(CanbusComponent),
 | 
			
		||||
            cv.Optional(CONF_CAN_ID): cv.int_range(min=0, max=0x1FFFFFFF),
 | 
			
		||||
            cv.Optional(CONF_USE_EXTENDED_ID, default=False): cv.boolean,
 | 
			
		||||
            cv.Optional(CONF_REMOTE_TRANSMISSION_REQUEST, default=False): cv.boolean,
 | 
			
		||||
            cv.Required(CONF_DATA): cv.templatable(validate_raw_data),
 | 
			
		||||
        },
 | 
			
		||||
        validate_id,
 | 
			
		||||
@@ -126,12 +148,16 @@ async def canbus_action_to_code(config, action_id, template_arg, args):
 | 
			
		||||
    if CONF_CAN_ID in config:
 | 
			
		||||
        can_id = await cg.templatable(config[CONF_CAN_ID], args, cg.uint32)
 | 
			
		||||
        cg.add(var.set_can_id(can_id))
 | 
			
		||||
 | 
			
		||||
    use_extended_id = await cg.templatable(
 | 
			
		||||
        config[CONF_USE_EXTENDED_ID], args, cg.uint32
 | 
			
		||||
    )
 | 
			
		||||
    cg.add(var.set_use_extended_id(use_extended_id))
 | 
			
		||||
 | 
			
		||||
    remote_transmission_request = await cg.templatable(
 | 
			
		||||
        config[CONF_REMOTE_TRANSMISSION_REQUEST], args, bool
 | 
			
		||||
    )
 | 
			
		||||
    cg.add(var.set_remote_transmission_request(remote_transmission_request))
 | 
			
		||||
 | 
			
		||||
    data = config[CONF_DATA]
 | 
			
		||||
    if isinstance(data, bytes):
 | 
			
		||||
        data = [int(x) for x in data]
 | 
			
		||||
 
 | 
			
		||||
@@ -22,20 +22,22 @@ void Canbus::dump_config() {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Canbus::send_data(uint32_t can_id, bool use_extended_id, const std::vector<uint8_t> &data) {
 | 
			
		||||
void Canbus::send_data(uint32_t can_id, bool use_extended_id, bool remote_transmission_request,
 | 
			
		||||
                       const std::vector<uint8_t> &data) {
 | 
			
		||||
  struct CanFrame can_message;
 | 
			
		||||
 | 
			
		||||
  uint8_t size = static_cast<uint8_t>(data.size());
 | 
			
		||||
  if (use_extended_id) {
 | 
			
		||||
    ESP_LOGD(TAG, "send extended id=0x%08x size=%d", can_id, size);
 | 
			
		||||
    ESP_LOGD(TAG, "send extended id=0x%08x rtr=%s size=%d", can_id, TRUEFALSE(remote_transmission_request), size);
 | 
			
		||||
  } else {
 | 
			
		||||
    ESP_LOGD(TAG, "send extended id=0x%03x size=%d", can_id, size);
 | 
			
		||||
    ESP_LOGD(TAG, "send extended id=0x%03x rtr=%s size=%d", can_id, TRUEFALSE(remote_transmission_request), size);
 | 
			
		||||
  }
 | 
			
		||||
  if (size > CAN_MAX_DATA_LENGTH)
 | 
			
		||||
    size = CAN_MAX_DATA_LENGTH;
 | 
			
		||||
  can_message.can_data_length_code = size;
 | 
			
		||||
  can_message.can_id = can_id;
 | 
			
		||||
  can_message.use_extended_id = use_extended_id;
 | 
			
		||||
  can_message.remote_transmission_request = remote_transmission_request;
 | 
			
		||||
 | 
			
		||||
  for (int i = 0; i < size; i++) {
 | 
			
		||||
    can_message.data[i] = data[i];
 | 
			
		||||
@@ -56,13 +58,15 @@ void Canbus::add_trigger(CanbusTrigger *trigger) {
 | 
			
		||||
 | 
			
		||||
void Canbus::loop() {
 | 
			
		||||
  struct CanFrame can_message;
 | 
			
		||||
  // readmessage
 | 
			
		||||
  if (this->read_message(&can_message) == canbus::ERROR_OK) {
 | 
			
		||||
  // read all messages until queue is empty
 | 
			
		||||
  int message_counter = 0;
 | 
			
		||||
  while (this->read_message(&can_message) == canbus::ERROR_OK) {
 | 
			
		||||
    message_counter++;
 | 
			
		||||
    if (can_message.use_extended_id) {
 | 
			
		||||
      ESP_LOGD(TAG, "received can message extended can_id=0x%x size=%d", can_message.can_id,
 | 
			
		||||
      ESP_LOGD(TAG, "received can message (#%d) extended can_id=0x%x size=%d", message_counter, can_message.can_id,
 | 
			
		||||
               can_message.can_data_length_code);
 | 
			
		||||
    } else {
 | 
			
		||||
      ESP_LOGD(TAG, "received can message std can_id=0x%x size=%d", can_message.can_id,
 | 
			
		||||
      ESP_LOGD(TAG, "received can message (#%d) std can_id=0x%x size=%d", message_counter, can_message.can_id,
 | 
			
		||||
               can_message.can_data_length_code);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -76,8 +80,11 @@ void Canbus::loop() {
 | 
			
		||||
 | 
			
		||||
    // fire all triggers
 | 
			
		||||
    for (auto *trigger : this->triggers_) {
 | 
			
		||||
      if ((trigger->can_id_ == can_message.can_id) && (trigger->use_extended_id_ == can_message.use_extended_id)) {
 | 
			
		||||
        trigger->trigger(data);
 | 
			
		||||
      if ((trigger->can_id_ == (can_message.can_id & trigger->can_id_mask_)) &&
 | 
			
		||||
          (trigger->use_extended_id_ == can_message.use_extended_id) &&
 | 
			
		||||
          (!trigger->remote_transmission_request_.has_value() ||
 | 
			
		||||
           trigger->remote_transmission_request_.value() == can_message.remote_transmission_request)) {
 | 
			
		||||
        trigger->trigger(data, can_message.can_id, can_message.remote_transmission_request);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -62,7 +62,12 @@ class Canbus : public Component {
 | 
			
		||||
  float get_setup_priority() const override { return setup_priority::HARDWARE; }
 | 
			
		||||
  void loop() override;
 | 
			
		||||
 | 
			
		||||
  void send_data(uint32_t can_id, bool use_extended_id, const std::vector<uint8_t> &data);
 | 
			
		||||
  void send_data(uint32_t can_id, bool use_extended_id, bool remote_transmission_request,
 | 
			
		||||
                 const std::vector<uint8_t> &data);
 | 
			
		||||
  void send_data(uint32_t can_id, bool use_extended_id, const std::vector<uint8_t> &data) {
 | 
			
		||||
    // for backwards compatibility only
 | 
			
		||||
    this->send_data(can_id, use_extended_id, false, data);
 | 
			
		||||
  }
 | 
			
		||||
  void set_can_id(uint32_t can_id) { this->can_id_ = can_id; }
 | 
			
		||||
  void set_use_extended_id(bool use_extended_id) { this->use_extended_id_ = use_extended_id; }
 | 
			
		||||
  void set_bitrate(CanSpeed bit_rate) { this->bit_rate_ = bit_rate; }
 | 
			
		||||
@@ -96,38 +101,51 @@ template<typename... Ts> class CanbusSendAction : public Action<Ts...>, public P
 | 
			
		||||
 | 
			
		||||
  void set_use_extended_id(bool use_extended_id) { this->use_extended_id_ = use_extended_id; }
 | 
			
		||||
 | 
			
		||||
  void set_remote_transmission_request(bool remote_transmission_request) {
 | 
			
		||||
    this->remote_transmission_request_ = remote_transmission_request;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void play(Ts... x) override {
 | 
			
		||||
    auto can_id = this->can_id_.has_value() ? *this->can_id_ : this->parent_->can_id_;
 | 
			
		||||
    auto use_extended_id =
 | 
			
		||||
        this->use_extended_id_.has_value() ? *this->use_extended_id_ : this->parent_->use_extended_id_;
 | 
			
		||||
    if (this->static_) {
 | 
			
		||||
      this->parent_->send_data(can_id, use_extended_id, this->data_static_);
 | 
			
		||||
      this->parent_->send_data(can_id, use_extended_id, this->remote_transmission_request_, this->data_static_);
 | 
			
		||||
    } else {
 | 
			
		||||
      auto val = this->data_func_(x...);
 | 
			
		||||
      this->parent_->send_data(can_id, use_extended_id, val);
 | 
			
		||||
      this->parent_->send_data(can_id, use_extended_id, this->remote_transmission_request_, val);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  optional<uint32_t> can_id_{};
 | 
			
		||||
  optional<bool> use_extended_id_{};
 | 
			
		||||
  bool remote_transmission_request_{false};
 | 
			
		||||
  bool static_{false};
 | 
			
		||||
  std::function<std::vector<uint8_t>(Ts...)> data_func_{};
 | 
			
		||||
  std::vector<uint8_t> data_static_{};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class CanbusTrigger : public Trigger<std::vector<uint8_t>>, public Component {
 | 
			
		||||
class CanbusTrigger : public Trigger<std::vector<uint8_t>, uint32_t, bool>, public Component {
 | 
			
		||||
  friend class Canbus;
 | 
			
		||||
 | 
			
		||||
 public:
 | 
			
		||||
  explicit CanbusTrigger(Canbus *parent, const std::uint32_t can_id, const bool use_extended_id)
 | 
			
		||||
      : parent_(parent), can_id_(can_id), use_extended_id_(use_extended_id){};
 | 
			
		||||
  explicit CanbusTrigger(Canbus *parent, const std::uint32_t can_id, const std::uint32_t can_id_mask,
 | 
			
		||||
                         const bool use_extended_id)
 | 
			
		||||
      : parent_(parent), can_id_(can_id), can_id_mask_(can_id_mask), use_extended_id_(use_extended_id){};
 | 
			
		||||
 | 
			
		||||
  void set_remote_transmission_request(bool remote_transmission_request) {
 | 
			
		||||
    this->remote_transmission_request_ = remote_transmission_request;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void setup() override { this->parent_->add_trigger(this); }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  Canbus *parent_;
 | 
			
		||||
  uint32_t can_id_;
 | 
			
		||||
  uint32_t can_id_mask_;
 | 
			
		||||
  bool use_extended_id_;
 | 
			
		||||
  optional<bool> remote_transmission_request_{};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace canbus
 | 
			
		||||
 
 | 
			
		||||
@@ -1,15 +1,14 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.components import binary_sensor
 | 
			
		||||
from esphome.const import CONF_CHANNEL, CONF_ID
 | 
			
		||||
from esphome.const import CONF_CHANNEL
 | 
			
		||||
from . import cap1188_ns, CAP1188Component, CONF_CAP1188_ID
 | 
			
		||||
 | 
			
		||||
DEPENDENCIES = ["cap1188"]
 | 
			
		||||
CAP1188Channel = cap1188_ns.class_("CAP1188Channel", binary_sensor.BinarySensor)
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend(
 | 
			
		||||
CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(CAP1188Channel).extend(
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(): cv.declare_id(CAP1188Channel),
 | 
			
		||||
        cv.GenerateID(CONF_CAP1188_ID): cv.use_id(CAP1188Component),
 | 
			
		||||
        cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=7),
 | 
			
		||||
    }
 | 
			
		||||
@@ -17,8 +16,7 @@ CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend(
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await binary_sensor.register_binary_sensor(var, config)
 | 
			
		||||
    var = await binary_sensor.new_binary_sensor(config)
 | 
			
		||||
    hub = await cg.get_variable(config[CONF_CAP1188_ID])
 | 
			
		||||
    cg.add(var.set_channel(config[CONF_CHANNEL]))
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										107
									
								
								esphome/components/captive_portal/captive_index.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								esphome/components/captive_portal/captive_index.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,107 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
// Generated from https://github.com/esphome/esphome-webserver
 | 
			
		||||
#include "esphome/core/hal.h"
 | 
			
		||||
namespace esphome {
 | 
			
		||||
 | 
			
		||||
namespace captive_portal {
 | 
			
		||||
 | 
			
		||||
const uint8_t INDEX_GZ[] PROGMEM = {
 | 
			
		||||
    0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0xdd, 0x58, 0x09, 0x6f, 0xdc, 0x36, 0x16, 0xfe, 0x2b,
 | 
			
		||||
    0xac, 0x92, 0x74, 0x34, 0x8d, 0xc5, 0xd1, 0x31, 0x97, 0x35, 0xd2, 0x14, 0x89, 0x37, 0x45, 0x0b, 0x24, 0x69, 0x00,
 | 
			
		||||
    0xbb, 0x5d, 0x14, 0x69, 0x00, 0x73, 0x24, 0x6a, 0xc4, 0x58, 0xa2, 0x54, 0x91, 0x9a, 0x23, 0x83, 0xd9, 0xdf, 0xde,
 | 
			
		||||
    0x47, 0x52, 0x73, 0x38, 0x6b, 0x2f, 0x90, 0x62, 0x8b, 0xa2, 0x4d, 0x6c, 0x9a, 0xc7, 0x3b, 0x3f, 0xf2, 0xf1, 0x3d,
 | 
			
		||||
    0x2a, 0xfa, 0x2a, 0xad, 0x12, 0xb9, 0xad, 0x29, 0xca, 0x65, 0x59, 0xcc, 0x23, 0xd5, 0xa2, 0x82, 0xf0, 0x65, 0x4c,
 | 
			
		||||
    0x39, 0x8c, 0x28, 0x49, 0xe7, 0x51, 0x49, 0x25, 0x41, 0x49, 0x4e, 0x1a, 0x41, 0x65, 0xfc, 0xd3, 0xcd, 0x77, 0xce,
 | 
			
		||||
    0x14, 0x0d, 0xe6, 0x51, 0xc1, 0xf8, 0x1d, 0x6a, 0x68, 0x11, 0xb3, 0xa4, 0xe2, 0x28, 0x6f, 0x68, 0x16, 0xa7, 0x44,
 | 
			
		||||
    0x92, 0x90, 0x95, 0x64, 0x49, 0x15, 0x81, 0x66, 0xe3, 0xa4, 0xa4, 0xf1, 0x8a, 0xd1, 0x75, 0x5d, 0x35, 0x12, 0x01,
 | 
			
		||||
    0xa5, 0xa4, 0x5c, 0xc6, 0xd6, 0x9a, 0xa5, 0x32, 0x8f, 0x53, 0xba, 0x62, 0x09, 0x75, 0xf4, 0xe0, 0x82, 0x71, 0x26,
 | 
			
		||||
    0x19, 0x29, 0x1c, 0x91, 0x90, 0x82, 0xc6, 0xde, 0x45, 0x2b, 0x68, 0xa3, 0x07, 0x64, 0x01, 0x63, 0x5e, 0x59, 0x20,
 | 
			
		||||
    0x52, 0x24, 0x0d, 0xab, 0x25, 0x52, 0xf6, 0xc6, 0x65, 0x95, 0xb6, 0x05, 0x9d, 0x67, 0x2d, 0x4f, 0x24, 0x03, 0x0b,
 | 
			
		||||
    0x84, 0xcd, 0xfb, 0xbb, 0x82, 0x4a, 0x44, 0xe3, 0x37, 0x44, 0xe6, 0xb8, 0x24, 0x1b, 0xdb, 0x74, 0x18, 0xb7, 0xfd,
 | 
			
		||||
    0x6f, 0x6c, 0xfe, 0xdc, 0x73, 0xdd, 0xfe, 0x85, 0x6e, 0xdc, 0xfe, 0x00, 0xfe, 0xce, 0x1a, 0x2a, 0xdb, 0x86, 0x23,
 | 
			
		||||
    0x62, 0xdf, 0x46, 0x35, 0x50, 0xa2, 0x34, 0xb6, 0x4a, 0xcf, 0xc7, 0xae, 0x3b, 0x45, 0xde, 0x25, 0xf6, 0x47, 0x8e,
 | 
			
		||||
    0xe7, 0xe1, 0xc0, 0xf1, 0x46, 0xc9, 0xc4, 0x19, 0x21, 0x6f, 0x08, 0x8d, 0xef, 0xe3, 0x11, 0x72, 0x3f, 0x59, 0x28,
 | 
			
		||||
    0x63, 0x45, 0x11, 0x5b, 0xbc, 0xe2, 0xd4, 0x42, 0x42, 0x36, 0xd5, 0x1d, 0x8d, 0xad, 0xa4, 0x6d, 0x1a, 0xf0, 0xee,
 | 
			
		||||
    0xaa, 0x2a, 0xaa, 0x06, 0xac, 0xfd, 0x95, 0xa3, 0x7b, 0xff, 0xbe, 0x58, 0x87, 0x6c, 0x08, 0x17, 0x59, 0xd5, 0x94,
 | 
			
		||||
    0xb1, 0xa5, 0x41, 0xb1, 0x9f, 0xee, 0xe8, 0x1e, 0xa9, 0xa6, 0x7f, 0xb6, 0xe8, 0x54, 0x0d, 0x5b, 0x32, 0x1e, 0x5b,
 | 
			
		||||
    0x9e, 0x8f, 0xbc, 0x29, 0xe8, 0xbd, 0xed, 0xef, 0x8f, 0xa0, 0x10, 0x05, 0x4a, 0xe7, 0x66, 0x65, 0xbf, 0xbf, 0x8d,
 | 
			
		||||
    0xc4, 0x6a, 0x89, 0x36, 0x65, 0xc1, 0x45, 0x6c, 0xe5, 0x52, 0xd6, 0xe1, 0x60, 0xb0, 0x5e, 0xaf, 0xf1, 0x3a, 0xc0,
 | 
			
		||||
    0x55, 0xb3, 0x1c, 0xf8, 0xae, 0xeb, 0x0e, 0x80, 0xc2, 0x42, 0x66, 0x7f, 0x2c, 0x7f, 0x68, 0xa1, 0x9c, 0xb2, 0x65,
 | 
			
		||||
    0x2e, 0x75, 0x7f, 0xfe, 0x74, 0xc7, 0xf7, 0x91, 0xa2, 0x98, 0xdf, 0x7e, 0x38, 0xd3, 0xd2, 0x9c, 0x69, 0xe1, 0xdf,
 | 
			
		||||
    0x12, 0xdb, 0x3a, 0xb8, 0xda, 0x7b, 0xa3, 0x8c, 0x9a, 0x10, 0x1f, 0xf9, 0xc8, 0xd5, 0xff, 0x7d, 0x47, 0xf5, 0xbb,
 | 
			
		||||
    0x91, 0xf3, 0xd9, 0x08, 0x9d, 0x8d, 0xe0, 0xaf, 0x02, 0xd0, 0x2f, 0xc7, 0xce, 0xe5, 0x91, 0xdf, 0x53, 0xeb, 0x2b,
 | 
			
		||||
    0xcf, 0x3d, 0x4d, 0x28, 0xa6, 0xef, 0xc7, 0xe7, 0x63, 0xc7, 0xff, 0x59, 0x11, 0x68, 0xf4, 0x8f, 0x5c, 0x8e, 0x9f,
 | 
			
		||||
    0x7b, 0x3f, 0x8f, 0xc9, 0x08, 0x8d, 0xba, 0x99, 0x91, 0xa3, 0xfa, 0xc7, 0x91, 0xd6, 0x85, 0x46, 0x2b, 0x20, 0x2b,
 | 
			
		||||
    0x9d, 0xb1, 0x33, 0x22, 0x01, 0x0a, 0x3a, 0xab, 0xa0, 0x07, 0xd3, 0x63, 0xe0, 0x3e, 0x9b, 0x73, 0x82, 0x4f, 0xbd,
 | 
			
		||||
    0xc1, 0xdc, 0xea, 0x87, 0x96, 0x75, 0x82, 0xa1, 0x3a, 0x87, 0x01, 0x7f, 0xac, 0xe0, 0xdc, 0x59, 0x56, 0x7f, 0x6f,
 | 
			
		||||
    0x7d, 0x2b, 0xc8, 0x8a, 0x5a, 0x71, 0x1c, 0x43, 0xa8, 0xb5, 0x25, 0x9c, 0x10, 0x5c, 0x54, 0x09, 0x51, 0x2c, 0x58,
 | 
			
		||||
    0x50, 0xd2, 0x24, 0xf9, 0xd7, 0x5f, 0xdb, 0xc7, 0xa5, 0x25, 0x95, 0xaf, 0x0a, 0xaa, 0xba, 0xe2, 0xe5, 0xf6, 0x86,
 | 
			
		||||
    0x2c, 0xdf, 0x42, 0x00, 0xd9, 0x16, 0x11, 0x2c, 0xa5, 0x56, 0xff, 0xbd, 0xfb, 0x01, 0x0b, 0xb9, 0x2d, 0x28, 0x4e,
 | 
			
		||||
    0x99, 0xa8, 0x0b, 0xb2, 0x8d, 0xad, 0x05, 0xc8, 0xba, 0xb3, 0xfa, 0x17, 0x19, 0x95, 0x49, 0x6e, 0x5b, 0x03, 0x08,
 | 
			
		||||
    0xb1, 0x8c, 0x2d, 0xf1, 0x47, 0x51, 0x71, 0xab, 0x8f, 0x65, 0x4e, 0xb9, 0x6d, 0x1f, 0x2c, 0x54, 0xf6, 0x71, 0xbd,
 | 
			
		||||
    0x64, 0x3f, 0xb4, 0x74, 0xb4, 0x41, 0x32, 0xa9, 0x42, 0x0e, 0xab, 0xe0, 0xbd, 0x38, 0xce, 0x2e, 0xaa, 0x74, 0xfb,
 | 
			
		||||
    0x88, 0x79, 0xb9, 0x67, 0x6c, 0x63, 0x9c, 0xd3, 0xe6, 0x86, 0x6e, 0xe0, 0xb8, 0xfc, 0x9b, 0x7d, 0xc7, 0xd0, 0x5b,
 | 
			
		||||
    0x2a, 0xd7, 0x55, 0x73, 0x27, 0x42, 0x64, 0x3d, 0x37, 0xe2, 0x66, 0x26, 0x42, 0x39, 0x26, 0xb5, 0xc0, 0xa2, 0x80,
 | 
			
		||||
    0xf0, 0xb7, 0xbd, 0x3e, 0xc4, 0x6a, 0x7d, 0xdf, 0x14, 0x83, 0xe2, 0x6d, 0x94, 0xb2, 0x15, 0x4a, 0x0a, 0x22, 0xe0,
 | 
			
		||||
    0xb8, 0x72, 0x23, 0xcb, 0x42, 0x87, 0xb8, 0xaa, 0x78, 0x02, 0xfc, 0x77, 0xb1, 0xf5, 0x00, 0x76, 0x2f, 0xb7, 0x3f,
 | 
			
		||||
    0xa4, 0x76, 0x4f, 0x00, 0x6a, 0xbd, 0x3e, 0x5e, 0x91, 0xa2, 0xa5, 0x28, 0x46, 0x32, 0x67, 0xe2, 0x64, 0xe2, 0xec,
 | 
			
		||||
    0x51, 0xb6, 0x5a, 0xdc, 0x01, 0x57, 0x06, 0xcb, 0xc2, 0xee, 0x5b, 0xc7, 0x38, 0x8e, 0x88, 0xb9, 0xe5, 0xac, 0x27,
 | 
			
		||||
    0xd6, 0x67, 0x36, 0x39, 0x05, 0xcd, 0xa4, 0x75, 0x16, 0xf0, 0x4f, 0x77, 0x70, 0x1b, 0xe1, 0x06, 0xf4, 0xf7, 0xf7,
 | 
			
		||||
    0xa7, 0xd9, 0x48, 0xd4, 0x84, 0x7f, 0xce, 0xaa, 0x6c, 0xd4, 0x81, 0x85, 0x55, 0x4f, 0x45, 0x17, 0x10, 0x9d, 0x74,
 | 
			
		||||
    0x0e, 0xc8, 0xb1, 0xff, 0x74, 0x07, 0x71, 0xa6, 0x8e, 0xce, 0xdd, 0x49, 0x68, 0x34, 0x00, 0x84, 0xe6, 0xb7, 0xfb,
 | 
			
		||||
    0x7e, 0xff, 0xe4, 0xce, 0x6f, 0x2d, 0x6d, 0xb6, 0xd7, 0xb4, 0xa0, 0x89, 0xac, 0x1a, 0xdb, 0x7a, 0x02, 0x9a, 0xe0,
 | 
			
		||||
    0x24, 0x68, 0xbf, 0xbf, 0xbf, 0x79, 0xf3, 0x3a, 0xae, 0x6c, 0xda, 0xbf, 0x78, 0x8c, 0x5a, 0xdd, 0xea, 0xef, 0xe1,
 | 
			
		||||
    0x56, 0xff, 0x4f, 0xdc, 0x53, 0xf7, 0x7a, 0xef, 0x03, 0xb0, 0x1a, 0xaf, 0x4f, 0x97, 0xbb, 0xba, 0x00, 0x9e, 0xc3,
 | 
			
		||||
    0x25, 0x72, 0x61, 0x3d, 0x17, 0xb6, 0x33, 0x1e, 0xf5, 0x41, 0x3d, 0xfc, 0x80, 0xe9, 0xfa, 0x7a, 0x86, 0x6b, 0x5a,
 | 
			
		||||
    0x1d, 0xd1, 0xf9, 0x37, 0xbb, 0x45, 0xb5, 0x71, 0x04, 0xfb, 0xc4, 0xf8, 0x32, 0x64, 0x3c, 0xa7, 0x0d, 0x93, 0x7b,
 | 
			
		||||
    0x30, 0x17, 0x6e, 0xfa, 0xba, 0x95, 0xbb, 0x9a, 0xa4, 0xa9, 0x5a, 0x19, 0xd5, 0x9b, 0x59, 0x06, 0x79, 0x41, 0x51,
 | 
			
		||||
    0xd2, 0xd0, 0xa3, 0xe5, 0xde, 0xac, 0xeb, 0x2b, 0x28, 0xbc, 0x1c, 0x3d, 0xdb, 0xab, 0x83, 0xb7, 0x93, 0xb0, 0x65,
 | 
			
		||||
    0x0e, 0x29, 0xd8, 0x92, 0x87, 0x09, 0xd8, 0x4d, 0x1b, 0xc3, 0x94, 0x91, 0x92, 0x15, 0xdb, 0x50, 0xc0, 0x65, 0xe8,
 | 
			
		||||
    0x40, 0xc2, 0x60, 0xd9, 0x7e, 0xd1, 0x4a, 0x59, 0x71, 0xd0, 0xdd, 0xa4, 0xb4, 0x09, 0xdd, 0x99, 0xe9, 0x38, 0x0d,
 | 
			
		||||
    0x49, 0x59, 0x2b, 0x42, 0x1c, 0x34, 0xb4, 0x9c, 0x2d, 0x48, 0x72, 0xb7, 0x6c, 0xaa, 0x96, 0xa7, 0x4e, 0xa2, 0x6e,
 | 
			
		||||
    0xeb, 0xf0, 0x89, 0x97, 0x91, 0x80, 0x26, 0xb3, 0x6e, 0x94, 0x65, 0xd9, 0x0c, 0x90, 0xa0, 0x8e, 0xb9, 0xfc, 0x42,
 | 
			
		||||
    0x1f, 0x0f, 0x15, 0xdb, 0x99, 0x99, 0xd8, 0x57, 0x13, 0xc6, 0x46, 0x48, 0x25, 0xcf, 0x66, 0x07, 0x77, 0xdc, 0x19,
 | 
			
		||||
    0xa4, 0x01, 0x01, 0x42, 0x6a, 0x88, 0x7f, 0x30, 0x73, 0x5f, 0x12, 0xc6, 0xcf, 0xad, 0x57, 0x67, 0x65, 0xd6, 0x85,
 | 
			
		||||
    0x2f, 0xc0, 0xa2, 0xd5, 0xe8, 0x20, 0x9e, 0x41, 0xa2, 0x32, 0xb9, 0x30, 0xf4, 0xc7, 0x6e, 0xbd, 0xd9, 0xe3, 0xee,
 | 
			
		||||
    0x8c, 0xec, 0x0e, 0xd4, 0x59, 0x41, 0x37, 0xb3, 0x8f, 0xad, 0x90, 0x2c, 0xdb, 0x3a, 0x5d, 0x2e, 0x0d, 0xe1, 0xbc,
 | 
			
		||||
    0x40, 0x0e, 0x5d, 0x00, 0x29, 0xa5, 0x7c, 0xa6, 0x75, 0x38, 0x4c, 0xd2, 0x52, 0x74, 0x38, 0x1d, 0xc5, 0xe8, 0x53,
 | 
			
		||||
    0x7a, 0x5f, 0xd6, 0xff, 0xa2, 0x56, 0xc7, 0x71, 0x57, 0x92, 0x06, 0x72, 0x8b, 0xb3, 0xa8, 0x00, 0xd3, 0x32, 0x74,
 | 
			
		||||
    0x26, 0xb0, 0x57, 0xdd, 0x94, 0x12, 0x06, 0x9e, 0x83, 0x99, 0xfa, 0x6e, 0x3a, 0xe0, 0xed, 0xd5, 0x1b, 0x24, 0xaa,
 | 
			
		||||
    0x82, 0xa5, 0x1d, 0x9d, 0x26, 0x41, 0xee, 0x11, 0x1e, 0x0f, 0xb6, 0x1b, 0xa9, 0xb9, 0x03, 0xd4, 0xc3, 0x6c, 0x4a,
 | 
			
		||||
    0x3c, 0xf7, 0x81, 0x1d, 0x49, 0xb3, 0xcc, 0x5f, 0x64, 0x47, 0xa4, 0x54, 0xaa, 0xdd, 0xb3, 0xee, 0x54, 0xf8, 0x43,
 | 
			
		||||
    0x10, 0x70, 0xd8, 0x1b, 0xe8, 0xef, 0x99, 0x8e, 0x8b, 0xdd, 0x99, 0x14, 0x7d, 0x52, 0xc3, 0xb6, 0x29, 0xec, 0x87,
 | 
			
		||||
    0x4e, 0xee, 0xb3, 0xe0, 0xea, 0x94, 0x09, 0x7b, 0x8f, 0x67, 0xc2, 0x1e, 0x52, 0xb5, 0xcb, 0xcb, 0x6a, 0x13, 0xf7,
 | 
			
		||||
    0x74, 0x4e, 0x1a, 0xc2, 0x4f, 0xef, 0x59, 0xf0, 0x0a, 0xf8, 0xff, 0x2f, 0x29, 0xee, 0x0f, 0xa7, 0xb7, 0x2f, 0x48,
 | 
			
		||||
    0x6d, 0x5f, 0x98, 0xd5, 0x8c, 0x77, 0xca, 0x79, 0xe8, 0x41, 0xfa, 0x62, 0x58, 0xb0, 0xa5, 0xf7, 0x67, 0x40, 0xfb,
 | 
			
		||||
    0xdf, 0x38, 0x06, 0x2f, 0xbc, 0x29, 0xbe, 0x44, 0xba, 0x31, 0x10, 0xe1, 0x60, 0x8a, 0x26, 0x57, 0x43, 0x3c, 0xf4,
 | 
			
		||||
    0x90, 0xaa, 0x9a, 0xc6, 0x68, 0x82, 0xa7, 0x40, 0x30, 0xc6, 0xc1, 0x04, 0x26, 0x90, 0xef, 0xe1, 0xd1, 0x6b, 0x3f,
 | 
			
		||||
    0xc0, 0xe3, 0x11, 0x50, 0xf9, 0x2e, 0x0e, 0x7c, 0x64, 0x68, 0xc7, 0xd8, 0x07, 0x71, 0x8a, 0x24, 0x28, 0x01, 0xe8,
 | 
			
		||||
    0x24, 0xc0, 0xee, 0x04, 0xc4, 0x8d, 0xb1, 0x7b, 0x89, 0xa7, 0x63, 0x34, 0xc5, 0x13, 0x80, 0x0e, 0x0f, 0x47, 0x85,
 | 
			
		||||
    0x33, 0xc2, 0x1e, 0x4c, 0x07, 0x63, 0x32, 0xc5, 0xc3, 0x00, 0xe9, 0xc6, 0xc0, 0x31, 0x01, 0x11, 0x0e, 0x76, 0xbd,
 | 
			
		||||
    0xd7, 0x01, 0xf6, 0x27, 0xa0, 0x77, 0x38, 0x7c, 0x01, 0x62, 0x2f, 0x87, 0xc8, 0xb4, 0x06, 0x5e, 0x50, 0x30, 0x7a,
 | 
			
		||||
    0x0c, 0x34, 0xff, 0x9f, 0x0b, 0x1a, 0x40, 0xe2, 0xa1, 0x00, 0x5f, 0x42, 0xec, 0x7a, 0x8a, 0xdf, 0xb4, 0x06, 0x37,
 | 
			
		||||
    0xcf, 0x43, 0xee, 0x1f, 0xc6, 0x2c, 0xf8, 0xe7, 0x62, 0xe6, 0x29, 0x04, 0xa0, 0x0b, 0xba, 0x41, 0x0e, 0xd2, 0x8d,
 | 
			
		||||
    0xd1, 0x0d, 0xcc, 0xd3, 0xab, 0x4b, 0x34, 0x05, 0xae, 0xf1, 0x14, 0x5d, 0xa2, 0x91, 0x42, 0x17, 0xd8, 0x87, 0x86,
 | 
			
		||||
    0xc9, 0x01, 0xa6, 0x2f, 0x84, 0x71, 0xf8, 0x37, 0x86, 0xf1, 0x31, 0x9f, 0xfe, 0xc6, 0x2e, 0xfd, 0x15, 0x57, 0x10,
 | 
			
		||||
    0x94, 0x63, 0xba, 0x0c, 0x8b, 0x06, 0xe6, 0x15, 0xaf, 0xaa, 0x28, 0x78, 0x94, 0x43, 0x35, 0x02, 0xef, 0x7a, 0x0f,
 | 
			
		||||
    0xb1, 0x34, 0xce, 0xbd, 0xf9, 0xbd, 0x2a, 0x1d, 0x28, 0xbd, 0x79, 0xa4, 0xd3, 0xf9, 0xfc, 0x26, 0xa7, 0xe8, 0xd5,
 | 
			
		||||
    0xf5, 0x3b, 0x78, 0x08, 0x16, 0x05, 0xe2, 0xd5, 0x1a, 0xde, 0x9b, 0x5b, 0x24, 0x2b, 0xf5, 0x82, 0xe7, 0x50, 0x2a,
 | 
			
		||||
    0xaa, 0x2e, 0x3c, 0x20, 0x50, 0x57, 0x2c, 0x60, 0x8c, 0xa3, 0x45, 0x33, 0x7f, 0x57, 0x50, 0x22, 0x28, 0x5a, 0xb2,
 | 
			
		||||
    0x15, 0x45, 0x4c, 0x42, 0x1d, 0x50, 0x52, 0x24, 0x99, 0x6a, 0x8e, 0x8c, 0x9a, 0xee, 0x6d, 0x25, 0x69, 0x88, 0xae,
 | 
			
		||||
    0xaa, 0x7a, 0xab, 0x85, 0x24, 0x39, 0xe1, 0x4b, 0x9a, 0x1e, 0x84, 0x29, 0xea, 0x6d, 0xd5, 0x36, 0xe8, 0x97, 0x17,
 | 
			
		||||
    0x6f, 0x5e, 0xab, 0x87, 0x36, 0x45, 0x4e, 0xa7, 0x6c, 0x23, 0xd1, 0x8f, 0x37, 0x2f, 0x50, 0x5b, 0xc3, 0xa6, 0x53,
 | 
			
		||||
    0x63, 0x5b, 0xb5, 0xa2, 0xcd, 0x1a, 0x2a, 0x4b, 0xaa, 0x48, 0x40, 0xb9, 0xa0, 0x52, 0x42, 0xa1, 0x21, 0x30, 0x94,
 | 
			
		||||
    0xce, 0xda, 0x13, 0x53, 0x75, 0x83, 0xbb, 0x20, 0x7e, 0xde, 0x95, 0xd7, 0x51, 0x1e, 0x18, 0xd7, 0xaf, 0x3b, 0x6a,
 | 
			
		||||
    0x70, 0x3d, 0x98, 0x47, 0xea, 0x39, 0x8d, 0x88, 0x7e, 0x84, 0xc4, 0x83, 0x35, 0xcb, 0x98, 0x7a, 0xb8, 0xcd, 0x23,
 | 
			
		||||
    0x5d, 0x8f, 0x2a, 0x09, 0xaa, 0x24, 0x32, 0x5f, 0x34, 0x74, 0xaf, 0xa0, 0x7c, 0x09, 0xaf, 0x64, 0xd8, 0x70, 0xa8,
 | 
			
		||||
    0x50, 0x12, 0x9a, 0x57, 0x05, 0x54, 0x40, 0xf1, 0xf5, 0xf5, 0x0f, 0xff, 0x52, 0x9f, 0x3f, 0xc0, 0xcf, 0x13, 0x27,
 | 
			
		||||
    0x3c, 0x29, 0x0c, 0xa3, 0xea, 0x74, 0x7c, 0xe3, 0xa1, 0xf9, 0x90, 0x51, 0xc3, 0x7b, 0x00, 0xfc, 0x4e, 0xef, 0x49,
 | 
			
		||||
    0x79, 0x77, 0x98, 0xec, 0x24, 0xe9, 0x5f, 0x5d, 0xd9, 0x1a, 0x26, 0xd1, 0x2e, 0x4a, 0x26, 0xe7, 0xd7, 0x60, 0x60,
 | 
			
		||||
    0x34, 0x30, 0x0b, 0xe0, 0x9c, 0x72, 0xc0, 0xd0, 0xe6, 0x1d, 0x0f, 0xec, 0xa8, 0x42, 0xec, 0x27, 0x8d, 0x98, 0xd9,
 | 
			
		||||
    0x60, 0xed, 0x65, 0x49, 0x65, 0x5e, 0xa5, 0xf1, 0xbb, 0x1f, 0xaf, 0x6f, 0x8e, 0x1e, 0x77, 0xb0, 0x52, 0x9e, 0x98,
 | 
			
		||||
    0x0f, 0x2c, 0x6d, 0x21, 0x59, 0x4d, 0x1a, 0xa9, 0xc5, 0x3a, 0x2a, 0xce, 0x0e, 0x1e, 0xe9, 0x75, 0xbd, 0x33, 0xda,
 | 
			
		||||
    0xa9, 0x8e, 0x71, 0x30, 0x47, 0x0f, 0xd9, 0x78, 0xd0, 0xfd, 0x99, 0x95, 0x03, 0x73, 0x14, 0x07, 0xe6, 0x5c, 0x0e,
 | 
			
		||||
    0xf4, 0xe7, 0xa7, 0xdf, 0x01, 0xf1, 0x69, 0xfc, 0xac, 0x8e, 0x12, 0x00, 0x00};
 | 
			
		||||
 | 
			
		||||
}  // namespace captive_portal
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
@@ -4,60 +4,27 @@
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include "esphome/core/application.h"
 | 
			
		||||
#include "esphome/components/wifi/wifi_component.h"
 | 
			
		||||
#include "captive_index.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace captive_portal {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "captive_portal";
 | 
			
		||||
 | 
			
		||||
void CaptivePortal::handle_index(AsyncWebServerRequest *request) {
 | 
			
		||||
  AsyncResponseStream *stream = request->beginResponseStream("text/html");
 | 
			
		||||
  stream->print(F("<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" "
 | 
			
		||||
                  "content=\"width=device-width,initial-scale=1,user-scalable=no\"/><title>"));
 | 
			
		||||
  stream->print(App.get_name().c_str());
 | 
			
		||||
  stream->print(F("</title><link rel=\"stylesheet\" href=\"/stylesheet.css\">"));
 | 
			
		||||
  stream->print(F("<script>function c(l){document.getElementById('ssid').value=l.innerText||l.textContent; "
 | 
			
		||||
                  "document.getElementById('psk').focus();}</script>"));
 | 
			
		||||
  stream->print(F("</head>"));
 | 
			
		||||
  stream->print(F("<body><div class=\"main\"><h1>WiFi Networks</h1>"));
 | 
			
		||||
 | 
			
		||||
  if (request->hasArg("save")) {
 | 
			
		||||
    stream->print(F("<div class=\"info\">The ESP will now try to connect to the network...<br/>Please give it some "
 | 
			
		||||
                    "time to connect.<br/>Note: Copy the changed network to your YAML file - the next OTA update will "
 | 
			
		||||
                    "overwrite these settings.</div>"));
 | 
			
		||||
  }
 | 
			
		||||
void CaptivePortal::handle_config(AsyncWebServerRequest *request) {
 | 
			
		||||
  AsyncResponseStream *stream = request->beginResponseStream("application/json");
 | 
			
		||||
  stream->addHeader("cache-control", "public, max-age=0, must-revalidate");
 | 
			
		||||
  stream->printf(R"({"name":"%s","aps":[{})", App.get_name().c_str());
 | 
			
		||||
 | 
			
		||||
  for (auto &scan : wifi::global_wifi_component->get_scan_result()) {
 | 
			
		||||
    if (scan.get_is_hidden())
 | 
			
		||||
      continue;
 | 
			
		||||
 | 
			
		||||
    stream->print(F("<div class=\"network\" onclick=\"c(this)\"><a href=\"#\" class=\"network-left\">"));
 | 
			
		||||
 | 
			
		||||
    if (scan.get_rssi() >= -50) {
 | 
			
		||||
      stream->print(F("<img src=\"/wifi-strength-4.svg\">"));
 | 
			
		||||
    } else if (scan.get_rssi() >= -65) {
 | 
			
		||||
      stream->print(F("<img src=\"/wifi-strength-3.svg\">"));
 | 
			
		||||
    } else if (scan.get_rssi() >= -85) {
 | 
			
		||||
      stream->print(F("<img src=\"/wifi-strength-2.svg\">"));
 | 
			
		||||
    } else {
 | 
			
		||||
      stream->print(F("<img src=\"/wifi-strength-1.svg\">"));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    stream->print(F("<span class=\"network-ssid\">"));
 | 
			
		||||
    stream->print(scan.get_ssid().c_str());
 | 
			
		||||
    stream->print(F("</span></a>"));
 | 
			
		||||
    if (scan.get_with_auth()) {
 | 
			
		||||
      stream->print(F("<img src=\"/lock.svg\">"));
 | 
			
		||||
    }
 | 
			
		||||
    stream->print(F("</div>"));
 | 
			
		||||
    // Assumes no " in ssid, possible unicode isses?
 | 
			
		||||
    stream->printf(R"(,{"ssid":"%s","rssi":%d,"lock":%d})", scan.get_ssid().c_str(), scan.get_rssi(),
 | 
			
		||||
                   scan.get_with_auth());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  stream->print(F("<h3>WiFi Settings</h3><form method=\"GET\" action=\"/wifisave\"><input id=\"ssid\" name=\"ssid\" "
 | 
			
		||||
                  "length=32 placeholder=\"SSID\"><br/><input id=\"psk\" name=\"psk\" length=64 type=\"password\" "
 | 
			
		||||
                  "placeholder=\"Password\"><br/><br/><button type=\"submit\">Save</button></form><br><hr><br>"));
 | 
			
		||||
  stream->print(F("<h1>OTA Update</h1><form method=\"POST\" action=\"/update\" enctype=\"multipart/form-data\"><input "
 | 
			
		||||
                  "type=\"file\" name=\"update\"><button type=\"submit\">Update</button></form>"));
 | 
			
		||||
  stream->print(F("</div></body></html>"));
 | 
			
		||||
  stream->print(F("]}"));
 | 
			
		||||
  request->send(stream);
 | 
			
		||||
}
 | 
			
		||||
void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) {
 | 
			
		||||
@@ -68,7 +35,7 @@ void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) {
 | 
			
		||||
  ESP_LOGI(TAG, "  Password=" LOG_SECRET("'%s'"), psk.c_str());
 | 
			
		||||
  wifi::global_wifi_component->save_wifi_sta(ssid, psk);
 | 
			
		||||
  wifi::global_wifi_component->start_scanning();
 | 
			
		||||
  request->redirect("/?save=true");
 | 
			
		||||
  request->redirect("/?save");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void CaptivePortal::setup() {}
 | 
			
		||||
@@ -98,44 +65,21 @@ void CaptivePortal::start() {
 | 
			
		||||
  this->active_ = true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const char STYLESHEET_CSS[] PROGMEM =
 | 
			
		||||
    R"(*{box-sizing:inherit}div,input{padding:5px;font-size:1em}input{width:95%}body{text-align:center;font-family:sans-serif}button{border:0;border-radius:.3rem;background-color:#1fa3ec;color:#fff;line-height:2.4rem;font-size:1.2rem;width:100%;padding:0}.main{text-align:left;display:inline-block;min-width:260px}.network{display:flex;justify-content:space-between;align-items:center}.network-left{display:flex;align-items:center}.network-ssid{margin-bottom:-7px;margin-left:10px}.info{border:1px solid;margin:10px 0;padding:15px 10px;color:#4f8a10;background-color:#dff2bf})";
 | 
			
		||||
const char LOCK_SVG[] PROGMEM =
 | 
			
		||||
    R"(<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path d="M12 17a2 2 0 0 0 2-2 2 2 0 0 0-2-2 2 2 0 0 0-2 2 2 2 0 0 0 2 2m6-9a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V10a2 2 0 0 1 2-2h1V6a5 5 0 0 1 5-5 5 5 0 0 1 5 5v2h1m-6-5a3 3 0 0 0-3 3v2h6V6a3 3 0 0 0-3-3z"/></svg>)";
 | 
			
		||||
 | 
			
		||||
void CaptivePortal::handleRequest(AsyncWebServerRequest *req) {
 | 
			
		||||
  if (req->url() == "/") {
 | 
			
		||||
    this->handle_index(req);
 | 
			
		||||
    AsyncWebServerResponse *response = req->beginResponse_P(200, "text/html", INDEX_GZ, sizeof(INDEX_GZ));
 | 
			
		||||
    response->addHeader("Content-Encoding", "gzip");
 | 
			
		||||
    req->send(response);
 | 
			
		||||
    return;
 | 
			
		||||
  } else if (req->url() == "/config.json") {
 | 
			
		||||
    this->handle_config(req);
 | 
			
		||||
    return;
 | 
			
		||||
  } else if (req->url() == "/wifisave") {
 | 
			
		||||
    this->handle_wifisave(req);
 | 
			
		||||
    return;
 | 
			
		||||
  } else if (req->url() == "/stylesheet.css") {
 | 
			
		||||
    req->send_P(200, "text/css", STYLESHEET_CSS);
 | 
			
		||||
    return;
 | 
			
		||||
  } else if (req->url() == "/lock.svg") {
 | 
			
		||||
    req->send_P(200, "image/svg+xml", LOCK_SVG);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  AsyncResponseStream *stream = req->beginResponseStream("image/svg+xml");
 | 
			
		||||
  stream->print(F("<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\"><path d=\"M12 3A18.9 18.9 0 0 "
 | 
			
		||||
                  "0 .38 7C4.41 12.06 7.89 16.37 12 21.5L23.65 7C20.32 4.41 16.22 3 12 "));
 | 
			
		||||
  if (req->url() == "/wifi-strength-4.svg") {
 | 
			
		||||
    stream->print(F("3z"));
 | 
			
		||||
  } else {
 | 
			
		||||
    if (req->url() == "/wifi-strength-1.svg") {
 | 
			
		||||
      stream->print(F("3m0 2c3.07 0 6.09.86 8.71 2.45l-5.1 6.36a8.43 8.43 0 0 0-7.22-.01L3.27 7.4"));
 | 
			
		||||
    } else if (req->url() == "/wifi-strength-2.svg") {
 | 
			
		||||
      stream->print(F("3m0 2c3.07 0 6.09.86 8.71 2.45l-3.21 3.98a11.32 11.32 0 0 0-11 0L3.27 7.4"));
 | 
			
		||||
    } else if (req->url() == "/wifi-strength-3.svg") {
 | 
			
		||||
      stream->print(F("3m0 2c3.07 0 6.09.86 8.71 2.45l-1.94 2.43A13.6 13.6 0 0 0 12 8C9 8 6.68 9 5.21 9.84l-1.94-2."));
 | 
			
		||||
    }
 | 
			
		||||
    stream->print(F("4A16.94 16.94 0 0 1 12 5z"));
 | 
			
		||||
  }
 | 
			
		||||
  stream->print(F("\"/></svg>"));
 | 
			
		||||
  req->send(stream);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
CaptivePortal::CaptivePortal(web_server_base::WebServerBase *base) : base_(base) { global_captive_portal = this; }
 | 
			
		||||
float CaptivePortal::get_setup_priority() const {
 | 
			
		||||
  // Before WiFi
 | 
			
		||||
 
 | 
			
		||||
@@ -39,17 +39,7 @@ class CaptivePortal : public AsyncWebHandler, public Component {
 | 
			
		||||
    if (request->method() == HTTP_GET) {
 | 
			
		||||
      if (request->url() == "/")
 | 
			
		||||
        return true;
 | 
			
		||||
      if (request->url() == "/stylesheet.css")
 | 
			
		||||
        return true;
 | 
			
		||||
      if (request->url() == "/wifi-strength-1.svg")
 | 
			
		||||
        return true;
 | 
			
		||||
      if (request->url() == "/wifi-strength-2.svg")
 | 
			
		||||
        return true;
 | 
			
		||||
      if (request->url() == "/wifi-strength-3.svg")
 | 
			
		||||
        return true;
 | 
			
		||||
      if (request->url() == "/wifi-strength-4.svg")
 | 
			
		||||
        return true;
 | 
			
		||||
      if (request->url() == "/lock.svg")
 | 
			
		||||
      if (request->url() == "/config.json")
 | 
			
		||||
        return true;
 | 
			
		||||
      if (request->url() == "/wifisave")
 | 
			
		||||
        return true;
 | 
			
		||||
@@ -58,7 +48,7 @@ class CaptivePortal : public AsyncWebHandler, public Component {
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void handle_index(AsyncWebServerRequest *request);
 | 
			
		||||
  void handle_config(AsyncWebServerRequest *request);
 | 
			
		||||
 | 
			
		||||
  void handle_wifisave(AsyncWebServerRequest *request);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,55 +0,0 @@
 | 
			
		||||
<!-- HTTP_HEAD -->
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html lang="en">
 | 
			
		||||
<head>
 | 
			
		||||
    <meta charset="UTF-8">
 | 
			
		||||
    <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no"/>
 | 
			
		||||
    <title>{{ App.get_name() }}</title>
 | 
			
		||||
    <link rel="stylesheet" href="./stylesheet.css">
 | 
			
		||||
    <script>
 | 
			
		||||
        function c(l) {
 | 
			
		||||
            document.getElementById('ssid').value = l.innerText || l.textContent;
 | 
			
		||||
            document.getElementById('psk').focus();
 | 
			
		||||
        }
 | 
			
		||||
    </script>
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
<div class="main">
 | 
			
		||||
    <h1>WiFi Networks</h1>
 | 
			
		||||
    <div class="info">
 | 
			
		||||
        The ESP will now try to connect to the network...<br/>
 | 
			
		||||
        Please give it some time to connect.<br/>
 | 
			
		||||
        Note: Copy the changed network to your YAML file - the next OTA update will overwrite these settings.
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="network" onclick="c(this)">
 | 
			
		||||
        <a href="#" class="network-left">
 | 
			
		||||
            <img src="./wifi-strength-4.svg">
 | 
			
		||||
            <span class="network-ssid">AP1</span>
 | 
			
		||||
        </a>
 | 
			
		||||
        <img src="./lock.svg">
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="network" onclick="c(this)">
 | 
			
		||||
        <a href="#" class="network-left">
 | 
			
		||||
            <img src="./wifi-strength-2.svg">
 | 
			
		||||
            <span class="network-ssid">AP2</span>
 | 
			
		||||
        </a>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <h3>WiFi Settings</h3>
 | 
			
		||||
    <form method="GET" action="/wifisave">
 | 
			
		||||
        <input id="ssid" name="ssid" length=32 placeholder="SSID"><br/>
 | 
			
		||||
        <input id="psk" name="psk" length=64 type="password" placeholder="Password"><br/>
 | 
			
		||||
        <br/>
 | 
			
		||||
        <button type="submit">Save</button>
 | 
			
		||||
    </form>
 | 
			
		||||
    <br><hr>
 | 
			
		||||
    <br>
 | 
			
		||||
 | 
			
		||||
    <h1>OTA Update</h1>
 | 
			
		||||
    <form method="POST" action="/update" enctype="multipart/form-data">
 | 
			
		||||
        <input type="file" name="update">
 | 
			
		||||
        <button type="submit">Update</button>
 | 
			
		||||
    </form>
 | 
			
		||||
</div>
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
@@ -1 +0,0 @@
 | 
			
		||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path d="M12 17a2 2 0 0 0 2-2 2 2 0 0 0-2-2 2 2 0 0 0-2 2 2 2 0 0 0 2 2m6-9a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V10a2 2 0 0 1 2-2h1V6a5 5 0 0 1 5-5 5 5 0 0 1 5 5v2h1m-6-5a3 3 0 0 0-3 3v2h6V6a3 3 0 0 0-3-3z"/></svg>
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 307 B  | 
@@ -1,58 +0,0 @@
 | 
			
		||||
* {
 | 
			
		||||
    box-sizing: inherit;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
div, input {
 | 
			
		||||
    padding: 5px;
 | 
			
		||||
    font-size: 1em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
input {
 | 
			
		||||
    width: 95%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
body {
 | 
			
		||||
    text-align: center;
 | 
			
		||||
    font-family: sans-serif;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
button {
 | 
			
		||||
    border: 0;
 | 
			
		||||
    border-radius: 0.3rem;
 | 
			
		||||
    background-color: #1fa3ec;
 | 
			
		||||
    color: #fff;
 | 
			
		||||
    line-height: 2.4rem;
 | 
			
		||||
    font-size: 1.2rem;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    padding: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.main {
 | 
			
		||||
    text-align: left;
 | 
			
		||||
    display: inline-block;
 | 
			
		||||
    min-width: 260px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.network {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    justify-content: space-between;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.network-left {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.network-ssid {
 | 
			
		||||
    margin-bottom: -7px;
 | 
			
		||||
    margin-left: 10px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.info {
 | 
			
		||||
    border: 1px solid;
 | 
			
		||||
    margin: 10px 0px;
 | 
			
		||||
    padding: 15px 10px;
 | 
			
		||||
    color: #4f8a10;
 | 
			
		||||
    background-color: #dff2bf;
 | 
			
		||||
}
 | 
			
		||||
@@ -1 +0,0 @@
 | 
			
		||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"><path d="M12 3A18.9 18.9 0 0 0 .38 7C4.41 12.06 7.89 16.37 12 21.5L23.65 7C20.32 4.41 16.22 3 12 3m0 2c3.07 0 6.09.86 8.71 2.45l-5.1 6.36a8.43 8.43 0 0 0-7.22-.01L3.27 7.44A16.94 16.94 0 0 1 12 5z"/></svg>
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 268 B  | 
@@ -1 +0,0 @@
 | 
			
		||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"><path d="M12 3A18.9 18.9 0 0 0 .38 7C4.41 12.06 7.89 16.37 12 21.5L23.65 7C20.32 4.41 16.22 3 12 3m0 2c3.07 0 6.09.86 8.71 2.45l-3.21 3.98a11.32 11.32 0 0 0-11 0L3.27 7.44A16.94 16.94 0 0 1 12 5z"/></svg>
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 267 B  | 
@@ -1 +0,0 @@
 | 
			
		||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"><path d="M12 3A18.9 18.9 0 0 0 .38 7C4.41 12.06 7.89 16.37 12 21.5L23.65 7C20.32 4.41 16.22 3 12 3m0 2c3.07 0 6.09.86 8.71 2.45l-1.94 2.43A13.6 13.6 0 0 0 12 8C9 8 6.68 9 5.21 9.84l-1.94-2.4A16.94 16.94 0 0 1 12 5z"/></svg>
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 286 B  | 
@@ -1 +0,0 @@
 | 
			
		||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"><path d="M12 3A18.9 18.9 0 0 0 .38 7C4.41 12.06 7.89 16.37 12 21.5L23.65 7C20.32 4.41 16.22 3 12 3z"/></svg>
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 171 B  | 
@@ -25,6 +25,7 @@ CONF_CD74HC4067_ID = "cd74hc4067_id"
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = (
 | 
			
		||||
    sensor.sensor_schema(
 | 
			
		||||
        CD74HC4067Sensor,
 | 
			
		||||
        unit_of_measurement=UNIT_VOLT,
 | 
			
		||||
        accuracy_decimals=3,
 | 
			
		||||
        device_class=DEVICE_CLASS_VOLTAGE,
 | 
			
		||||
@@ -33,7 +34,6 @@ CONFIG_SCHEMA = (
 | 
			
		||||
    )
 | 
			
		||||
    .extend(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(CD74HC4067Sensor),
 | 
			
		||||
            cv.GenerateID(CONF_CD74HC4067_ID): cv.use_id(CD74HC4067Component),
 | 
			
		||||
            cv.Required(CONF_NUMBER): cv.int_range(0, 15),
 | 
			
		||||
            cv.Required(CONF_SENSOR): cv.use_id(voltage_sampler.VoltageSampler),
 | 
			
		||||
@@ -47,8 +47,8 @@ async def to_code(config):
 | 
			
		||||
    parent = await cg.get_variable(config[CONF_CD74HC4067_ID])
 | 
			
		||||
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID], parent)
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    await sensor.register_sensor(var, config)
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    cg.add(var.set_pin(config[CONF_NUMBER]))
 | 
			
		||||
 | 
			
		||||
    sens = await cg.get_variable(config[CONF_SENSOR])
 | 
			
		||||
 
 | 
			
		||||
@@ -287,9 +287,11 @@ CLIMATE_CONTROL_ACTION_SCHEMA = cv.Schema(
 | 
			
		||||
        cv.Exclusive(CONF_FAN_MODE, "fan_mode"): cv.templatable(
 | 
			
		||||
            validate_climate_fan_mode
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Exclusive(CONF_CUSTOM_FAN_MODE, "fan_mode"): cv.string_strict,
 | 
			
		||||
        cv.Exclusive(CONF_CUSTOM_FAN_MODE, "fan_mode"): cv.templatable(
 | 
			
		||||
            cv.string_strict
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Exclusive(CONF_PRESET, "preset"): cv.templatable(validate_climate_preset),
 | 
			
		||||
        cv.Exclusive(CONF_CUSTOM_PRESET, "preset"): cv.string_strict,
 | 
			
		||||
        cv.Exclusive(CONF_CUSTOM_PRESET, "preset"): cv.templatable(cv.string_strict),
 | 
			
		||||
        cv.Optional(CONF_SWING_MODE): cv.templatable(validate_climate_swing_mode),
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
@@ -324,13 +326,17 @@ async def climate_control_to_code(config, action_id, template_arg, args):
 | 
			
		||||
        template_ = await cg.templatable(config[CONF_FAN_MODE], args, ClimateFanMode)
 | 
			
		||||
        cg.add(var.set_fan_mode(template_))
 | 
			
		||||
    if CONF_CUSTOM_FAN_MODE in config:
 | 
			
		||||
        template_ = await cg.templatable(config[CONF_CUSTOM_FAN_MODE], args, str)
 | 
			
		||||
        template_ = await cg.templatable(
 | 
			
		||||
            config[CONF_CUSTOM_FAN_MODE], args, cg.std_string
 | 
			
		||||
        )
 | 
			
		||||
        cg.add(var.set_custom_fan_mode(template_))
 | 
			
		||||
    if CONF_PRESET in config:
 | 
			
		||||
        template_ = await cg.templatable(config[CONF_PRESET], args, ClimatePreset)
 | 
			
		||||
        cg.add(var.set_preset(template_))
 | 
			
		||||
    if CONF_CUSTOM_PRESET in config:
 | 
			
		||||
        template_ = await cg.templatable(config[CONF_CUSTOM_PRESET], args, str)
 | 
			
		||||
        template_ = await cg.templatable(
 | 
			
		||||
            config[CONF_CUSTOM_PRESET], args, cg.std_string
 | 
			
		||||
        )
 | 
			
		||||
        cg.add(var.set_custom_preset(template_))
 | 
			
		||||
    if CONF_SWING_MODE in config:
 | 
			
		||||
        template_ = await cg.templatable(
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
#include "climate.h"
 | 
			
		||||
#include "esphome/core/macros.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace climate {
 | 
			
		||||
@@ -326,14 +327,17 @@ optional<ClimateDeviceRestoreState> Climate::restore_state_() {
 | 
			
		||||
  return recovered;
 | 
			
		||||
}
 | 
			
		||||
void Climate::save_state_() {
 | 
			
		||||
#if defined(USE_ESP_IDF) && !defined(CLANG_TIDY)
 | 
			
		||||
#if (defined(USE_ESP_IDF) || (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(3, 0, 0))) && \
 | 
			
		||||
    !defined(CLANG_TIDY)
 | 
			
		||||
#pragma GCC diagnostic ignored "-Wclass-memaccess"
 | 
			
		||||
#define TEMP_IGNORE_MEMACCESS
 | 
			
		||||
#endif
 | 
			
		||||
  ClimateDeviceRestoreState state{};
 | 
			
		||||
  // initialize as zero to prevent random data on stack triggering erase
 | 
			
		||||
  memset(&state, 0, sizeof(ClimateDeviceRestoreState));
 | 
			
		||||
#if USE_ESP_IDF && !defined(CLANG_TIDY)
 | 
			
		||||
#ifdef TEMP_IGNORE_MEMACCESS
 | 
			
		||||
#pragma GCC diagnostic pop
 | 
			
		||||
#undef TEMP_IGNORE_MEMACCESS
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  state.mode = this->mode;
 | 
			
		||||
 
 | 
			
		||||
@@ -76,7 +76,7 @@ enum ClimateSwingMode : uint8_t {
 | 
			
		||||
  CLIMATE_SWING_HORIZONTAL = 3,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/// Enum for all modes a climate swing can be in
 | 
			
		||||
/// Enum for all preset modes
 | 
			
		||||
enum ClimatePreset : uint8_t {
 | 
			
		||||
  /// No preset is active
 | 
			
		||||
  CLIMATE_PRESET_NONE = 0,
 | 
			
		||||
@@ -108,7 +108,7 @@ const LogString *climate_fan_mode_to_string(ClimateFanMode mode);
 | 
			
		||||
/// Convert the given ClimateSwingMode to a human-readable string.
 | 
			
		||||
const LogString *climate_swing_mode_to_string(ClimateSwingMode mode);
 | 
			
		||||
 | 
			
		||||
/// Convert the given ClimateSwingMode to a human-readable string.
 | 
			
		||||
/// Convert the given PresetMode to a human-readable string.
 | 
			
		||||
const LogString *climate_preset_to_string(ClimatePreset preset);
 | 
			
		||||
 | 
			
		||||
}  // namespace climate
 | 
			
		||||
 
 | 
			
		||||
@@ -141,7 +141,7 @@ class ClimateTraits {
 | 
			
		||||
  }
 | 
			
		||||
  bool supports_swing_mode(ClimateSwingMode swing_mode) const { return supported_swing_modes_.count(swing_mode); }
 | 
			
		||||
  bool get_supports_swing_modes() const { return !supported_swing_modes_.empty(); }
 | 
			
		||||
  std::set<ClimateSwingMode> get_supported_swing_modes() { return supported_swing_modes_; }
 | 
			
		||||
  std::set<ClimateSwingMode> get_supported_swing_modes() const { return supported_swing_modes_; }
 | 
			
		||||
 | 
			
		||||
  float get_visual_min_temperature() const { return visual_min_temperature_; }
 | 
			
		||||
  void set_visual_min_temperature(float visual_min_temperature) { visual_min_temperature_ = visual_min_temperature; }
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										5
									
								
								esphome/components/copy/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								esphome/components/copy/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
 | 
			
		||||
CODEOWNERS = ["@OttoWinter"]
 | 
			
		||||
 | 
			
		||||
copy_ns = cg.esphome_ns.namespace("copy")
 | 
			
		||||
							
								
								
									
										41
									
								
								esphome/components/copy/binary_sensor/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								esphome/components/copy/binary_sensor/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,41 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.components import binary_sensor
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_DEVICE_CLASS,
 | 
			
		||||
    CONF_ENTITY_CATEGORY,
 | 
			
		||||
    CONF_ICON,
 | 
			
		||||
    CONF_SOURCE_ID,
 | 
			
		||||
)
 | 
			
		||||
from esphome.core.entity_helpers import inherit_property_from
 | 
			
		||||
 | 
			
		||||
from .. import copy_ns
 | 
			
		||||
 | 
			
		||||
CopyBinarySensor = copy_ns.class_(
 | 
			
		||||
    "CopyBinarySensor", binary_sensor.BinarySensor, cg.Component
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = (
 | 
			
		||||
    binary_sensor.binary_sensor_schema(CopyBinarySensor)
 | 
			
		||||
    .extend(
 | 
			
		||||
        {
 | 
			
		||||
            cv.Required(CONF_SOURCE_ID): cv.use_id(binary_sensor.BinarySensor),
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
    .extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
FINAL_VALIDATE_SCHEMA = cv.All(
 | 
			
		||||
    inherit_property_from(CONF_ICON, CONF_SOURCE_ID),
 | 
			
		||||
    inherit_property_from(CONF_ENTITY_CATEGORY, CONF_SOURCE_ID),
 | 
			
		||||
    inherit_property_from(CONF_DEVICE_CLASS, CONF_SOURCE_ID),
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = await binary_sensor.new_binary_sensor(config)
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
 | 
			
		||||
    source = await cg.get_variable(config[CONF_SOURCE_ID])
 | 
			
		||||
    cg.add(var.set_source(source))
 | 
			
		||||
							
								
								
									
										18
									
								
								esphome/components/copy/binary_sensor/copy_binary_sensor.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								esphome/components/copy/binary_sensor/copy_binary_sensor.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
			
		||||
#include "copy_binary_sensor.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace copy {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "copy.binary_sensor";
 | 
			
		||||
 | 
			
		||||
void CopyBinarySensor::setup() {
 | 
			
		||||
  source_->add_on_state_callback([this](bool value) { this->publish_state(value); });
 | 
			
		||||
  if (source_->has_state())
 | 
			
		||||
    this->publish_state(source_->state);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void CopyBinarySensor::dump_config() { LOG_BINARY_SENSOR("", "Copy Binary Sensor", this); }
 | 
			
		||||
 | 
			
		||||
}  // namespace copy
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										21
									
								
								esphome/components/copy/binary_sensor/copy_binary_sensor.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								esphome/components/copy/binary_sensor/copy_binary_sensor.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/components/binary_sensor/binary_sensor.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace copy {
 | 
			
		||||
 | 
			
		||||
class CopyBinarySensor : public binary_sensor::BinarySensor, public Component {
 | 
			
		||||
 public:
 | 
			
		||||
  void set_source(binary_sensor::BinarySensor *source) { source_ = source; }
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
  float get_setup_priority() const override { return setup_priority::DATA; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  binary_sensor::BinarySensor *source_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace copy
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										42
									
								
								esphome/components/copy/button/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								esphome/components/copy/button/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,42 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.components import button
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_DEVICE_CLASS,
 | 
			
		||||
    CONF_ENTITY_CATEGORY,
 | 
			
		||||
    CONF_ICON,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_SOURCE_ID,
 | 
			
		||||
)
 | 
			
		||||
from esphome.core.entity_helpers import inherit_property_from
 | 
			
		||||
 | 
			
		||||
from .. import copy_ns
 | 
			
		||||
 | 
			
		||||
CopyButton = copy_ns.class_("CopyButton", button.Button, cg.Component)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = (
 | 
			
		||||
    button.button_schema()
 | 
			
		||||
    .extend(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(CopyButton),
 | 
			
		||||
            cv.Required(CONF_SOURCE_ID): cv.use_id(button.Button),
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
    .extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
FINAL_VALIDATE_SCHEMA = cv.All(
 | 
			
		||||
    inherit_property_from(CONF_ICON, CONF_SOURCE_ID),
 | 
			
		||||
    inherit_property_from(CONF_ENTITY_CATEGORY, CONF_SOURCE_ID),
 | 
			
		||||
    inherit_property_from(CONF_DEVICE_CLASS, CONF_SOURCE_ID),
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await button.register_button(var, config)
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
 | 
			
		||||
    source = await cg.get_variable(config[CONF_SOURCE_ID])
 | 
			
		||||
    cg.add(var.set_source(source))
 | 
			
		||||
							
								
								
									
										14
									
								
								esphome/components/copy/button/copy_button.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								esphome/components/copy/button/copy_button.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,14 @@
 | 
			
		||||
#include "copy_button.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace copy {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "copy.button";
 | 
			
		||||
 | 
			
		||||
void CopyButton::dump_config() { LOG_BUTTON("", "Copy Button", this); }
 | 
			
		||||
 | 
			
		||||
void CopyButton::press_action() { source_->press(); }
 | 
			
		||||
 | 
			
		||||
}  // namespace copy
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										22
									
								
								esphome/components/copy/button/copy_button.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								esphome/components/copy/button/copy_button.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,22 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/components/button/button.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace copy {
 | 
			
		||||
 | 
			
		||||
class CopyButton : public button::Button, public Component {
 | 
			
		||||
 public:
 | 
			
		||||
  void set_source(button::Button *source) { source_ = source; }
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
  float get_setup_priority() const override { return setup_priority::DATA; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void press_action() override;
 | 
			
		||||
 | 
			
		||||
  button::Button *source_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace copy
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										38
									
								
								esphome/components/copy/cover/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								esphome/components/copy/cover/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,38 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.components import cover
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_DEVICE_CLASS,
 | 
			
		||||
    CONF_ENTITY_CATEGORY,
 | 
			
		||||
    CONF_ICON,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_SOURCE_ID,
 | 
			
		||||
)
 | 
			
		||||
from esphome.core.entity_helpers import inherit_property_from
 | 
			
		||||
 | 
			
		||||
from .. import copy_ns
 | 
			
		||||
 | 
			
		||||
CopyCover = copy_ns.class_("CopyCover", cover.Cover, cg.Component)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cover.COVER_SCHEMA.extend(
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(): cv.declare_id(CopyCover),
 | 
			
		||||
        cv.Required(CONF_SOURCE_ID): cv.use_id(cover.Cover),
 | 
			
		||||
    }
 | 
			
		||||
).extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
 | 
			
		||||
FINAL_VALIDATE_SCHEMA = cv.All(
 | 
			
		||||
    inherit_property_from(CONF_ICON, CONF_SOURCE_ID),
 | 
			
		||||
    inherit_property_from(CONF_ENTITY_CATEGORY, CONF_SOURCE_ID),
 | 
			
		||||
    inherit_property_from(CONF_DEVICE_CLASS, CONF_SOURCE_ID),
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await cover.register_cover(var, config)
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
 | 
			
		||||
    source = await cg.get_variable(config[CONF_SOURCE_ID])
 | 
			
		||||
    cg.add(var.set_source(source))
 | 
			
		||||
							
								
								
									
										50
									
								
								esphome/components/copy/cover/copy_cover.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								esphome/components/copy/cover/copy_cover.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,50 @@
 | 
			
		||||
#include "copy_cover.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace copy {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "copy.cover";
 | 
			
		||||
 | 
			
		||||
void CopyCover::setup() {
 | 
			
		||||
  source_->add_on_state_callback([this]() {
 | 
			
		||||
    this->current_operation = this->source_->current_operation;
 | 
			
		||||
    this->position = this->source_->position;
 | 
			
		||||
    this->tilt = this->source_->tilt;
 | 
			
		||||
    this->publish_state();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  this->current_operation = this->source_->current_operation;
 | 
			
		||||
  this->position = this->source_->position;
 | 
			
		||||
  this->tilt = this->source_->tilt;
 | 
			
		||||
  this->publish_state();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void CopyCover::dump_config() { LOG_COVER("", "Copy Cover", this); }
 | 
			
		||||
 | 
			
		||||
cover::CoverTraits CopyCover::get_traits() {
 | 
			
		||||
  auto base = source_->get_traits();
 | 
			
		||||
  cover::CoverTraits traits{};
 | 
			
		||||
  // copy traits manually so it doesn't break when new options are added
 | 
			
		||||
  // but the control() method hasn't implemented them yet.
 | 
			
		||||
  traits.set_is_assumed_state(base.get_is_assumed_state());
 | 
			
		||||
  traits.set_supports_position(base.get_supports_position());
 | 
			
		||||
  traits.set_supports_tilt(base.get_supports_tilt());
 | 
			
		||||
  traits.set_supports_toggle(base.get_supports_toggle());
 | 
			
		||||
  return traits;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void CopyCover::control(const cover::CoverCall &call) {
 | 
			
		||||
  auto call2 = source_->make_call();
 | 
			
		||||
  call2.set_stop(call.get_stop());
 | 
			
		||||
  if (call.get_tilt().has_value())
 | 
			
		||||
    call2.set_tilt(*call.get_tilt());
 | 
			
		||||
  if (call.get_position().has_value())
 | 
			
		||||
    call2.set_position(*call.get_position());
 | 
			
		||||
  if (call.get_tilt().has_value())
 | 
			
		||||
    call2.set_tilt(*call.get_tilt());
 | 
			
		||||
  call2.perform();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace copy
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										25
									
								
								esphome/components/copy/cover/copy_cover.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								esphome/components/copy/cover/copy_cover.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,25 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/components/cover/cover.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace copy {
 | 
			
		||||
 | 
			
		||||
class CopyCover : public cover::Cover, public Component {
 | 
			
		||||
 public:
 | 
			
		||||
  void set_source(cover::Cover *source) { source_ = source; }
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
  float get_setup_priority() const override { return setup_priority::DATA; }
 | 
			
		||||
 | 
			
		||||
  cover::CoverTraits get_traits() override;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void control(const cover::CoverCall &call) override;
 | 
			
		||||
 | 
			
		||||
  cover::Cover *source_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace copy
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										36
									
								
								esphome/components/copy/fan/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								esphome/components/copy/fan/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,36 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.components import fan
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_ENTITY_CATEGORY,
 | 
			
		||||
    CONF_ICON,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_SOURCE_ID,
 | 
			
		||||
)
 | 
			
		||||
from esphome.core.entity_helpers import inherit_property_from
 | 
			
		||||
 | 
			
		||||
from .. import copy_ns
 | 
			
		||||
 | 
			
		||||
CopyFan = copy_ns.class_("CopyFan", fan.Fan, cg.Component)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = fan.FAN_SCHEMA.extend(
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(): cv.declare_id(CopyFan),
 | 
			
		||||
        cv.Required(CONF_SOURCE_ID): cv.use_id(fan.Fan),
 | 
			
		||||
    }
 | 
			
		||||
).extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
 | 
			
		||||
FINAL_VALIDATE_SCHEMA = cv.All(
 | 
			
		||||
    inherit_property_from(CONF_ICON, CONF_SOURCE_ID),
 | 
			
		||||
    inherit_property_from(CONF_ENTITY_CATEGORY, CONF_SOURCE_ID),
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await fan.register_fan(var, config)
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
 | 
			
		||||
    source = await cg.get_variable(config[CONF_SOURCE_ID])
 | 
			
		||||
    cg.add(var.set_source(source))
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user