mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-30 22:53:59 +00:00 
			
		
		
		
	
							
								
								
									
										2
									
								
								.github/workflows/ci-docker.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/ci-docker.yml
									
									
									
									
										vendored
									
									
								
							| @@ -11,6 +11,7 @@ on: | ||||
|       - ".github/workflows/**" | ||||
|       - "requirements*.txt" | ||||
|       - "platformio.ini" | ||||
|       - "script/platformio_install_deps.py" | ||||
|  | ||||
|   pull_request: | ||||
|     paths: | ||||
| @@ -18,6 +19,7 @@ on: | ||||
|       - ".github/workflows/**" | ||||
|       - "requirements*.txt" | ||||
|       - "platformio.ini" | ||||
|       - "script/platformio_install_deps.py" | ||||
|  | ||||
| permissions: | ||||
|   contents: read | ||||
|   | ||||
							
								
								
									
										1
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							| @@ -23,6 +23,7 @@ jobs: | ||||
|     runs-on: ubuntu-latest | ||||
|     strategy: | ||||
|       fail-fast: false | ||||
|       max-parallel: 5 | ||||
|       matrix: | ||||
|         include: | ||||
|           - id: ci-custom | ||||
|   | ||||
							
								
								
									
										2
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							| @@ -117,7 +117,7 @@ jobs: | ||||
|             --suffix "${{ matrix.image.suffix }}" | ||||
|  | ||||
|       - name: Build and push | ||||
|         uses: docker/build-push-action@v3 | ||||
|         uses: docker/build-push-action@v4 | ||||
|         with: | ||||
|           context: . | ||||
|           file: ./docker/Dockerfile | ||||
|   | ||||
							
								
								
									
										2
									
								
								.github/workflows/sync-device-classes.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/sync-device-classes.yml
									
									
									
									
										vendored
									
									
								
							| @@ -48,7 +48,7 @@ jobs: | ||||
|           echo "$delimiter" >> $GITHUB_OUTPUT | ||||
|  | ||||
|       - name: Commit changes | ||||
|         uses: peter-evans/create-pull-request@v4 | ||||
|         uses: peter-evans/create-pull-request@v5 | ||||
|         with: | ||||
|           commit-message: "Synchronise Device Classes from Home Assistant" | ||||
|           committer: esphomebot <esphome@nabucasa.com> | ||||
|   | ||||
| @@ -27,7 +27,7 @@ repos: | ||||
|           - --branch=release | ||||
|           - --branch=beta | ||||
|   - repo: https://github.com/asottile/pyupgrade | ||||
|     rev: v3.3.1 | ||||
|     rev: v3.3.2 | ||||
|     hooks: | ||||
|       - id: pyupgrade | ||||
|         args: [--py39-plus] | ||||
|   | ||||
							
								
								
									
										12
									
								
								CODEOWNERS
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								CODEOWNERS
									
									
									
									
									
								
							| @@ -21,6 +21,7 @@ esphome/components/airthings_wave_mini/* @ncareau | ||||
| esphome/components/airthings_wave_plus/* @jeromelaban | ||||
| esphome/components/am43/* @buxtronix | ||||
| esphome/components/am43/cover/* @buxtronix | ||||
| esphome/components/am43/sensor/* @buxtronix | ||||
| esphome/components/analog_threshold/* @ianchi | ||||
| esphome/components/animation/* @syndlex | ||||
| esphome/components/anova/* @buxtronix | ||||
| @@ -83,6 +84,7 @@ esphome/components/esp32_ble_server/* @jesserockz | ||||
| esphome/components/esp32_camera_web_server/* @ayufan | ||||
| esphome/components/esp32_can/* @Sympatron | ||||
| esphome/components/esp32_improv/* @jesserockz | ||||
| esphome/components/esp32_rmt_led_strip/* @jesserockz | ||||
| esphome/components/esp8266/* @esphome/core | ||||
| esphome/components/ethernet_info/* @gtjadsonsantos | ||||
| esphome/components/exposure_notifications/* @OttoWinter | ||||
| @@ -94,6 +96,7 @@ esphome/components/feedback/* @ianchi | ||||
| esphome/components/fingerprint_grow/* @OnFreund @loongyh | ||||
| esphome/components/fs3000/* @kahrendt | ||||
| esphome/components/globals/* @esphome/core | ||||
| esphome/components/gp8403/* @jesserockz | ||||
| esphome/components/gpio/* @esphome/core | ||||
| esphome/components/gps/* @coogle | ||||
| esphome/components/graph/* @synco | ||||
| @@ -106,13 +109,16 @@ esphome/components/heatpumpir/* @rob-deutsch | ||||
| esphome/components/hitachi_ac424/* @sourabhjaiswal | ||||
| esphome/components/homeassistant/* @OttoWinter | ||||
| esphome/components/honeywellabp/* @RubyBailey | ||||
| esphome/components/host/* @esphome/core | ||||
| esphome/components/hrxl_maxsonar_wr/* @netmikey | ||||
| esphome/components/hte501/* @Stock-M | ||||
| esphome/components/hydreon_rgxx/* @functionpointer | ||||
| esphome/components/hyt271/* @Philippe12 | ||||
| esphome/components/i2c/* @esphome/core | ||||
| esphome/components/i2s_audio/* @jesserockz | ||||
| esphome/components/i2s_audio/media_player/* @jesserockz | ||||
| esphome/components/i2s_audio/microphone/* @jesserockz | ||||
| esphome/components/i2s_audio/speaker/* @jesserockz | ||||
| esphome/components/ili9xxx/* @nielsnl68 | ||||
| esphome/components/improv_base/* @esphome/core | ||||
| esphome/components/improv_serial/* @esphome/core | ||||
| @@ -138,6 +144,7 @@ esphome/components/ltr390/* @sjtrny | ||||
| esphome/components/matrix_keypad/* @ssieb | ||||
| esphome/components/max31865/* @DAVe3283 | ||||
| esphome/components/max44009/* @berfenger | ||||
| esphome/components/max6956/* @looping40 | ||||
| esphome/components/max7219digit/* @rspaargaren | ||||
| esphome/components/max9611/* @mckaymatthew | ||||
| esphome/components/mcp23008/* @jesserockz | ||||
| @@ -162,6 +169,7 @@ esphome/components/midea/* @dudanov | ||||
| esphome/components/midea_ir/* @dudanov | ||||
| esphome/components/mitsubishi/* @RubyBailey | ||||
| esphome/components/mlx90393/* @functionpointer | ||||
| esphome/components/mlx90614/* @jesserockz | ||||
| esphome/components/mmc5603/* @benhoff | ||||
| esphome/components/modbus_controller/* @martgras | ||||
| esphome/components/modbus_controller/binary_sensor/* @martgras | ||||
| @@ -186,6 +194,7 @@ esphome/components/nfc/* @jesserockz | ||||
| esphome/components/number/* @esphome/core | ||||
| esphome/components/ota/* @esphome/core | ||||
| esphome/components/output/* @esphome/core | ||||
| esphome/components/pca6416a/* @Mat931 | ||||
| esphome/components/pca9554/* @hwstar | ||||
| esphome/components/pcf85063/* @brogon | ||||
| esphome/components/pid/* @OttoWinter | ||||
| @@ -232,7 +241,7 @@ esphome/components/shutdown/* @esphome/core @jsuanet | ||||
| esphome/components/sigma_delta_output/* @Cat-Ion | ||||
| esphome/components/sim800l/* @glmnet | ||||
| esphome/components/sm10bit_base/* @Cossid | ||||
| esphome/components/sm2135/* @BoukeHaarsma23 | ||||
| esphome/components/sm2135/* @BoukeHaarsma23 @dd32 @matika77 | ||||
| esphome/components/sm2235/* @Cossid | ||||
| esphome/components/sm2335/* @Cossid | ||||
| esphome/components/sml/* @alengwenus | ||||
| @@ -240,6 +249,7 @@ esphome/components/smt100/* @piechade | ||||
| esphome/components/sn74hc165/* @jesserockz | ||||
| esphome/components/socket/* @esphome/core | ||||
| esphome/components/sonoff_d1/* @anatoly-savchenkov | ||||
| esphome/components/speaker/* @jesserockz | ||||
| esphome/components/spi/* @esphome/core | ||||
| esphome/components/sprinkler/* @kbx81 | ||||
| esphome/components/sps30/* @martgras | ||||
|   | ||||
| @@ -24,8 +24,9 @@ RUN \ | ||||
|         python3-setuptools=52.0.0-4 \ | ||||
|         python3-pil=8.1.2+dfsg-0.3+deb11u1 \ | ||||
|         python3-cryptography=3.3.2-1 \ | ||||
|         python3-venv=3.9.2-3 \ | ||||
|         iputils-ping=3:20210202-1 \ | ||||
|         git=1:2.30.2-1 \ | ||||
|         git=1:2.30.2-1+deb11u2 \ | ||||
|         curl=7.74.0-1.3+deb11u7 \ | ||||
|         openssh-client=1:8.4p1-5+deb11u1 \ | ||||
|     && rm -rf \ | ||||
| @@ -59,10 +60,10 @@ RUN \ | ||||
|  | ||||
|  | ||||
| # First install requirements to leverage caching when requirements don't change | ||||
| COPY requirements.txt requirements_optional.txt docker/platformio_install_deps.py platformio.ini / | ||||
| COPY requirements.txt requirements_optional.txt script/platformio_install_deps.py platformio.ini / | ||||
| RUN \ | ||||
|     pip3 install --no-cache-dir -r /requirements.txt -r /requirements_optional.txt \ | ||||
|     && /platformio_install_deps.py /platformio.ini | ||||
|     && /platformio_install_deps.py /platformio.ini --libraries | ||||
|  | ||||
|  | ||||
| # ======================= docker-type image ======================= | ||||
|   | ||||
| @@ -1,30 +0,0 @@ | ||||
| #!/usr/bin/env python3 | ||||
| # This script is used in the docker containers to preinstall | ||||
| # all platformio libraries in the global storage | ||||
|  | ||||
| import configparser | ||||
| import subprocess | ||||
| import sys | ||||
|  | ||||
| config = configparser.ConfigParser(inline_comment_prefixes=(';', )) | ||||
| config.read(sys.argv[1]) | ||||
|  | ||||
| libs = [] | ||||
| # Extract from every lib_deps key in all sections | ||||
| for section in config.sections(): | ||||
|     conf = config[section] | ||||
|     if "lib_deps" not in conf: | ||||
|         continue | ||||
|     for lib_dep in conf["lib_deps"].splitlines(): | ||||
|         if not lib_dep: | ||||
|             # Empty line or comment | ||||
|             continue | ||||
|         if lib_dep.startswith("${"): | ||||
|             # Extending from another section | ||||
|             continue | ||||
|         if "@" not in lib_dep: | ||||
|             # No version pinned, this is an internal lib | ||||
|             continue | ||||
|         libs.append(lib_dep) | ||||
|  | ||||
| subprocess.check_call(['platformio', 'lib', '-g', 'install', *libs]) | ||||
| @@ -1 +1,118 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome import pins | ||||
| from esphome.const import CONF_INPUT | ||||
|  | ||||
| from esphome.core import CORE | ||||
| from esphome.components.esp32 import get_esp32_variant | ||||
| from esphome.components.esp32.const import ( | ||||
|     VARIANT_ESP32, | ||||
|     VARIANT_ESP32C3, | ||||
|     VARIANT_ESP32H2, | ||||
|     VARIANT_ESP32S2, | ||||
|     VARIANT_ESP32S3, | ||||
| ) | ||||
|  | ||||
| CODEOWNERS = ["@esphome/core"] | ||||
|  | ||||
| ATTENUATION_MODES = { | ||||
|     "0db": cg.global_ns.ADC_ATTEN_DB_0, | ||||
|     "2.5db": cg.global_ns.ADC_ATTEN_DB_2_5, | ||||
|     "6db": cg.global_ns.ADC_ATTEN_DB_6, | ||||
|     "11db": cg.global_ns.ADC_ATTEN_DB_11, | ||||
|     "auto": "auto", | ||||
| } | ||||
|  | ||||
| adc1_channel_t = cg.global_ns.enum("adc1_channel_t") | ||||
|  | ||||
| # From https://github.com/espressif/esp-idf/blob/master/components/driver/include/driver/adc_common.h | ||||
| # pin to adc1 channel mapping | ||||
| ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = { | ||||
|     VARIANT_ESP32: { | ||||
|         36: adc1_channel_t.ADC1_CHANNEL_0, | ||||
|         37: adc1_channel_t.ADC1_CHANNEL_1, | ||||
|         38: adc1_channel_t.ADC1_CHANNEL_2, | ||||
|         39: adc1_channel_t.ADC1_CHANNEL_3, | ||||
|         32: adc1_channel_t.ADC1_CHANNEL_4, | ||||
|         33: adc1_channel_t.ADC1_CHANNEL_5, | ||||
|         34: adc1_channel_t.ADC1_CHANNEL_6, | ||||
|         35: adc1_channel_t.ADC1_CHANNEL_7, | ||||
|     }, | ||||
|     VARIANT_ESP32S2: { | ||||
|         1: adc1_channel_t.ADC1_CHANNEL_0, | ||||
|         2: adc1_channel_t.ADC1_CHANNEL_1, | ||||
|         3: adc1_channel_t.ADC1_CHANNEL_2, | ||||
|         4: adc1_channel_t.ADC1_CHANNEL_3, | ||||
|         5: adc1_channel_t.ADC1_CHANNEL_4, | ||||
|         6: adc1_channel_t.ADC1_CHANNEL_5, | ||||
|         7: adc1_channel_t.ADC1_CHANNEL_6, | ||||
|         8: adc1_channel_t.ADC1_CHANNEL_7, | ||||
|         9: adc1_channel_t.ADC1_CHANNEL_8, | ||||
|         10: adc1_channel_t.ADC1_CHANNEL_9, | ||||
|     }, | ||||
|     VARIANT_ESP32S3: { | ||||
|         1: adc1_channel_t.ADC1_CHANNEL_0, | ||||
|         2: adc1_channel_t.ADC1_CHANNEL_1, | ||||
|         3: adc1_channel_t.ADC1_CHANNEL_2, | ||||
|         4: adc1_channel_t.ADC1_CHANNEL_3, | ||||
|         5: adc1_channel_t.ADC1_CHANNEL_4, | ||||
|         6: adc1_channel_t.ADC1_CHANNEL_5, | ||||
|         7: adc1_channel_t.ADC1_CHANNEL_6, | ||||
|         8: adc1_channel_t.ADC1_CHANNEL_7, | ||||
|         9: adc1_channel_t.ADC1_CHANNEL_8, | ||||
|         10: adc1_channel_t.ADC1_CHANNEL_9, | ||||
|     }, | ||||
|     VARIANT_ESP32C3: { | ||||
|         0: adc1_channel_t.ADC1_CHANNEL_0, | ||||
|         1: adc1_channel_t.ADC1_CHANNEL_1, | ||||
|         2: adc1_channel_t.ADC1_CHANNEL_2, | ||||
|         3: adc1_channel_t.ADC1_CHANNEL_3, | ||||
|         4: adc1_channel_t.ADC1_CHANNEL_4, | ||||
|     }, | ||||
|     VARIANT_ESP32H2: { | ||||
|         0: adc1_channel_t.ADC1_CHANNEL_0, | ||||
|         1: adc1_channel_t.ADC1_CHANNEL_1, | ||||
|         2: adc1_channel_t.ADC1_CHANNEL_2, | ||||
|         3: adc1_channel_t.ADC1_CHANNEL_3, | ||||
|         4: adc1_channel_t.ADC1_CHANNEL_4, | ||||
|     }, | ||||
| } | ||||
|  | ||||
|  | ||||
| def validate_adc_pin(value): | ||||
|     if str(value).upper() == "VCC": | ||||
|         return cv.only_on_esp8266("VCC") | ||||
|  | ||||
|     if str(value).upper() == "TEMPERATURE": | ||||
|         return cv.only_on_rp2040("TEMPERATURE") | ||||
|  | ||||
|     if CORE.is_esp32: | ||||
|         value = pins.internal_gpio_input_pin_number(value) | ||||
|         variant = get_esp32_variant() | ||||
|         if variant not in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL: | ||||
|             raise cv.Invalid(f"This ESP32 variant ({variant}) is not supported") | ||||
|  | ||||
|         if value not in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant]: | ||||
|             raise cv.Invalid(f"{variant} doesn't support ADC on this pin") | ||||
|         return pins.internal_gpio_input_pin_schema(value) | ||||
|  | ||||
|     if CORE.is_esp8266: | ||||
|         from esphome.components.esp8266.gpio import CONF_ANALOG | ||||
|  | ||||
|         value = pins.internal_gpio_pin_number({CONF_ANALOG: True, CONF_INPUT: True})( | ||||
|             value | ||||
|         ) | ||||
|  | ||||
|         if value != 17:  # A0 | ||||
|             raise cv.Invalid("ESP8266: Only pin A0 (GPIO17) supports ADC.") | ||||
|         return pins.gpio_pin_schema( | ||||
|             {CONF_ANALOG: True, CONF_INPUT: True}, internal=True | ||||
|         )(value) | ||||
|  | ||||
|     if CORE.is_rp2040: | ||||
|         value = pins.internal_gpio_input_pin_number(value) | ||||
|         if value not in (26, 27, 28, 29): | ||||
|             raise cv.Invalid("RP2040: Only pins 26, 27, 28 and 29 support ADC.") | ||||
|         return pins.internal_gpio_input_pin_schema(value) | ||||
|  | ||||
|     raise NotImplementedError | ||||
|   | ||||
| @@ -1,133 +1,27 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome import pins | ||||
| from esphome.components import sensor, voltage_sampler | ||||
| from esphome.components.esp32 import get_esp32_variant | ||||
| from esphome.const import ( | ||||
|     CONF_ATTENUATION, | ||||
|     CONF_RAW, | ||||
|     CONF_ID, | ||||
|     CONF_INPUT, | ||||
|     CONF_NUMBER, | ||||
|     CONF_PIN, | ||||
|     CONF_RAW, | ||||
|     DEVICE_CLASS_VOLTAGE, | ||||
|     STATE_CLASS_MEASUREMENT, | ||||
|     UNIT_VOLT, | ||||
| ) | ||||
| from esphome.core import CORE | ||||
| from esphome.components.esp32 import get_esp32_variant | ||||
| from esphome.components.esp32.const import ( | ||||
|     VARIANT_ESP32, | ||||
|     VARIANT_ESP32C3, | ||||
|     VARIANT_ESP32H2, | ||||
|     VARIANT_ESP32S2, | ||||
|     VARIANT_ESP32S3, | ||||
|  | ||||
| from . import ( | ||||
|     ATTENUATION_MODES, | ||||
|     ESP32_VARIANT_ADC1_PIN_TO_CHANNEL, | ||||
|     validate_adc_pin, | ||||
| ) | ||||
|  | ||||
|  | ||||
| AUTO_LOAD = ["voltage_sampler"] | ||||
|  | ||||
| ATTENUATION_MODES = { | ||||
|     "0db": cg.global_ns.ADC_ATTEN_DB_0, | ||||
|     "2.5db": cg.global_ns.ADC_ATTEN_DB_2_5, | ||||
|     "6db": cg.global_ns.ADC_ATTEN_DB_6, | ||||
|     "11db": cg.global_ns.ADC_ATTEN_DB_11, | ||||
|     "auto": "auto", | ||||
| } | ||||
|  | ||||
| adc1_channel_t = cg.global_ns.enum("adc1_channel_t") | ||||
|  | ||||
| # From https://github.com/espressif/esp-idf/blob/master/components/driver/include/driver/adc_common.h | ||||
| # pin to adc1 channel mapping | ||||
| ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = { | ||||
|     VARIANT_ESP32: { | ||||
|         36: adc1_channel_t.ADC1_CHANNEL_0, | ||||
|         37: adc1_channel_t.ADC1_CHANNEL_1, | ||||
|         38: adc1_channel_t.ADC1_CHANNEL_2, | ||||
|         39: adc1_channel_t.ADC1_CHANNEL_3, | ||||
|         32: adc1_channel_t.ADC1_CHANNEL_4, | ||||
|         33: adc1_channel_t.ADC1_CHANNEL_5, | ||||
|         34: adc1_channel_t.ADC1_CHANNEL_6, | ||||
|         35: adc1_channel_t.ADC1_CHANNEL_7, | ||||
|     }, | ||||
|     VARIANT_ESP32S2: { | ||||
|         1: adc1_channel_t.ADC1_CHANNEL_0, | ||||
|         2: adc1_channel_t.ADC1_CHANNEL_1, | ||||
|         3: adc1_channel_t.ADC1_CHANNEL_2, | ||||
|         4: adc1_channel_t.ADC1_CHANNEL_3, | ||||
|         5: adc1_channel_t.ADC1_CHANNEL_4, | ||||
|         6: adc1_channel_t.ADC1_CHANNEL_5, | ||||
|         7: adc1_channel_t.ADC1_CHANNEL_6, | ||||
|         8: adc1_channel_t.ADC1_CHANNEL_7, | ||||
|         9: adc1_channel_t.ADC1_CHANNEL_8, | ||||
|         10: adc1_channel_t.ADC1_CHANNEL_9, | ||||
|     }, | ||||
|     VARIANT_ESP32S3: { | ||||
|         1: adc1_channel_t.ADC1_CHANNEL_0, | ||||
|         2: adc1_channel_t.ADC1_CHANNEL_1, | ||||
|         3: adc1_channel_t.ADC1_CHANNEL_2, | ||||
|         4: adc1_channel_t.ADC1_CHANNEL_3, | ||||
|         5: adc1_channel_t.ADC1_CHANNEL_4, | ||||
|         6: adc1_channel_t.ADC1_CHANNEL_5, | ||||
|         7: adc1_channel_t.ADC1_CHANNEL_6, | ||||
|         8: adc1_channel_t.ADC1_CHANNEL_7, | ||||
|         9: adc1_channel_t.ADC1_CHANNEL_8, | ||||
|         10: adc1_channel_t.ADC1_CHANNEL_9, | ||||
|     }, | ||||
|     VARIANT_ESP32C3: { | ||||
|         0: adc1_channel_t.ADC1_CHANNEL_0, | ||||
|         1: adc1_channel_t.ADC1_CHANNEL_1, | ||||
|         2: adc1_channel_t.ADC1_CHANNEL_2, | ||||
|         3: adc1_channel_t.ADC1_CHANNEL_3, | ||||
|         4: adc1_channel_t.ADC1_CHANNEL_4, | ||||
|     }, | ||||
|     VARIANT_ESP32H2: { | ||||
|         0: adc1_channel_t.ADC1_CHANNEL_0, | ||||
|         1: adc1_channel_t.ADC1_CHANNEL_1, | ||||
|         2: adc1_channel_t.ADC1_CHANNEL_2, | ||||
|         3: adc1_channel_t.ADC1_CHANNEL_3, | ||||
|         4: adc1_channel_t.ADC1_CHANNEL_4, | ||||
|     }, | ||||
| } | ||||
|  | ||||
|  | ||||
| def validate_adc_pin(value): | ||||
|     if str(value).upper() == "VCC": | ||||
|         return cv.only_on_esp8266("VCC") | ||||
|  | ||||
|     if str(value).upper() == "TEMPERATURE": | ||||
|         return cv.only_on_rp2040("TEMPERATURE") | ||||
|  | ||||
|     if CORE.is_esp32: | ||||
|         value = pins.internal_gpio_input_pin_number(value) | ||||
|         variant = get_esp32_variant() | ||||
|         if variant not in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL: | ||||
|             raise cv.Invalid(f"This ESP32 variant ({variant}) is not supported") | ||||
|  | ||||
|         if value not in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant]: | ||||
|             raise cv.Invalid(f"{variant} doesn't support ADC on this pin") | ||||
|         return pins.internal_gpio_input_pin_schema(value) | ||||
|  | ||||
|     if CORE.is_esp8266: | ||||
|         from esphome.components.esp8266.gpio import CONF_ANALOG | ||||
|  | ||||
|         value = pins.internal_gpio_pin_number({CONF_ANALOG: True, CONF_INPUT: True})( | ||||
|             value | ||||
|         ) | ||||
|  | ||||
|         if value != 17:  # A0 | ||||
|             raise cv.Invalid("ESP8266: Only pin A0 (GPIO17) supports ADC.") | ||||
|         return pins.gpio_pin_schema( | ||||
|             {CONF_ANALOG: True, CONF_INPUT: True}, internal=True | ||||
|         )(value) | ||||
|  | ||||
|     if CORE.is_rp2040: | ||||
|         value = pins.internal_gpio_input_pin_number(value) | ||||
|         if value not in (26, 27, 28, 29): | ||||
|             raise cv.Invalid("RP2040: Only pins 26, 27, 28 and 29 support ADC.") | ||||
|         return pins.internal_gpio_input_pin_schema(value) | ||||
|  | ||||
|     raise NotImplementedError | ||||
|  | ||||
|  | ||||
| def validate_config(config): | ||||
|     if config[CONF_RAW] and config.get(CONF_ATTENUATION, None) == "auto": | ||||
|   | ||||
| @@ -0,0 +1 @@ | ||||
| CODEOWNERS = ["@buxtronix"] | ||||
|   | ||||
| @@ -5,7 +5,7 @@ from esphome.const import CONF_ID, CONF_PIN | ||||
|  | ||||
| CODEOWNERS = ["@buxtronix"] | ||||
| DEPENDENCIES = ["ble_client"] | ||||
| AUTO_LOAD = ["am43", "sensor"] | ||||
| AUTO_LOAD = ["am43"] | ||||
|  | ||||
| CONF_INVERT_POSITION = "invert_position" | ||||
|  | ||||
| @@ -27,10 +27,10 @@ CONFIG_SCHEMA = ( | ||||
| ) | ||||
|  | ||||
|  | ||||
| def to_code(config): | ||||
| async def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     cg.add(var.set_pin(config[CONF_PIN])) | ||||
|     cg.add(var.set_invert_position(config[CONF_INVERT_POSITION])) | ||||
|     yield cg.register_component(var, config) | ||||
|     yield cover.register_cover(var, config) | ||||
|     yield ble_client.register_ble_node(var, config) | ||||
|     await cg.register_component(var, config) | ||||
|     await cover.register_cover(var, config) | ||||
|     await ble_client.register_ble_node(var, config) | ||||
|   | ||||
| @@ -40,6 +40,7 @@ void Am43Component::loop() { | ||||
|  | ||||
| CoverTraits Am43Component::get_traits() { | ||||
|   auto traits = CoverTraits(); | ||||
|   traits.set_supports_stop(true); | ||||
|   traits.set_supports_position(true); | ||||
|   traits.set_supports_tilt(false); | ||||
|   traits.set_is_assumed_state(false); | ||||
|   | ||||
| @@ -11,6 +11,7 @@ from esphome.const import ( | ||||
|     UNIT_PERCENT, | ||||
| ) | ||||
| 
 | ||||
| AUTO_LOAD = ["am43"] | ||||
| CODEOWNERS = ["@buxtronix"] | ||||
| 
 | ||||
| am43_ns = cg.esphome_ns.namespace("am43") | ||||
| @@ -38,15 +39,15 @@ CONFIG_SCHEMA = ( | ||||
| ) | ||||
| 
 | ||||
| 
 | ||||
| def to_code(config): | ||||
| async def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     yield cg.register_component(var, config) | ||||
|     yield ble_client.register_ble_node(var, config) | ||||
|     await cg.register_component(var, config) | ||||
|     await ble_client.register_ble_node(var, config) | ||||
| 
 | ||||
|     if CONF_BATTERY_LEVEL in config: | ||||
|         sens = yield sensor.new_sensor(config[CONF_BATTERY_LEVEL]) | ||||
|         sens = await sensor.new_sensor(config[CONF_BATTERY_LEVEL]) | ||||
|         cg.add(var.set_battery(sens)) | ||||
| 
 | ||||
|     if CONF_ILLUMINANCE in config: | ||||
|         sens = yield sensor.new_sensor(config[CONF_ILLUMINANCE]) | ||||
|         sens = await sensor.new_sensor(config[CONF_ILLUMINANCE]) | ||||
|         cg.add(var.set_illuminance(sens)) | ||||
| @@ -1,6 +1,6 @@ | ||||
| #include "am43.h" | ||||
| #include "esphome/core/log.h" | ||||
| #include "am43_sensor.h" | ||||
| #include "esphome/core/hal.h" | ||||
| #include "esphome/core/log.h" | ||||
| 
 | ||||
| #ifdef USE_ESP32 | ||||
| 
 | ||||
| @@ -4,7 +4,6 @@ from esphome.components import i2c | ||||
| from esphome.const import CONF_ID | ||||
|  | ||||
| DEPENDENCIES = ["i2c"] | ||||
| AUTO_LOAD = ["sensor", "binary_sensor"] | ||||
| MULTI_CONF = True | ||||
|  | ||||
| CONF_APDS9960_ID = "apds9960_id" | ||||
|   | ||||
| @@ -116,8 +116,12 @@ void APDS9960::setup() { | ||||
|   APDS9960_WRITE_BYTE(0x80, val); | ||||
| } | ||||
| bool APDS9960::is_color_enabled_() const { | ||||
|   return this->red_channel_ != nullptr || this->green_channel_ != nullptr || this->blue_channel_ != nullptr || | ||||
|          this->clear_channel_ != nullptr; | ||||
| #ifdef USE_SENSOR | ||||
|   return this->red_sensor_ != nullptr || this->green_sensor_ != nullptr || this->blue_sensor_ != nullptr || | ||||
|          this->clear_sensor_ != nullptr; | ||||
| #else | ||||
|   return false; | ||||
| #endif | ||||
| } | ||||
|  | ||||
| void APDS9960::dump_config() { | ||||
| @@ -125,6 +129,15 @@ void APDS9960::dump_config() { | ||||
|   LOG_I2C_DEVICE(this); | ||||
|  | ||||
|   LOG_UPDATE_INTERVAL(this); | ||||
|  | ||||
| #ifdef USE_SENSOR | ||||
|   LOG_SENSOR("  ", "Red channel", this->red_sensor_); | ||||
|   LOG_SENSOR("  ", "Green channel", this->green_sensor_); | ||||
|   LOG_SENSOR("  ", "Blue channel", this->blue_sensor_); | ||||
|   LOG_SENSOR("  ", "Clear channel", this->clear_sensor_); | ||||
|   LOG_SENSOR("  ", "Proximity", this->proximity_sensor_); | ||||
| #endif | ||||
|  | ||||
|   if (this->is_failed()) { | ||||
|     switch (this->error_code_) { | ||||
|       case COMMUNICATION_FAILED: | ||||
| @@ -181,17 +194,22 @@ void APDS9960::read_color_data_(uint8_t status) { | ||||
|   float blue_perc = (uint_blue / float(UINT16_MAX)) * 100.0f; | ||||
|  | ||||
|   ESP_LOGD(TAG, "Got clear=%.1f%% red=%.1f%% green=%.1f%% blue=%.1f%%", clear_perc, red_perc, green_perc, blue_perc); | ||||
|   if (this->clear_channel_ != nullptr) | ||||
|     this->clear_channel_->publish_state(clear_perc); | ||||
|   if (this->red_channel_ != nullptr) | ||||
|     this->red_channel_->publish_state(red_perc); | ||||
|   if (this->green_channel_ != nullptr) | ||||
|     this->green_channel_->publish_state(green_perc); | ||||
|   if (this->blue_channel_ != nullptr) | ||||
|     this->blue_channel_->publish_state(blue_perc); | ||||
| #ifdef USE_SENSOR | ||||
|   if (this->clear_sensor_ != nullptr) | ||||
|     this->clear_sensor_->publish_state(clear_perc); | ||||
|   if (this->red_sensor_ != nullptr) | ||||
|     this->red_sensor_->publish_state(red_perc); | ||||
|   if (this->green_sensor_ != nullptr) | ||||
|     this->green_sensor_->publish_state(green_perc); | ||||
|   if (this->blue_sensor_ != nullptr) | ||||
|     this->blue_sensor_->publish_state(blue_perc); | ||||
| #endif | ||||
| } | ||||
| void APDS9960::read_proximity_data_(uint8_t status) { | ||||
|   if (this->proximity_ == nullptr) | ||||
| #ifndef USE_SENSOR | ||||
|   return; | ||||
| #else | ||||
|   if (this->proximity_sensor_ == nullptr) | ||||
|     return; | ||||
|  | ||||
|   if ((status & 0b10) == 0x00) { | ||||
| @@ -204,7 +222,8 @@ void APDS9960::read_proximity_data_(uint8_t status) { | ||||
|  | ||||
|   float prox_perc = (prox / float(UINT8_MAX)) * 100.0f; | ||||
|   ESP_LOGD(TAG, "Got proximity=%.1f%%", prox_perc); | ||||
|   this->proximity_->publish_state(prox_perc); | ||||
|   this->proximity_sensor_->publish_state(prox_perc); | ||||
| #endif | ||||
| } | ||||
| void APDS9960::read_gesture_data_() { | ||||
|   if (!this->is_gesture_enabled_()) | ||||
| @@ -256,28 +275,29 @@ void APDS9960::read_gesture_data_() { | ||||
|   } | ||||
| } | ||||
| void APDS9960::report_gesture_(int gesture) { | ||||
| #ifdef USE_BINARY_SENSOR | ||||
|   binary_sensor::BinarySensor *bin; | ||||
|   switch (gesture) { | ||||
|     case 1: | ||||
|       bin = this->up_direction_; | ||||
|       bin = this->up_direction_binary_sensor_; | ||||
|       this->gesture_up_started_ = false; | ||||
|       this->gesture_down_started_ = false; | ||||
|       ESP_LOGD(TAG, "Got gesture UP"); | ||||
|       break; | ||||
|     case 2: | ||||
|       bin = this->down_direction_; | ||||
|       bin = this->down_direction_binary_sensor_; | ||||
|       this->gesture_up_started_ = false; | ||||
|       this->gesture_down_started_ = false; | ||||
|       ESP_LOGD(TAG, "Got gesture DOWN"); | ||||
|       break; | ||||
|     case 3: | ||||
|       bin = this->left_direction_; | ||||
|       bin = this->left_direction_binary_sensor_; | ||||
|       this->gesture_left_started_ = false; | ||||
|       this->gesture_right_started_ = false; | ||||
|       ESP_LOGD(TAG, "Got gesture LEFT"); | ||||
|       break; | ||||
|     case 4: | ||||
|       bin = this->right_direction_; | ||||
|       bin = this->right_direction_binary_sensor_; | ||||
|       this->gesture_left_started_ = false; | ||||
|       this->gesture_right_started_ = false; | ||||
|       ESP_LOGD(TAG, "Got gesture RIGHT"); | ||||
| @@ -290,6 +310,7 @@ void APDS9960::report_gesture_(int gesture) { | ||||
|     bin->publish_state(true); | ||||
|     bin->publish_state(false); | ||||
|   } | ||||
| #endif | ||||
| } | ||||
| void APDS9960::process_dataset_(int up, int down, int left, int right) { | ||||
|   /* Algorithm: (see Figure 11 in datasheet) | ||||
| @@ -365,10 +386,22 @@ void APDS9960::process_dataset_(int up, int down, int left, int right) { | ||||
|   } | ||||
| } | ||||
| float APDS9960::get_setup_priority() const { return setup_priority::DATA; } | ||||
| bool APDS9960::is_proximity_enabled_() const { return this->proximity_ != nullptr || this->is_gesture_enabled_(); } | ||||
| bool APDS9960::is_proximity_enabled_() const { | ||||
|   return | ||||
| #ifdef USE_SENSOR | ||||
|       this->proximity_sensor_ != nullptr | ||||
| #else | ||||
|       false | ||||
| #endif | ||||
|       || this->is_gesture_enabled_(); | ||||
| } | ||||
| bool APDS9960::is_gesture_enabled_() const { | ||||
|   return this->up_direction_ != nullptr || this->left_direction_ != nullptr || this->down_direction_ != nullptr || | ||||
|          this->right_direction_ != nullptr; | ||||
| #ifdef USE_BINARY_SENSOR | ||||
|   return this->up_direction_binary_sensor_ != nullptr || this->left_direction_binary_sensor_ != nullptr || | ||||
|          this->down_direction_binary_sensor_ != nullptr || this->right_direction_binary_sensor_ != nullptr; | ||||
| #else | ||||
|   return false; | ||||
| #endif | ||||
| } | ||||
|  | ||||
| }  // namespace apds9960 | ||||
|   | ||||
| @@ -1,14 +1,34 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/components/i2c/i2c.h" | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/defines.h" | ||||
| #ifdef USE_SENSOR | ||||
| #include "esphome/components/sensor/sensor.h" | ||||
| #endif | ||||
| #ifdef USE_BINARY_SENSOR | ||||
| #include "esphome/components/binary_sensor/binary_sensor.h" | ||||
| #endif | ||||
|  | ||||
| namespace esphome { | ||||
| namespace apds9960 { | ||||
|  | ||||
| class APDS9960 : public PollingComponent, public i2c::I2CDevice { | ||||
| #ifdef USE_SENSOR | ||||
|   SUB_SENSOR(red) | ||||
|   SUB_SENSOR(green) | ||||
|   SUB_SENSOR(blue) | ||||
|   SUB_SENSOR(clear) | ||||
|   SUB_SENSOR(proximity) | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_BINARY_SENSOR | ||||
|   SUB_BINARY_SENSOR(up_direction) | ||||
|   SUB_BINARY_SENSOR(right_direction) | ||||
|   SUB_BINARY_SENSOR(down_direction) | ||||
|   SUB_BINARY_SENSOR(left_direction) | ||||
| #endif | ||||
|  | ||||
|  public: | ||||
|   void setup() override; | ||||
|   void dump_config() override; | ||||
| @@ -23,16 +43,6 @@ class APDS9960 : public PollingComponent, public i2c::I2CDevice { | ||||
|   void set_gesture_gain(uint8_t gain) { this->gesture_gain_ = gain; } | ||||
|   void set_gesture_wait_time(uint8_t wait_time) { this->gesture_wait_time_ = wait_time; } | ||||
|  | ||||
|   void set_red_channel(sensor::Sensor *red_channel) { red_channel_ = red_channel; } | ||||
|   void set_green_channel(sensor::Sensor *green_channel) { green_channel_ = green_channel; } | ||||
|   void set_blue_channel(sensor::Sensor *blue_channel) { blue_channel_ = blue_channel; } | ||||
|   void set_clear_channel(sensor::Sensor *clear_channel) { clear_channel_ = clear_channel; } | ||||
|   void set_up_direction(binary_sensor::BinarySensor *up_direction) { up_direction_ = up_direction; } | ||||
|   void set_right_direction(binary_sensor::BinarySensor *right_direction) { right_direction_ = right_direction; } | ||||
|   void set_down_direction(binary_sensor::BinarySensor *down_direction) { down_direction_ = down_direction; } | ||||
|   void set_left_direction(binary_sensor::BinarySensor *left_direction) { left_direction_ = left_direction; } | ||||
|   void set_proximity(sensor::Sensor *proximity) { proximity_ = proximity; } | ||||
|  | ||||
|  protected: | ||||
|   bool is_color_enabled_() const; | ||||
|   bool is_proximity_enabled_() const; | ||||
| @@ -50,15 +60,6 @@ class APDS9960 : public PollingComponent, public i2c::I2CDevice { | ||||
|   uint8_t gesture_gain_; | ||||
|   uint8_t gesture_wait_time_; | ||||
|  | ||||
|   sensor::Sensor *red_channel_{nullptr}; | ||||
|   sensor::Sensor *green_channel_{nullptr}; | ||||
|   sensor::Sensor *blue_channel_{nullptr}; | ||||
|   sensor::Sensor *clear_channel_{nullptr}; | ||||
|   binary_sensor::BinarySensor *up_direction_{nullptr}; | ||||
|   binary_sensor::BinarySensor *right_direction_{nullptr}; | ||||
|   binary_sensor::BinarySensor *down_direction_{nullptr}; | ||||
|   binary_sensor::BinarySensor *left_direction_{nullptr}; | ||||
|   sensor::Sensor *proximity_{nullptr}; | ||||
|   enum ErrorCode { | ||||
|     NONE = 0, | ||||
|     COMMUNICATION_FAILED, | ||||
|   | ||||
| @@ -6,19 +6,14 @@ from . import APDS9960, CONF_APDS9960_ID | ||||
|  | ||||
| DEPENDENCIES = ["apds9960"] | ||||
|  | ||||
| DIRECTIONS = { | ||||
|     "UP": "set_up_direction", | ||||
|     "DOWN": "set_down_direction", | ||||
|     "LEFT": "set_left_direction", | ||||
|     "RIGHT": "set_right_direction", | ||||
| } | ||||
| DIRECTIONS = ["up", "down", "left", "right"] | ||||
|  | ||||
| CONFIG_SCHEMA = binary_sensor.binary_sensor_schema( | ||||
|     device_class=DEVICE_CLASS_MOVING | ||||
| ).extend( | ||||
|     { | ||||
|         cv.GenerateID(CONF_APDS9960_ID): cv.use_id(APDS9960), | ||||
|         cv.Required(CONF_DIRECTION): cv.one_of(*DIRECTIONS, upper=True), | ||||
|         cv.Required(CONF_DIRECTION): cv.one_of(*DIRECTIONS, lower=True), | ||||
|     } | ||||
| ) | ||||
|  | ||||
| @@ -26,5 +21,5 @@ CONFIG_SCHEMA = binary_sensor.binary_sensor_schema( | ||||
| async def to_code(config): | ||||
|     hub = await cg.get_variable(config[CONF_APDS9960_ID]) | ||||
|     var = await binary_sensor.new_binary_sensor(config) | ||||
|     func = getattr(hub, DIRECTIONS[config[CONF_DIRECTION]]) | ||||
|     func = getattr(hub, f"set_{config[CONF_DIRECTION]}_direction_binary_sensor") | ||||
|     cg.add(func(var)) | ||||
|   | ||||
| @@ -11,13 +11,7 @@ from . import APDS9960, CONF_APDS9960_ID | ||||
|  | ||||
| DEPENDENCIES = ["apds9960"] | ||||
|  | ||||
| TYPES = { | ||||
|     "CLEAR": "set_clear_channel", | ||||
|     "RED": "set_red_channel", | ||||
|     "GREEN": "set_green_channel", | ||||
|     "BLUE": "set_blue_channel", | ||||
|     "PROXIMITY": "set_proximity", | ||||
| } | ||||
| TYPES = ["clear", "red", "green", "blue", "proximity"] | ||||
|  | ||||
| CONFIG_SCHEMA = sensor.sensor_schema( | ||||
|     unit_of_measurement=UNIT_PERCENT, | ||||
| @@ -26,7 +20,7 @@ CONFIG_SCHEMA = sensor.sensor_schema( | ||||
|     state_class=STATE_CLASS_MEASUREMENT, | ||||
| ).extend( | ||||
|     { | ||||
|         cv.Required(CONF_TYPE): cv.one_of(*TYPES, upper=True), | ||||
|         cv.Required(CONF_TYPE): cv.one_of(*TYPES, lower=True), | ||||
|         cv.GenerateID(CONF_APDS9960_ID): cv.use_id(APDS9960), | ||||
|     } | ||||
| ) | ||||
| @@ -35,5 +29,5 @@ CONFIG_SCHEMA = sensor.sensor_schema( | ||||
| async def to_code(config): | ||||
|     hub = await cg.get_variable(config[CONF_APDS9960_ID]) | ||||
|     var = await sensor.new_sensor(config) | ||||
|     func = getattr(hub, TYPES[config[CONF_TYPE]]) | ||||
|     func = getattr(hub, f"set_{config[CONF_TYPE]}_sensor") | ||||
|     cg.add(func(var)) | ||||
|   | ||||
| @@ -288,6 +288,7 @@ message ListEntitiesCoverResponse { | ||||
|   bool disabled_by_default = 9; | ||||
|   string icon = 10; | ||||
|   EntityCategory entity_category = 11; | ||||
|   bool supports_stop = 12; | ||||
| } | ||||
|  | ||||
| enum LegacyCoverState { | ||||
| @@ -861,8 +862,7 @@ message ClimateStateResponse { | ||||
|   float target_temperature = 4; | ||||
|   float target_temperature_low = 5; | ||||
|   float target_temperature_high = 6; | ||||
|   // For older peers, equal to preset == CLIMATE_PRESET_AWAY | ||||
|   bool legacy_away = 7; | ||||
|   bool unused_legacy_away = 7; | ||||
|   ClimateAction action = 8; | ||||
|   ClimateFanMode fan_mode = 9; | ||||
|   ClimateSwingMode swing_mode = 10; | ||||
| @@ -885,9 +885,8 @@ message ClimateCommandRequest { | ||||
|   float target_temperature_low = 7; | ||||
|   bool has_target_temperature_high = 8; | ||||
|   float target_temperature_high = 9; | ||||
|   // legacy, for older peers, newer ones should use CLIMATE_PRESET_AWAY in preset | ||||
|   bool has_legacy_away = 10; | ||||
|   bool legacy_away = 11; | ||||
|   bool unused_has_legacy_away = 10; | ||||
|   bool unused_legacy_away = 11; | ||||
|   bool has_fan_mode = 12; | ||||
|   ClimateFanMode fan_mode = 13; | ||||
|   bool has_swing_mode = 14; | ||||
|   | ||||
| @@ -223,6 +223,7 @@ bool APIConnection::send_cover_info(cover::Cover *cover) { | ||||
|   msg.assumed_state = traits.get_is_assumed_state(); | ||||
|   msg.supports_position = traits.get_supports_position(); | ||||
|   msg.supports_tilt = traits.get_supports_tilt(); | ||||
|   msg.supports_stop = traits.get_supports_stop(); | ||||
|   msg.device_class = cover->get_device_class(); | ||||
|   msg.disabled_by_default = cover->is_disabled_by_default(); | ||||
|   msg.icon = cover->get_icon(); | ||||
| @@ -530,7 +531,6 @@ bool APIConnection::send_climate_state(climate::Climate *climate) { | ||||
|     resp.custom_fan_mode = climate->custom_fan_mode.value(); | ||||
|   if (traits.get_supports_presets() && climate->preset.has_value()) { | ||||
|     resp.preset = static_cast<enums::ClimatePreset>(climate->preset.value()); | ||||
|     resp.legacy_away = resp.preset == enums::CLIMATE_PRESET_AWAY; | ||||
|   } | ||||
|   if (!traits.get_supported_custom_presets().empty() && climate->custom_preset.has_value()) | ||||
|     resp.custom_preset = climate->custom_preset.value(); | ||||
| @@ -591,8 +591,6 @@ void APIConnection::climate_command(const ClimateCommandRequest &msg) { | ||||
|     call.set_target_temperature_low(msg.target_temperature_low); | ||||
|   if (msg.has_target_temperature_high) | ||||
|     call.set_target_temperature_high(msg.target_temperature_high); | ||||
|   if (msg.has_legacy_away) | ||||
|     call.set_preset(msg.legacy_away ? climate::CLIMATE_PRESET_AWAY : climate::CLIMATE_PRESET_HOME); | ||||
|   if (msg.has_fan_mode) | ||||
|     call.set_fan_mode(static_cast<climate::ClimateFanMode>(msg.fan_mode)); | ||||
|   if (msg.has_custom_fan_mode) | ||||
| @@ -944,7 +942,7 @@ HelloResponse APIConnection::hello(const HelloRequest &msg) { | ||||
|  | ||||
|   HelloResponse resp; | ||||
|   resp.api_version_major = 1; | ||||
|   resp.api_version_minor = 7; | ||||
|   resp.api_version_minor = 8; | ||||
|   resp.server_info = App.get_name() + " (esphome v" ESPHOME_VERSION ")"; | ||||
|   resp.name = App.get_name(); | ||||
|  | ||||
| @@ -981,6 +979,8 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) { | ||||
|   resp.manufacturer = "Espressif"; | ||||
| #elif defined(USE_RP2040) | ||||
|   resp.manufacturer = "Raspberry Pi"; | ||||
| #elif defined(USE_HOST) | ||||
|   resp.manufacturer = "Host"; | ||||
| #endif | ||||
|   resp.model = ESPHOME_BOARD; | ||||
| #ifdef USE_DEEP_SLEEP | ||||
| @@ -999,7 +999,7 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) { | ||||
|                                      : bluetooth_proxy::PASSIVE_ONLY_VERSION; | ||||
| #endif | ||||
| #ifdef USE_VOICE_ASSISTANT | ||||
|   resp.voice_assistant_version = 1; | ||||
|   resp.voice_assistant_version = voice_assistant::global_voice_assistant->get_version(); | ||||
| #endif | ||||
|   return resp; | ||||
| } | ||||
|   | ||||
| @@ -941,6 +941,10 @@ bool ListEntitiesCoverResponse::decode_varint(uint32_t field_id, ProtoVarInt val | ||||
|       this->entity_category = value.as_enum<enums::EntityCategory>(); | ||||
|       return true; | ||||
|     } | ||||
|     case 12: { | ||||
|       this->supports_stop = value.as_bool(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| @@ -993,6 +997,7 @@ void ListEntitiesCoverResponse::encode(ProtoWriteBuffer buffer) const { | ||||
|   buffer.encode_bool(9, this->disabled_by_default); | ||||
|   buffer.encode_string(10, this->icon); | ||||
|   buffer.encode_enum<enums::EntityCategory>(11, this->entity_category); | ||||
|   buffer.encode_bool(12, this->supports_stop); | ||||
| } | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void ListEntitiesCoverResponse::dump_to(std::string &out) const { | ||||
| @@ -1042,6 +1047,10 @@ void ListEntitiesCoverResponse::dump_to(std::string &out) const { | ||||
|   out.append("  entity_category: "); | ||||
|   out.append(proto_enum_to_string<enums::EntityCategory>(this->entity_category)); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  supports_stop: "); | ||||
|   out.append(YESNO(this->supports_stop)); | ||||
|   out.append("\n"); | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| @@ -3649,7 +3658,7 @@ bool ClimateStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { | ||||
|       return true; | ||||
|     } | ||||
|     case 7: { | ||||
|       this->legacy_away = value.as_bool(); | ||||
|       this->unused_legacy_away = value.as_bool(); | ||||
|       return true; | ||||
|     } | ||||
|     case 8: { | ||||
| @@ -3719,7 +3728,7 @@ void ClimateStateResponse::encode(ProtoWriteBuffer buffer) const { | ||||
|   buffer.encode_float(4, this->target_temperature); | ||||
|   buffer.encode_float(5, this->target_temperature_low); | ||||
|   buffer.encode_float(6, this->target_temperature_high); | ||||
|   buffer.encode_bool(7, this->legacy_away); | ||||
|   buffer.encode_bool(7, this->unused_legacy_away); | ||||
|   buffer.encode_enum<enums::ClimateAction>(8, this->action); | ||||
|   buffer.encode_enum<enums::ClimateFanMode>(9, this->fan_mode); | ||||
|   buffer.encode_enum<enums::ClimateSwingMode>(10, this->swing_mode); | ||||
| @@ -3760,8 +3769,8 @@ void ClimateStateResponse::dump_to(std::string &out) const { | ||||
|   out.append(buffer); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  legacy_away: "); | ||||
|   out.append(YESNO(this->legacy_away)); | ||||
|   out.append("  unused_legacy_away: "); | ||||
|   out.append(YESNO(this->unused_legacy_away)); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  action: "); | ||||
| @@ -3813,11 +3822,11 @@ bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) | ||||
|       return true; | ||||
|     } | ||||
|     case 10: { | ||||
|       this->has_legacy_away = value.as_bool(); | ||||
|       this->unused_has_legacy_away = value.as_bool(); | ||||
|       return true; | ||||
|     } | ||||
|     case 11: { | ||||
|       this->legacy_away = value.as_bool(); | ||||
|       this->unused_legacy_away = value.as_bool(); | ||||
|       return true; | ||||
|     } | ||||
|     case 12: { | ||||
| @@ -3902,8 +3911,8 @@ void ClimateCommandRequest::encode(ProtoWriteBuffer buffer) const { | ||||
|   buffer.encode_float(7, this->target_temperature_low); | ||||
|   buffer.encode_bool(8, this->has_target_temperature_high); | ||||
|   buffer.encode_float(9, this->target_temperature_high); | ||||
|   buffer.encode_bool(10, this->has_legacy_away); | ||||
|   buffer.encode_bool(11, this->legacy_away); | ||||
|   buffer.encode_bool(10, this->unused_has_legacy_away); | ||||
|   buffer.encode_bool(11, this->unused_legacy_away); | ||||
|   buffer.encode_bool(12, this->has_fan_mode); | ||||
|   buffer.encode_enum<enums::ClimateFanMode>(13, this->fan_mode); | ||||
|   buffer.encode_bool(14, this->has_swing_mode); | ||||
| @@ -3959,12 +3968,12 @@ void ClimateCommandRequest::dump_to(std::string &out) const { | ||||
|   out.append(buffer); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  has_legacy_away: "); | ||||
|   out.append(YESNO(this->has_legacy_away)); | ||||
|   out.append("  unused_has_legacy_away: "); | ||||
|   out.append(YESNO(this->unused_has_legacy_away)); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  legacy_away: "); | ||||
|   out.append(YESNO(this->legacy_away)); | ||||
|   out.append("  unused_legacy_away: "); | ||||
|   out.append(YESNO(this->unused_legacy_away)); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  has_fan_mode: "); | ||||
|   | ||||
| @@ -375,6 +375,7 @@ class ListEntitiesCoverResponse : public ProtoMessage { | ||||
|   bool disabled_by_default{false}; | ||||
|   std::string icon{}; | ||||
|   enums::EntityCategory entity_category{}; | ||||
|   bool supports_stop{false}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   void dump_to(std::string &out) const override; | ||||
| @@ -958,7 +959,7 @@ class ClimateStateResponse : public ProtoMessage { | ||||
|   float target_temperature{0.0f}; | ||||
|   float target_temperature_low{0.0f}; | ||||
|   float target_temperature_high{0.0f}; | ||||
|   bool legacy_away{false}; | ||||
|   bool unused_legacy_away{false}; | ||||
|   enums::ClimateAction action{}; | ||||
|   enums::ClimateFanMode fan_mode{}; | ||||
|   enums::ClimateSwingMode swing_mode{}; | ||||
| @@ -986,8 +987,8 @@ class ClimateCommandRequest : public ProtoMessage { | ||||
|   float target_temperature_low{0.0f}; | ||||
|   bool has_target_temperature_high{false}; | ||||
|   float target_temperature_high{0.0f}; | ||||
|   bool has_legacy_away{false}; | ||||
|   bool legacy_away{false}; | ||||
|   bool unused_has_legacy_away{false}; | ||||
|   bool unused_legacy_away{false}; | ||||
|   bool has_fan_mode{false}; | ||||
|   enums::ClimateFanMode fan_mode{}; | ||||
|   bool has_swing_mode{false}; | ||||
|   | ||||
| @@ -18,5 +18,5 @@ async def to_code(config): | ||||
|         # https://github.com/esphome/AsyncTCP/blob/master/library.json | ||||
|         cg.add_library("esphome/AsyncTCP-esphome", "1.2.2") | ||||
|     elif CORE.is_esp8266: | ||||
|         # https://github.com/OttoWinter/ESPAsyncTCP | ||||
|         cg.add_library("ottowinter/ESPAsyncTCP-esphome", "1.2.3") | ||||
|         # https://github.com/esphome/ESPAsyncTCP | ||||
|         cg.add_library("esphome/ESPAsyncTCP-esphome", "1.2.3") | ||||
|   | ||||
| @@ -43,12 +43,7 @@ void BinarySensor::send_state_internal(bool state, bool is_initial) { | ||||
| } | ||||
|  | ||||
| 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()) | ||||
|     return *this->device_class_; | ||||
|   return ""; | ||||
| } | ||||
|  | ||||
| void BinarySensor::add_filter(Filter *filter) { | ||||
|   filter->parent_ = this; | ||||
|   if (this->filter_list_ == nullptr) { | ||||
|   | ||||
| @@ -34,7 +34,7 @@ namespace binary_sensor { | ||||
|  * The sub classes should notify the front-end of new states via the publish_state() method which | ||||
|  * handles inverted inputs for you. | ||||
|  */ | ||||
| class BinarySensor : public EntityBase { | ||||
| class BinarySensor : public EntityBase, public EntityBase_DeviceClass { | ||||
|  public: | ||||
|   explicit BinarySensor(); | ||||
|  | ||||
| @@ -60,12 +60,6 @@ class BinarySensor : public EntityBase { | ||||
|   /// The current reported state of the binary sensor. | ||||
|   bool state; | ||||
|  | ||||
|   /// Manually set the Home Assistant device class (see binary_sensor::device_class) | ||||
|   void set_device_class(const std::string &device_class); | ||||
|  | ||||
|   /// Get the device class for this binary sensor, using the manual override if specified. | ||||
|   std::string get_device_class(); | ||||
|  | ||||
|   void add_filter(Filter *filter); | ||||
|   void add_filters(const std::vector<Filter *> &filters); | ||||
|  | ||||
| @@ -82,7 +76,6 @@ class BinarySensor : public EntityBase { | ||||
|  | ||||
|  protected: | ||||
|   CallbackManager<void(bool)> state_callback_{}; | ||||
|   optional<std::string> device_class_{};  ///< Stores the override of the device class | ||||
|   Filter *filter_list_{nullptr}; | ||||
|   bool has_state_{false}; | ||||
|   bool publish_initial_state_{false}; | ||||
|   | ||||
| @@ -16,6 +16,9 @@ void BinarySensorMap::loop() { | ||||
|     case BINARY_SENSOR_MAP_TYPE_SUM: | ||||
|       this->process_sum_(); | ||||
|       break; | ||||
|     case BINARY_SENSOR_MAP_TYPE_BAYESIAN: | ||||
|       this->process_bayesian_(); | ||||
|       break; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @@ -23,46 +26,51 @@ void BinarySensorMap::process_group_() { | ||||
|   float total_current_value = 0.0; | ||||
|   uint8_t num_active_sensors = 0; | ||||
|   uint64_t mask = 0x00; | ||||
|   // check all binary_sensors for its state. when active add its value to total_current_value. | ||||
|   // create a bitmask for the binary_sensor status on all channels | ||||
|  | ||||
|   // - check all binary_sensors for its state | ||||
|   //  - if active, add its value to total_current_value. | ||||
|   // - creates a bitmask for the binary_sensor states on all channels | ||||
|   for (size_t i = 0; i < this->channels_.size(); i++) { | ||||
|     auto bs = this->channels_[i]; | ||||
|     if (bs.binary_sensor->state) { | ||||
|       num_active_sensors++; | ||||
|       total_current_value += bs.sensor_value; | ||||
|       total_current_value += bs.parameters.sensor_value; | ||||
|       mask |= 1ULL << i; | ||||
|     } | ||||
|   } | ||||
|   // check if the sensor map was touched | ||||
|  | ||||
|   // potentially update state only if a binary_sensor is active | ||||
|   if (mask != 0ULL) { | ||||
|     // did the bit_mask change or is it a new sensor touch | ||||
|     // publish the average if the bitmask has changed | ||||
|     if (this->last_mask_ != mask) { | ||||
|       float publish_value = total_current_value / num_active_sensors; | ||||
|       this->publish_state(publish_value); | ||||
|     } | ||||
|   } else if (this->last_mask_ != 0ULL) { | ||||
|     // is this a new sensor release | ||||
|     // no buttons are pressed and the states have changed since last run, so publish NAN | ||||
|     ESP_LOGV(TAG, "'%s' - No binary sensor active, publishing NAN", this->name_.c_str()); | ||||
|     this->publish_state(NAN); | ||||
|   } | ||||
|  | ||||
|   this->last_mask_ = mask; | ||||
| } | ||||
|  | ||||
| void BinarySensorMap::process_sum_() { | ||||
|   float total_current_value = 0.0; | ||||
|   uint64_t mask = 0x00; | ||||
|  | ||||
|   // - check all binary_sensor states | ||||
|   // - if active, add its value to total_current_value | ||||
|   // - creates a bitmask for the binary_sensor status on all channels | ||||
|   // - creates a bitmask for the binary_sensor states on all channels | ||||
|   for (size_t i = 0; i < this->channels_.size(); i++) { | ||||
|     auto bs = this->channels_[i]; | ||||
|     if (bs.binary_sensor->state) { | ||||
|       total_current_value += bs.sensor_value; | ||||
|       total_current_value += bs.parameters.sensor_value; | ||||
|       mask |= 1ULL << i; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // update state only if the binary sensor states have changed or if no state has ever been sent on boot | ||||
|   // update state only if any binary_sensor states have changed or if no state has ever been sent on boot | ||||
|   if ((this->last_mask_ != mask) || (!this->has_state())) { | ||||
|     this->publish_state(total_current_value); | ||||
|   } | ||||
| @@ -70,15 +78,65 @@ void BinarySensorMap::process_sum_() { | ||||
|   this->last_mask_ = mask; | ||||
| } | ||||
|  | ||||
| void BinarySensorMap::process_bayesian_() { | ||||
|   float posterior_probability = this->bayesian_prior_; | ||||
|   uint64_t mask = 0x00; | ||||
|  | ||||
|   // - compute the posterior probability by taking the product of the predicate probablities for each observation | ||||
|   // - create a bitmask for the binary_sensor states on all channels/observations | ||||
|   for (size_t i = 0; i < this->channels_.size(); i++) { | ||||
|     auto bs = this->channels_[i]; | ||||
|  | ||||
|     posterior_probability *= | ||||
|         this->bayesian_predicate_(bs.binary_sensor->state, posterior_probability, | ||||
|                                   bs.parameters.probabilities.given_true, bs.parameters.probabilities.given_false); | ||||
|  | ||||
|     mask |= ((uint64_t) (bs.binary_sensor->state)) << i; | ||||
|   } | ||||
|  | ||||
|   // update state only if any binary_sensor states have changed or if no state has ever been sent on boot | ||||
|   if ((this->last_mask_ != mask) || (!this->has_state())) { | ||||
|     this->publish_state(posterior_probability); | ||||
|   } | ||||
|  | ||||
|   this->last_mask_ = mask; | ||||
| } | ||||
|  | ||||
| float BinarySensorMap::bayesian_predicate_(bool sensor_state, float prior, float prob_given_true, | ||||
|                                            float prob_given_false) { | ||||
|   float prob_state_source_true = prob_given_true; | ||||
|   float prob_state_source_false = prob_given_false; | ||||
|  | ||||
|   // if sensor is off, then we use the probabilities for the observation's complement | ||||
|   if (!sensor_state) { | ||||
|     prob_state_source_true = 1 - prob_given_true; | ||||
|     prob_state_source_false = 1 - prob_given_false; | ||||
|   } | ||||
|  | ||||
|   return prob_state_source_true / (prior * prob_state_source_true + (1.0 - prior) * prob_state_source_false); | ||||
| } | ||||
|  | ||||
| void BinarySensorMap::add_channel(binary_sensor::BinarySensor *sensor, float value) { | ||||
|   BinarySensorMapChannel sensor_channel{ | ||||
|       .binary_sensor = sensor, | ||||
|       .sensor_value = value, | ||||
|       .parameters{ | ||||
|           .sensor_value = value, | ||||
|       }, | ||||
|   }; | ||||
|   this->channels_.push_back(sensor_channel); | ||||
| } | ||||
|  | ||||
| void BinarySensorMap::set_sensor_type(BinarySensorMapType sensor_type) { this->sensor_type_ = sensor_type; } | ||||
|  | ||||
| void BinarySensorMap::add_channel(binary_sensor::BinarySensor *sensor, float prob_given_true, float prob_given_false) { | ||||
|   BinarySensorMapChannel sensor_channel{ | ||||
|       .binary_sensor = sensor, | ||||
|       .parameters{ | ||||
|           .probabilities{ | ||||
|               .given_true = prob_given_true, | ||||
|               .given_false = prob_given_false, | ||||
|           }, | ||||
|       }, | ||||
|   }; | ||||
|   this->channels_.push_back(sensor_channel); | ||||
| } | ||||
| }  // namespace binary_sensor_map | ||||
| }  // namespace esphome | ||||
|   | ||||
| @@ -12,51 +12,88 @@ namespace binary_sensor_map { | ||||
| enum BinarySensorMapType { | ||||
|   BINARY_SENSOR_MAP_TYPE_GROUP, | ||||
|   BINARY_SENSOR_MAP_TYPE_SUM, | ||||
|   BINARY_SENSOR_MAP_TYPE_BAYESIAN, | ||||
| }; | ||||
|  | ||||
| struct BinarySensorMapChannel { | ||||
|   binary_sensor::BinarySensor *binary_sensor; | ||||
|   float sensor_value; | ||||
|   union { | ||||
|     float sensor_value; | ||||
|     struct { | ||||
|       float given_true; | ||||
|       float given_false; | ||||
|     } probabilities; | ||||
|   } parameters; | ||||
| }; | ||||
|  | ||||
| /** Class to group binary_sensors to one Sensor. | ||||
| /** Class to map one or more binary_sensors to one Sensor. | ||||
|  * | ||||
|  * Each binary sensor represents a float value in the group. | ||||
|  * Each binary sensor has configured parameters that each mapping type uses to compute the single numerical result | ||||
|  */ | ||||
| class BinarySensorMap : public sensor::Sensor, public Component { | ||||
|  public: | ||||
|   void dump_config() override; | ||||
|  | ||||
|   /** | ||||
|    * The loop checks all binary_sensor states | ||||
|    * When the binary_sensor reports a true value for its state, then the float value it represents is added to the | ||||
|    * total_current_value | ||||
|    * The loop calls the configured type processing method | ||||
|    * | ||||
|    * Only when the total_current_value changed and at least one sensor reports an active state we publish the sensors | ||||
|    * average value. When the value changed and no sensors ar active we publish NAN. | ||||
|    * */ | ||||
|    * The processing method loops through all sensors and calculates the numerical result | ||||
|    * The result is only published if a binary sensor state has changed or, for some types, on initial boot | ||||
|    */ | ||||
|   void loop() override; | ||||
|   float get_setup_priority() const override { return setup_priority::DATA; } | ||||
|   /** Add binary_sensors to the group. | ||||
|    * Each binary_sensor represents a float value when its state is true | ||||
|  | ||||
|   /** | ||||
|    * Add binary_sensors to the group when only one parameter is needed for the configured mapping type. | ||||
|    * | ||||
|    * @param *sensor The binary sensor. | ||||
|    * @param value  The value this binary_sensor represents | ||||
|    */ | ||||
|   void add_channel(binary_sensor::BinarySensor *sensor, float value); | ||||
|   void set_sensor_type(BinarySensorMapType sensor_type); | ||||
|  | ||||
|   /** | ||||
|    * Add binary_sensors to the group when two parameters are needed for the Bayesian mapping type. | ||||
|    * | ||||
|    * @param *sensor The binary sensor. | ||||
|    * @param prob_given_true Probability this observation is on when the Bayesian event is true | ||||
|    * @param prob_given_false Probability this observation is on when the Bayesian event is false | ||||
|    */ | ||||
|   void add_channel(binary_sensor::BinarySensor *sensor, float prob_given_true, float prob_given_false); | ||||
|  | ||||
|   void set_sensor_type(BinarySensorMapType sensor_type) { this->sensor_type_ = sensor_type; } | ||||
|  | ||||
|   void set_bayesian_prior(float prior) { this->bayesian_prior_ = prior; }; | ||||
|  | ||||
|  protected: | ||||
|   std::vector<BinarySensorMapChannel> channels_{}; | ||||
|   BinarySensorMapType sensor_type_{BINARY_SENSOR_MAP_TYPE_GROUP}; | ||||
|   // this gives max 64 channels per binary_sensor_map | ||||
|  | ||||
|   // this allows a max of 64 channels/observations in order to keep track of binary_sensor states | ||||
|   uint64_t last_mask_{0x00}; | ||||
|  | ||||
|   // Bayesian event prior probability before taking into account any observations | ||||
|   float bayesian_prior_{}; | ||||
|  | ||||
|   /** | ||||
|    * methods to process the types of binary_sensor_maps | ||||
|    * GROUP: process_group_() just map to a value | ||||
|    * Methods to process the binary_sensor_maps types | ||||
|    * | ||||
|    * GROUP: process_group_() averages all the values | ||||
|    * ADD: process_add_() adds all the values | ||||
|    * BAYESIAN: process_bayesian_() computes the predicate probability | ||||
|    * */ | ||||
|   void process_group_(); | ||||
|   void process_sum_(); | ||||
|   void process_bayesian_(); | ||||
|  | ||||
|   /** | ||||
|    * Computes the Bayesian predicate for a specific observation | ||||
|    * If the sensor state is false, then we use the parameters' probabilities for the observatiosn complement | ||||
|    * | ||||
|    * @param sensor_state  State of observation | ||||
|    * @param prior Prior probability before accounting for this observation | ||||
|    * @param prob_given_true Probability this observation is on when the Bayesian event is true | ||||
|    * @param prob_given_false Probability this observation is on when the Bayesian event is false | ||||
|    * */ | ||||
|   float bayesian_predicate_(bool sensor_state, float prior, float prob_given_true, float prob_given_false); | ||||
| }; | ||||
|  | ||||
| }  // namespace binary_sensor_map | ||||
|   | ||||
| @@ -20,16 +20,29 @@ BinarySensorMap = binary_sensor_map_ns.class_( | ||||
| ) | ||||
| SensorMapType = binary_sensor_map_ns.enum("SensorMapType") | ||||
|  | ||||
| CONF_BAYESIAN = "bayesian" | ||||
| CONF_PRIOR = "prior" | ||||
| CONF_PROB_GIVEN_TRUE = "prob_given_true" | ||||
| CONF_PROB_GIVEN_FALSE = "prob_given_false" | ||||
| CONF_OBSERVATIONS = "observations" | ||||
|  | ||||
| SENSOR_MAP_TYPES = { | ||||
|     CONF_GROUP: SensorMapType.BINARY_SENSOR_MAP_TYPE_GROUP, | ||||
|     CONF_SUM: SensorMapType.BINARY_SENSOR_MAP_TYPE_SUM, | ||||
|     CONF_BAYESIAN: SensorMapType.BINARY_SENSOR_MAP_TYPE_BAYESIAN, | ||||
| } | ||||
|  | ||||
| entry = { | ||||
| entry_one_parameter = { | ||||
|     cv.Required(CONF_BINARY_SENSOR): cv.use_id(binary_sensor.BinarySensor), | ||||
|     cv.Required(CONF_VALUE): cv.float_, | ||||
| } | ||||
|  | ||||
| entry_bayesian_parameters = { | ||||
|     cv.Required(CONF_BINARY_SENSOR): cv.use_id(binary_sensor.BinarySensor), | ||||
|     cv.Required(CONF_PROB_GIVEN_TRUE): cv.float_range(min=0, max=1), | ||||
|     cv.Required(CONF_PROB_GIVEN_FALSE): cv.float_range(min=0, max=1), | ||||
| } | ||||
|  | ||||
| CONFIG_SCHEMA = cv.typed_schema( | ||||
|     { | ||||
|         CONF_GROUP: sensor.sensor_schema( | ||||
| @@ -39,7 +52,7 @@ CONFIG_SCHEMA = cv.typed_schema( | ||||
|         ).extend( | ||||
|             { | ||||
|                 cv.Required(CONF_CHANNELS): cv.All( | ||||
|                     cv.ensure_list(entry), cv.Length(min=1, max=64) | ||||
|                     cv.ensure_list(entry_one_parameter), cv.Length(min=1, max=64) | ||||
|                 ), | ||||
|             } | ||||
|         ), | ||||
| @@ -50,7 +63,18 @@ CONFIG_SCHEMA = cv.typed_schema( | ||||
|         ).extend( | ||||
|             { | ||||
|                 cv.Required(CONF_CHANNELS): cv.All( | ||||
|                     cv.ensure_list(entry), cv.Length(min=1, max=64) | ||||
|                     cv.ensure_list(entry_one_parameter), cv.Length(min=1, max=64) | ||||
|                 ), | ||||
|             } | ||||
|         ), | ||||
|         CONF_BAYESIAN: sensor.sensor_schema( | ||||
|             BinarySensorMap, | ||||
|             accuracy_decimals=2, | ||||
|         ).extend( | ||||
|             { | ||||
|                 cv.Required(CONF_PRIOR): cv.float_range(min=0, max=1), | ||||
|                 cv.Required(CONF_OBSERVATIONS): cv.All( | ||||
|                     cv.ensure_list(entry_bayesian_parameters), cv.Length(min=1, max=64) | ||||
|                 ), | ||||
|             } | ||||
|         ), | ||||
| @@ -66,6 +90,17 @@ async def to_code(config): | ||||
|     constant = SENSOR_MAP_TYPES[config[CONF_TYPE]] | ||||
|     cg.add(var.set_sensor_type(constant)) | ||||
|  | ||||
|     for ch in config[CONF_CHANNELS]: | ||||
|         input_var = await cg.get_variable(ch[CONF_BINARY_SENSOR]) | ||||
|         cg.add(var.add_channel(input_var, ch[CONF_VALUE])) | ||||
|     if config[CONF_TYPE] == CONF_BAYESIAN: | ||||
|         cg.add(var.set_bayesian_prior(config[CONF_PRIOR])) | ||||
|  | ||||
|         for obs in config[CONF_OBSERVATIONS]: | ||||
|             input_var = await cg.get_variable(obs[CONF_BINARY_SENSOR]) | ||||
|             cg.add( | ||||
|                 var.add_channel( | ||||
|                     input_var, obs[CONF_PROB_GIVEN_TRUE], obs[CONF_PROB_GIVEN_FALSE] | ||||
|                 ) | ||||
|             ) | ||||
|     else: | ||||
|         for ch in config[CONF_CHANNELS]: | ||||
|             input_var = await cg.get_variable(ch[CONF_BINARY_SENSOR]) | ||||
|             cg.add(var.add_channel(input_var, ch[CONF_VALUE])) | ||||
|   | ||||
| @@ -29,8 +29,35 @@ BLEClientConnectTrigger = ble_client_ns.class_( | ||||
| BLEClientDisconnectTrigger = ble_client_ns.class_( | ||||
|     "BLEClientDisconnectTrigger", automation.Trigger.template(BLEClientNodeConstRef) | ||||
| ) | ||||
| BLEClientPasskeyRequestTrigger = ble_client_ns.class_( | ||||
|     "BLEClientPasskeyRequestTrigger", automation.Trigger.template(BLEClientNodeConstRef) | ||||
| ) | ||||
| BLEClientPasskeyNotificationTrigger = ble_client_ns.class_( | ||||
|     "BLEClientPasskeyNotificationTrigger", | ||||
|     automation.Trigger.template(BLEClientNodeConstRef, cg.uint32), | ||||
| ) | ||||
| BLEClientNumericComparisonRequestTrigger = ble_client_ns.class_( | ||||
|     "BLEClientNumericComparisonRequestTrigger", | ||||
|     automation.Trigger.template(BLEClientNodeConstRef, cg.uint32), | ||||
| ) | ||||
|  | ||||
| # Actions | ||||
| BLEWriteAction = ble_client_ns.class_("BLEClientWriteAction", automation.Action) | ||||
| BLEPasskeyReplyAction = ble_client_ns.class_( | ||||
|     "BLEClientPasskeyReplyAction", automation.Action | ||||
| ) | ||||
| BLENumericComparisonReplyAction = ble_client_ns.class_( | ||||
|     "BLEClientNumericComparisonReplyAction", automation.Action | ||||
| ) | ||||
| BLERemoveBondAction = ble_client_ns.class_( | ||||
|     "BLEClientRemoveBondAction", automation.Action | ||||
| ) | ||||
|  | ||||
| CONF_PASSKEY = "passkey" | ||||
| CONF_ACCEPT = "accept" | ||||
| CONF_ON_PASSKEY_REQUEST = "on_passkey_request" | ||||
| CONF_ON_PASSKEY_NOTIFICATION = "on_passkey_notification" | ||||
| CONF_ON_NUMERIC_COMPARISON_REQUEST = "on_numeric_comparison_request" | ||||
|  | ||||
| # Espressif platformio framework is built with MAX_BLE_CONN to 3, so | ||||
| # enforce this in yaml checks. | ||||
| @@ -56,6 +83,29 @@ CONFIG_SCHEMA = ( | ||||
|                     ), | ||||
|                 } | ||||
|             ), | ||||
|             cv.Optional(CONF_ON_PASSKEY_REQUEST): automation.validate_automation( | ||||
|                 { | ||||
|                     cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( | ||||
|                         BLEClientPasskeyRequestTrigger | ||||
|                     ), | ||||
|                 } | ||||
|             ), | ||||
|             cv.Optional(CONF_ON_PASSKEY_NOTIFICATION): automation.validate_automation( | ||||
|                 { | ||||
|                     cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( | ||||
|                         BLEClientPasskeyNotificationTrigger | ||||
|                     ), | ||||
|                 } | ||||
|             ), | ||||
|             cv.Optional( | ||||
|                 CONF_ON_NUMERIC_COMPARISON_REQUEST | ||||
|             ): automation.validate_automation( | ||||
|                 { | ||||
|                     cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( | ||||
|                         BLEClientNumericComparisonRequestTrigger | ||||
|                     ), | ||||
|                 } | ||||
|             ), | ||||
|         } | ||||
|     ) | ||||
|     .extend(cv.COMPONENT_SCHEMA) | ||||
| @@ -85,13 +135,34 @@ BLE_WRITE_ACTION_SCHEMA = cv.Schema( | ||||
|     } | ||||
| ) | ||||
|  | ||||
| BLE_NUMERIC_COMPARISON_REPLY_ACTION_SCHEMA = cv.Schema( | ||||
|     { | ||||
|         cv.GenerateID(CONF_ID): cv.use_id(BLEClient), | ||||
|         cv.Required(CONF_ACCEPT): cv.templatable(cv.boolean), | ||||
|     } | ||||
| ) | ||||
|  | ||||
| BLE_PASSKEY_REPLY_ACTION_SCHEMA = cv.Schema( | ||||
|     { | ||||
|         cv.GenerateID(CONF_ID): cv.use_id(BLEClient), | ||||
|         cv.Required(CONF_PASSKEY): cv.templatable(cv.int_range(min=0, max=999999)), | ||||
|     } | ||||
| ) | ||||
|  | ||||
|  | ||||
| BLE_REMOVE_BOND_ACTION_SCHEMA = cv.Schema( | ||||
|     { | ||||
|         cv.GenerateID(CONF_ID): cv.use_id(BLEClient), | ||||
|     } | ||||
| ) | ||||
|  | ||||
|  | ||||
| @automation.register_action( | ||||
|     "ble_client.ble_write", BLEWriteAction, BLE_WRITE_ACTION_SCHEMA | ||||
| ) | ||||
| async def ble_write_to_code(config, action_id, template_arg, args): | ||||
|     paren = await cg.get_variable(config[CONF_ID]) | ||||
|     var = cg.new_Pvariable(action_id, template_arg, paren) | ||||
|     parent = await cg.get_variable(config[CONF_ID]) | ||||
|     var = cg.new_Pvariable(action_id, template_arg, parent) | ||||
|  | ||||
|     value = config[CONF_VALUE] | ||||
|     if cg.is_template(value): | ||||
| @@ -137,6 +208,54 @@ async def ble_write_to_code(config, action_id, template_arg, args): | ||||
|     return var | ||||
|  | ||||
|  | ||||
| @automation.register_action( | ||||
|     "ble_client.numeric_comparison_reply", | ||||
|     BLENumericComparisonReplyAction, | ||||
|     BLE_NUMERIC_COMPARISON_REPLY_ACTION_SCHEMA, | ||||
| ) | ||||
| async def numeric_comparison_reply_to_code(config, action_id, template_arg, args): | ||||
|     parent = await cg.get_variable(config[CONF_ID]) | ||||
|     var = cg.new_Pvariable(action_id, template_arg, parent) | ||||
|  | ||||
|     accept = config[CONF_ACCEPT] | ||||
|     if cg.is_template(accept): | ||||
|         templ = await cg.templatable(accept, args, cg.bool_) | ||||
|         cg.add(var.set_value_template(templ)) | ||||
|     else: | ||||
|         cg.add(var.set_value_simple(accept)) | ||||
|  | ||||
|     return var | ||||
|  | ||||
|  | ||||
| @automation.register_action( | ||||
|     "ble_client.passkey_reply", BLEPasskeyReplyAction, BLE_PASSKEY_REPLY_ACTION_SCHEMA | ||||
| ) | ||||
| async def passkey_reply_to_code(config, action_id, template_arg, args): | ||||
|     parent = await cg.get_variable(config[CONF_ID]) | ||||
|     var = cg.new_Pvariable(action_id, template_arg, parent) | ||||
|  | ||||
|     passkey = config[CONF_PASSKEY] | ||||
|     if cg.is_template(passkey): | ||||
|         templ = await cg.templatable(passkey, args, cg.uint32) | ||||
|         cg.add(var.set_value_template(templ)) | ||||
|     else: | ||||
|         cg.add(var.set_value_simple(passkey)) | ||||
|  | ||||
|     return var | ||||
|  | ||||
|  | ||||
| @automation.register_action( | ||||
|     "ble_client.remove_bond", | ||||
|     BLERemoveBondAction, | ||||
|     BLE_REMOVE_BOND_ACTION_SCHEMA, | ||||
| ) | ||||
| async def remove_bond_to_code(config, action_id, template_arg, args): | ||||
|     parent = await cg.get_variable(config[CONF_ID]) | ||||
|     var = cg.new_Pvariable(action_id, template_arg, parent) | ||||
|  | ||||
|     return var | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     await cg.register_component(var, config) | ||||
| @@ -148,3 +267,12 @@ async def to_code(config): | ||||
|     for conf in config.get(CONF_ON_DISCONNECT, []): | ||||
|         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) | ||||
|         await automation.build_automation(trigger, [], conf) | ||||
|     for conf in config.get(CONF_ON_PASSKEY_REQUEST, []): | ||||
|         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) | ||||
|         await automation.build_automation(trigger, [], conf) | ||||
|     for conf in config.get(CONF_ON_PASSKEY_NOTIFICATION, []): | ||||
|         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) | ||||
|         await automation.build_automation(trigger, [(cg.uint32, "passkey")], conf) | ||||
|     for conf in config.get(CONF_ON_NUMERIC_COMPARISON_REQUEST, []): | ||||
|         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) | ||||
|         await automation.build_automation(trigger, [(cg.uint32, "passkey")], conf) | ||||
|   | ||||
| @@ -37,6 +37,44 @@ class BLEClientDisconnectTrigger : public Trigger<>, public BLEClientNode { | ||||
|   } | ||||
| }; | ||||
|  | ||||
| class BLEClientPasskeyRequestTrigger : public Trigger<>, public BLEClientNode { | ||||
|  public: | ||||
|   explicit BLEClientPasskeyRequestTrigger(BLEClient *parent) { parent->register_ble_node(this); } | ||||
|   void loop() override {} | ||||
|   void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override { | ||||
|     if (event == ESP_GAP_BLE_PASSKEY_REQ_EVT && | ||||
|         memcmp(param->ble_security.auth_cmpl.bd_addr, this->parent_->get_remote_bda(), 6) == 0) { | ||||
|       this->trigger(); | ||||
|     } | ||||
|   } | ||||
| }; | ||||
|  | ||||
| class BLEClientPasskeyNotificationTrigger : public Trigger<uint32_t>, public BLEClientNode { | ||||
|  public: | ||||
|   explicit BLEClientPasskeyNotificationTrigger(BLEClient *parent) { parent->register_ble_node(this); } | ||||
|   void loop() override {} | ||||
|   void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override { | ||||
|     if (event == ESP_GAP_BLE_PASSKEY_NOTIF_EVT && | ||||
|         memcmp(param->ble_security.auth_cmpl.bd_addr, this->parent_->get_remote_bda(), 6) == 0) { | ||||
|       uint32_t passkey = param->ble_security.key_notif.passkey; | ||||
|       this->trigger(passkey); | ||||
|     } | ||||
|   } | ||||
| }; | ||||
|  | ||||
| class BLEClientNumericComparisonRequestTrigger : public Trigger<uint32_t>, public BLEClientNode { | ||||
|  public: | ||||
|   explicit BLEClientNumericComparisonRequestTrigger(BLEClient *parent) { parent->register_ble_node(this); } | ||||
|   void loop() override {} | ||||
|   void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override { | ||||
|     if (event == ESP_GAP_BLE_NC_REQ_EVT && | ||||
|         memcmp(param->ble_security.auth_cmpl.bd_addr, this->parent_->get_remote_bda(), 6) == 0) { | ||||
|       uint32_t passkey = param->ble_security.key_notif.passkey; | ||||
|       this->trigger(passkey); | ||||
|     } | ||||
|   } | ||||
| }; | ||||
|  | ||||
| class BLEWriterClientNode : public BLEClientNode { | ||||
|  public: | ||||
|   BLEWriterClientNode(BLEClient *ble_client) { | ||||
| @@ -94,6 +132,86 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ | ||||
|   std::function<std::vector<uint8_t>(Ts...)> value_template_{}; | ||||
| }; | ||||
|  | ||||
| template<typename... Ts> class BLEClientPasskeyReplyAction : public Action<Ts...> { | ||||
|  public: | ||||
|   BLEClientPasskeyReplyAction(BLEClient *ble_client) { parent_ = ble_client; } | ||||
|  | ||||
|   void play(Ts... x) override { | ||||
|     uint32_t passkey; | ||||
|     if (has_simple_value_) { | ||||
|       passkey = this->value_simple_; | ||||
|     } else { | ||||
|       passkey = this->value_template_(x...); | ||||
|     } | ||||
|     if (passkey > 999999) | ||||
|       return; | ||||
|     esp_bd_addr_t remote_bda; | ||||
|     memcpy(remote_bda, parent_->get_remote_bda(), sizeof(esp_bd_addr_t)); | ||||
|     esp_ble_passkey_reply(remote_bda, true, passkey); | ||||
|   } | ||||
|  | ||||
|   void set_value_template(std::function<uint32_t(Ts...)> func) { | ||||
|     this->value_template_ = std::move(func); | ||||
|     has_simple_value_ = false; | ||||
|   } | ||||
|  | ||||
|   void set_value_simple(const uint32_t &value) { | ||||
|     this->value_simple_ = value; | ||||
|     has_simple_value_ = true; | ||||
|   } | ||||
|  | ||||
|  private: | ||||
|   BLEClient *parent_{nullptr}; | ||||
|   bool has_simple_value_ = true; | ||||
|   uint32_t value_simple_{0}; | ||||
|   std::function<uint32_t(Ts...)> value_template_{}; | ||||
| }; | ||||
|  | ||||
| template<typename... Ts> class BLEClientNumericComparisonReplyAction : public Action<Ts...> { | ||||
|  public: | ||||
|   BLEClientNumericComparisonReplyAction(BLEClient *ble_client) { parent_ = ble_client; } | ||||
|  | ||||
|   void play(Ts... x) override { | ||||
|     esp_bd_addr_t remote_bda; | ||||
|     memcpy(remote_bda, parent_->get_remote_bda(), sizeof(esp_bd_addr_t)); | ||||
|     if (has_simple_value_) { | ||||
|       esp_ble_confirm_reply(remote_bda, this->value_simple_); | ||||
|     } else { | ||||
|       esp_ble_confirm_reply(remote_bda, this->value_template_(x...)); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void set_value_template(std::function<bool(Ts...)> func) { | ||||
|     this->value_template_ = std::move(func); | ||||
|     has_simple_value_ = false; | ||||
|   } | ||||
|  | ||||
|   void set_value_simple(const bool &value) { | ||||
|     this->value_simple_ = value; | ||||
|     has_simple_value_ = true; | ||||
|   } | ||||
|  | ||||
|  private: | ||||
|   BLEClient *parent_{nullptr}; | ||||
|   bool has_simple_value_ = true; | ||||
|   bool value_simple_{false}; | ||||
|   std::function<bool(Ts...)> value_template_{}; | ||||
| }; | ||||
|  | ||||
| template<typename... Ts> class BLEClientRemoveBondAction : public Action<Ts...> { | ||||
|  public: | ||||
|   BLEClientRemoveBondAction(BLEClient *ble_client) { parent_ = ble_client; } | ||||
|  | ||||
|   void play(Ts... x) override { | ||||
|     esp_bd_addr_t remote_bda; | ||||
|     memcpy(remote_bda, parent_->get_remote_bda(), sizeof(esp_bd_addr_t)); | ||||
|     esp_ble_remove_bond_device(remote_bda); | ||||
|   } | ||||
|  | ||||
|  private: | ||||
|   BLEClient *parent_{nullptr}; | ||||
| }; | ||||
|  | ||||
| }  // namespace ble_client | ||||
| }  // namespace esphome | ||||
|  | ||||
|   | ||||
| @@ -27,7 +27,7 @@ class BLEClient; | ||||
| class BLEClientNode { | ||||
|  public: | ||||
|   virtual void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, | ||||
|                                    esp_ble_gattc_cb_param_t *param) = 0; | ||||
|                                    esp_ble_gattc_cb_param_t *param){}; | ||||
|   virtual void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {} | ||||
|   virtual void loop() {} | ||||
|   void set_address(uint64_t address) { address_ = address; } | ||||
|   | ||||
| @@ -13,8 +13,5 @@ void Button::press() { | ||||
| } | ||||
| void Button::add_on_press_callback(std::function<void()> &&callback) { this->press_callback_.add(std::move(callback)); } | ||||
|  | ||||
| void Button::set_device_class(const std::string &device_class) { this->device_class_ = device_class; } | ||||
| std::string Button::get_device_class() { return this->device_class_; } | ||||
|  | ||||
| }  // namespace button | ||||
| }  // namespace esphome | ||||
|   | ||||
| @@ -26,7 +26,7 @@ namespace button { | ||||
|  * | ||||
|  * A button is just a momentary switch that does not have a state, only a trigger. | ||||
|  */ | ||||
| class Button : public EntityBase { | ||||
| class Button : public EntityBase, public EntityBase_DeviceClass { | ||||
|  public: | ||||
|   /** Press this button. This is called by the front-end. | ||||
|    * | ||||
| @@ -40,19 +40,12 @@ class Button : public EntityBase { | ||||
|    */ | ||||
|   void add_on_press_callback(std::function<void()> &&callback); | ||||
|  | ||||
|   /// Set the Home Assistant device class (see button::device_class). | ||||
|   void set_device_class(const std::string &device_class); | ||||
|  | ||||
|   /// Get the device class for this button. | ||||
|   std::string get_device_class(); | ||||
|  | ||||
|  protected: | ||||
|   /** You should implement this virtual method if you want to create your own button. | ||||
|    */ | ||||
|   virtual void press_action() = 0; | ||||
|  | ||||
|   CallbackManager<void()> press_callback_{}; | ||||
|   std::string device_class_{}; | ||||
| }; | ||||
|  | ||||
| }  // namespace button | ||||
|   | ||||
| @@ -343,7 +343,7 @@ CLIMATE_CONTROL_ACTION_SCHEMA = cv.Schema( | ||||
|         cv.Optional(CONF_TARGET_TEMPERATURE): cv.templatable(cv.temperature), | ||||
|         cv.Optional(CONF_TARGET_TEMPERATURE_LOW): cv.templatable(cv.temperature), | ||||
|         cv.Optional(CONF_TARGET_TEMPERATURE_HIGH): cv.templatable(cv.temperature), | ||||
|         cv.Optional(CONF_AWAY): cv.templatable(cv.boolean), | ||||
|         cv.Optional(CONF_AWAY): cv.invalid("Use preset instead"), | ||||
|         cv.Exclusive(CONF_FAN_MODE, "fan_mode"): cv.templatable( | ||||
|             validate_climate_fan_mode | ||||
|         ), | ||||
| @@ -379,9 +379,6 @@ async def climate_control_to_code(config, action_id, template_arg, args): | ||||
|             config[CONF_TARGET_TEMPERATURE_HIGH], args, float | ||||
|         ) | ||||
|         cg.add(var.set_target_temperature_high(template_)) | ||||
|     if CONF_AWAY in config: | ||||
|         template_ = await cg.templatable(config[CONF_AWAY], args, bool) | ||||
|         cg.add(var.set_away(template_)) | ||||
|     if CONF_FAN_MODE in config: | ||||
|         template_ = await cg.templatable(config[CONF_FAN_MODE], args, ClimateFanMode) | ||||
|         cg.add(var.set_fan_mode(template_)) | ||||
|   | ||||
| @@ -264,25 +264,11 @@ const optional<ClimateMode> &ClimateCall::get_mode() const { return this->mode_; | ||||
| const optional<float> &ClimateCall::get_target_temperature() const { return this->target_temperature_; } | ||||
| const optional<float> &ClimateCall::get_target_temperature_low() const { return this->target_temperature_low_; } | ||||
| const optional<float> &ClimateCall::get_target_temperature_high() const { return this->target_temperature_high_; } | ||||
| optional<bool> ClimateCall::get_away() const { | ||||
|   if (!this->preset_.has_value()) | ||||
|     return {}; | ||||
|   return *this->preset_ == ClimatePreset::CLIMATE_PRESET_AWAY; | ||||
| } | ||||
| const optional<ClimateFanMode> &ClimateCall::get_fan_mode() const { return this->fan_mode_; } | ||||
| const optional<std::string> &ClimateCall::get_custom_fan_mode() const { return this->custom_fan_mode_; } | ||||
| const optional<ClimatePreset> &ClimateCall::get_preset() const { return this->preset_; } | ||||
| const optional<std::string> &ClimateCall::get_custom_preset() const { return this->custom_preset_; } | ||||
| const optional<ClimateSwingMode> &ClimateCall::get_swing_mode() const { return this->swing_mode_; } | ||||
| ClimateCall &ClimateCall::set_away(bool away) { | ||||
|   this->preset_ = away ? CLIMATE_PRESET_AWAY : CLIMATE_PRESET_HOME; | ||||
|   return *this; | ||||
| } | ||||
| ClimateCall &ClimateCall::set_away(optional<bool> away) { | ||||
|   if (away.has_value()) | ||||
|     this->preset_ = *away ? CLIMATE_PRESET_AWAY : CLIMATE_PRESET_HOME; | ||||
|   return *this; | ||||
| } | ||||
| ClimateCall &ClimateCall::set_target_temperature_high(optional<float> target_temperature_high) { | ||||
|   this->target_temperature_high_ = target_temperature_high; | ||||
|   return *this; | ||||
|   | ||||
| @@ -64,10 +64,6 @@ class ClimateCall { | ||||
|    * For climate devices with two point target temperature control | ||||
|    */ | ||||
|   ClimateCall &set_target_temperature_high(optional<float> target_temperature_high); | ||||
|   ESPDEPRECATED("set_away() is deprecated, please use .set_preset(CLIMATE_PRESET_AWAY) instead", "v1.20") | ||||
|   ClimateCall &set_away(bool away); | ||||
|   ESPDEPRECATED("set_away() is deprecated, please use .set_preset(CLIMATE_PRESET_AWAY) instead", "v1.20") | ||||
|   ClimateCall &set_away(optional<bool> away); | ||||
|   /// Set the fan mode of the climate device. | ||||
|   ClimateCall &set_fan_mode(ClimateFanMode fan_mode); | ||||
|   /// Set the fan mode of the climate device. | ||||
| @@ -97,8 +93,6 @@ class ClimateCall { | ||||
|   const optional<float> &get_target_temperature() const; | ||||
|   const optional<float> &get_target_temperature_low() const; | ||||
|   const optional<float> &get_target_temperature_high() const; | ||||
|   ESPDEPRECATED("get_away() is deprecated, please use .get_preset() instead", "v1.20") | ||||
|   optional<bool> get_away() const; | ||||
|   const optional<ClimateFanMode> &get_fan_mode() const; | ||||
|   const optional<ClimateSwingMode> &get_swing_mode() const; | ||||
|   const optional<std::string> &get_custom_fan_mode() const; | ||||
| @@ -184,14 +178,6 @@ class Climate : public EntityBase { | ||||
|     }; | ||||
|   }; | ||||
|  | ||||
|   /** Whether the climate device is in away mode. | ||||
|    * | ||||
|    * Away allows climate devices to have two different target temperature configs: | ||||
|    * one for normal mode and one for away mode. | ||||
|    */ | ||||
|   ESPDEPRECATED("away is deprecated, use preset instead", "v1.20") | ||||
|   bool away{false}; | ||||
|  | ||||
|   /// The active fan mode of the climate device. | ||||
|   optional<ClimateFanMode> fan_mode; | ||||
|  | ||||
|   | ||||
| @@ -117,15 +117,6 @@ class ClimateTraits { | ||||
|   bool supports_custom_preset(const std::string &custom_preset) const { | ||||
|     return supported_custom_presets_.count(custom_preset); | ||||
|   } | ||||
|   ESPDEPRECATED("This method is deprecated, use set_supported_presets() instead", "v1.20") | ||||
|   void set_supports_away(bool supports) { | ||||
|     if (supports) { | ||||
|       supported_presets_.insert(CLIMATE_PRESET_AWAY); | ||||
|       supported_presets_.insert(CLIMATE_PRESET_HOME); | ||||
|     } | ||||
|   } | ||||
|   ESPDEPRECATED("This method is deprecated, use supports_preset() instead", "v1.20") | ||||
|   bool get_supports_away() const { return supports_preset(CLIMATE_PRESET_AWAY); } | ||||
|  | ||||
|   void set_supported_swing_modes(std::set<ClimateSwingMode> modes) { supported_swing_modes_ = std::move(modes); } | ||||
|   void add_supported_swing_mode(ClimateSwingMode mode) { supported_swing_modes_.insert(mode); } | ||||
|   | ||||
| @@ -28,6 +28,7 @@ cover::CoverTraits CopyCover::get_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_stop(base.get_supports_stop()); | ||||
|   traits.set_supports_position(base.get_supports_position()); | ||||
|   traits.set_supports_tilt(base.get_supports_tilt()); | ||||
|   traits.set_supports_toggle(base.get_supports_toggle()); | ||||
|   | ||||
| @@ -145,7 +145,7 @@ CoverCall &CoverCall::set_stop(bool stop) { | ||||
|   return *this; | ||||
| } | ||||
| bool CoverCall::get_stop() const { return this->stop_; } | ||||
| void Cover::set_device_class(const std::string &device_class) { this->device_class_override_ = device_class; } | ||||
|  | ||||
| CoverCall Cover::make_call() { return {this}; } | ||||
| void Cover::open() { | ||||
|   auto call = this->make_call(); | ||||
| @@ -204,11 +204,7 @@ optional<CoverRestoreState> Cover::restore_state_() { | ||||
|     return {}; | ||||
|   return recovered; | ||||
| } | ||||
| std::string Cover::get_device_class() { | ||||
|   if (this->device_class_override_.has_value()) | ||||
|     return *this->device_class_override_; | ||||
|   return ""; | ||||
| } | ||||
|  | ||||
| bool Cover::is_fully_open() const { return this->position == COVER_OPEN; } | ||||
| bool Cover::is_fully_closed() const { return this->position == COVER_CLOSED; } | ||||
|  | ||||
|   | ||||
| @@ -108,7 +108,7 @@ const char *cover_operation_to_str(CoverOperation op); | ||||
|  * to control all values of the cover. Also implement get_traits() to return what operations | ||||
|  * the cover supports. | ||||
|  */ | ||||
| class Cover : public EntityBase { | ||||
| class Cover : public EntityBase, public EntityBase_DeviceClass { | ||||
|  public: | ||||
|   explicit Cover(); | ||||
|  | ||||
| @@ -156,8 +156,6 @@ class Cover : public EntityBase { | ||||
|   void publish_state(bool save = true); | ||||
|  | ||||
|   virtual CoverTraits get_traits() = 0; | ||||
|   void set_device_class(const std::string &device_class); | ||||
|   std::string get_device_class(); | ||||
|  | ||||
|   /// Helper method to check if the cover is fully open. Equivalent to comparing .position against 1.0 | ||||
|   bool is_fully_open() const; | ||||
| @@ -172,7 +170,6 @@ class Cover : public EntityBase { | ||||
|   optional<CoverRestoreState> restore_state_(); | ||||
|  | ||||
|   CallbackManager<void()> state_callback_{}; | ||||
|   optional<std::string> device_class_override_{}; | ||||
|  | ||||
|   ESPPreferenceObject rtc_; | ||||
| }; | ||||
|   | ||||
| @@ -15,12 +15,15 @@ class CoverTraits { | ||||
|   void set_supports_tilt(bool supports_tilt) { this->supports_tilt_ = supports_tilt; } | ||||
|   bool get_supports_toggle() const { return this->supports_toggle_; } | ||||
|   void set_supports_toggle(bool supports_toggle) { this->supports_toggle_ = supports_toggle; } | ||||
|   bool get_supports_stop() const { return this->supports_stop_; } | ||||
|   void set_supports_stop(bool supports_stop) { this->supports_stop_ = supports_stop; } | ||||
|  | ||||
|  protected: | ||||
|   bool is_assumed_state_{false}; | ||||
|   bool supports_position_{false}; | ||||
|   bool supports_tilt_{false}; | ||||
|   bool supports_toggle_{false}; | ||||
|   bool supports_stop_{false}; | ||||
| }; | ||||
|  | ||||
| }  // namespace cover | ||||
|   | ||||
| @@ -12,6 +12,7 @@ using namespace esphome::cover; | ||||
|  | ||||
| CoverTraits CurrentBasedCover::get_traits() { | ||||
|   auto traits = CoverTraits(); | ||||
|   traits.set_supports_stop(true); | ||||
|   traits.set_supports_position(true); | ||||
|   traits.set_supports_toggle(true); | ||||
|   traits.set_is_assumed_state(false); | ||||
|   | ||||
| @@ -7,9 +7,10 @@ import requests | ||||
|  | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| import esphome.final_validate as fv | ||||
| from esphome import git | ||||
| from esphome.components.packages import validate_source_shorthand | ||||
| from esphome.const import CONF_REF, CONF_WIFI | ||||
| from esphome.const import CONF_REF, CONF_WIFI, CONF_ESPHOME, CONF_PROJECT | ||||
| from esphome.wizard import wizard_file | ||||
| from esphome.yaml_util import dump | ||||
|  | ||||
| @@ -52,6 +53,17 @@ CONFIG_SCHEMA = cv.All( | ||||
|     validate_full_url, | ||||
| ) | ||||
|  | ||||
|  | ||||
| def _final_validate(config): | ||||
|     full_config = fv.full_config.get()[CONF_ESPHOME] | ||||
|     if CONF_PROJECT not in full_config: | ||||
|         raise cv.Invalid( | ||||
|             "Dashboard import requires the `esphome` -> `project` information to be provided." | ||||
|         ) | ||||
|  | ||||
|  | ||||
| FINAL_VALIDATE_SCHEMA = _final_validate | ||||
|  | ||||
| WIFI_CONFIG = """ | ||||
|  | ||||
| wifi: | ||||
|   | ||||
| @@ -72,6 +72,7 @@ class DemoCover : public cover::Cover, public Component { | ||||
|         traits.set_supports_tilt(true); | ||||
|         break; | ||||
|       case DemoCoverType::TYPE_4: | ||||
|         traits.set_supports_stop(true); | ||||
|         traits.set_is_assumed_state(true); | ||||
|         traits.set_supports_tilt(true); | ||||
|         break; | ||||
|   | ||||
| @@ -40,6 +40,7 @@ DEVICE = { | ||||
|  | ||||
| NextAction = dfplayer_ns.class_("NextAction", automation.Action) | ||||
| PreviousAction = dfplayer_ns.class_("PreviousAction", automation.Action) | ||||
| PlayMp3Action = dfplayer_ns.class_("PlayMp3Action", automation.Action) | ||||
| PlayFileAction = dfplayer_ns.class_("PlayFileAction", automation.Action) | ||||
| PlayFolderAction = dfplayer_ns.class_("PlayFolderAction", automation.Action) | ||||
| SetVolumeAction = dfplayer_ns.class_("SetVolumeAction", automation.Action) | ||||
| @@ -113,6 +114,25 @@ async def dfplayer_previous_to_code(config, action_id, template_arg, args): | ||||
|     return var | ||||
|  | ||||
|  | ||||
| @automation.register_action( | ||||
|     "dfplayer.play_mp3", | ||||
|     PlayMp3Action, | ||||
|     cv.maybe_simple_value( | ||||
|         { | ||||
|             cv.GenerateID(): cv.use_id(DFPlayer), | ||||
|             cv.Required(CONF_FILE): cv.templatable(cv.int_), | ||||
|         }, | ||||
|         key=CONF_FILE, | ||||
|     ), | ||||
| ) | ||||
| async def dfplayer_play_mp3_to_code(config, action_id, template_arg, args): | ||||
|     var = cg.new_Pvariable(action_id, template_arg) | ||||
|     await cg.register_parented(var, config[CONF_ID]) | ||||
|     template_ = await cg.templatable(config[CONF_FILE], args, float) | ||||
|     cg.add(var.set_file(template_)) | ||||
|     return var | ||||
|  | ||||
|  | ||||
| @automation.register_action( | ||||
|     "dfplayer.play", | ||||
|     PlayFileAction, | ||||
|   | ||||
| @@ -7,10 +7,10 @@ namespace dfplayer { | ||||
| static const char *const TAG = "dfplayer"; | ||||
|  | ||||
| void DFPlayer::play_folder(uint16_t folder, uint16_t file) { | ||||
|   if (folder < 100 && file < 256) { | ||||
|   if (folder <= 10 && file <= 1000) { | ||||
|     this->ack_set_is_playing_ = true; | ||||
|     this->send_cmd_(0x0F, (uint8_t) folder, (uint8_t) file); | ||||
|   } else if (folder <= 10 && file <= 1000) { | ||||
|   } else if (folder < 100 && file < 256) { | ||||
|     this->ack_set_is_playing_ = true; | ||||
|     this->send_cmd_(0x14, (((uint16_t) folder) << 12) | file); | ||||
|   } else { | ||||
|   | ||||
| @@ -35,6 +35,10 @@ class DFPlayer : public uart::UARTDevice, public Component { | ||||
|     this->ack_set_is_playing_ = true; | ||||
|     this->send_cmd_(0x02); | ||||
|   } | ||||
|   void play_mp3(uint16_t file) { | ||||
|     this->ack_set_is_playing_ = true; | ||||
|     this->send_cmd_(0x12, file); | ||||
|   } | ||||
|   void play_file(uint16_t file) { | ||||
|     this->ack_set_is_playing_ = true; | ||||
|     this->send_cmd_(0x03, file); | ||||
| @@ -113,6 +117,16 @@ class DFPlayer : public uart::UARTDevice, public Component { | ||||
| DFPLAYER_SIMPLE_ACTION(NextAction, next) | ||||
| DFPLAYER_SIMPLE_ACTION(PreviousAction, previous) | ||||
|  | ||||
| template<typename... Ts> class PlayMp3Action : public Action<Ts...>, public Parented<DFPlayer> { | ||||
|  public: | ||||
|   TEMPLATABLE_VALUE(uint16_t, file) | ||||
|  | ||||
|   void play(Ts... x) override { | ||||
|     auto file = this->file_.value(x...); | ||||
|     this->parent_->play_mp3(file); | ||||
|   } | ||||
| }; | ||||
|  | ||||
| template<typename... Ts> class PlayFileAction : public Action<Ts...>, public Parented<DFPlayer> { | ||||
|  public: | ||||
|   TEMPLATABLE_VALUE(uint16_t, file) | ||||
|   | ||||
| @@ -282,10 +282,14 @@ void DisplayBuffer::print(int x, int y, Font *font, Color color, TextAlign align | ||||
|     int scan_x1, scan_y1, scan_width, scan_height; | ||||
|     glyph.scan_area(&scan_x1, &scan_y1, &scan_width, &scan_height); | ||||
|  | ||||
|     for (int glyph_x = scan_x1; glyph_x < scan_x1 + scan_width; glyph_x++) { | ||||
|       for (int glyph_y = scan_y1; glyph_y < scan_y1 + scan_height; glyph_y++) { | ||||
|         if (glyph.get_pixel(glyph_x, glyph_y)) { | ||||
|           this->draw_pixel_at(glyph_x + x_at, glyph_y + y_start, color); | ||||
|     { | ||||
|       const int glyph_x_max = scan_x1 + scan_width; | ||||
|       const int glyph_y_max = scan_y1 + scan_height; | ||||
|       for (int glyph_x = scan_x1; glyph_x < glyph_x_max; glyph_x++) { | ||||
|         for (int glyph_y = scan_y1; glyph_y < glyph_y_max; glyph_y++) { | ||||
|           if (glyph.get_pixel(glyph_x, glyph_y)) { | ||||
|             this->draw_pixel_at(glyph_x + x_at, glyph_y + y_start, color); | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   | ||||
| @@ -11,6 +11,7 @@ using namespace esphome::cover; | ||||
|  | ||||
| CoverTraits EndstopCover::get_traits() { | ||||
|   auto traits = CoverTraits(); | ||||
|   traits.set_supports_stop(true); | ||||
|   traits.set_supports_position(true); | ||||
|   traits.set_supports_toggle(true); | ||||
|   traits.set_is_assumed_state(false); | ||||
|   | ||||
| @@ -252,7 +252,7 @@ def _parse_platform_version(value): | ||||
|     try: | ||||
|         # if platform version is a valid version constraint, prefix the default package | ||||
|         cv.platformio_version_constraint(value) | ||||
|         return f"platformio/espressif32 @ {value}" | ||||
|         return f"platformio/espressif32@{value}" | ||||
|     except cv.Invalid: | ||||
|         return value | ||||
|  | ||||
| @@ -367,12 +367,12 @@ async def to_code(config): | ||||
|         cg.add_build_flag("-Wno-nonnull-compare") | ||||
|         cg.add_platformio_option( | ||||
|             "platform_packages", | ||||
|             [f"platformio/framework-espidf @ {conf[CONF_SOURCE]}"], | ||||
|             [f"platformio/framework-espidf@{conf[CONF_SOURCE]}"], | ||||
|         ) | ||||
|         # platformio/toolchain-esp32ulp does not support linux_aarch64 yet and has not been updated for over 2 years | ||||
|         # This is espressif's own published version which is more up to date. | ||||
|         cg.add_platformio_option( | ||||
|             "platform_packages", ["espressif/toolchain-esp32ulp @ 2.35.0-20220830"] | ||||
|             "platform_packages", ["espressif/toolchain-esp32ulp@2.35.0-20220830"] | ||||
|         ) | ||||
|         add_idf_sdkconfig_option("CONFIG_PARTITION_TABLE_SINGLE_APP", False) | ||||
|         add_idf_sdkconfig_option("CONFIG_PARTITION_TABLE_CUSTOM", True) | ||||
| @@ -433,7 +433,7 @@ async def to_code(config): | ||||
|         cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ARDUINO") | ||||
|         cg.add_platformio_option( | ||||
|             "platform_packages", | ||||
|             [f"platformio/framework-arduinoespressif32 @ {conf[CONF_SOURCE]}"], | ||||
|             [f"platformio/framework-arduinoespressif32@{conf[CONF_SOURCE]}"], | ||||
|         ) | ||||
|  | ||||
|         cg.add_platformio_option("board_build.partitions", "partitions.csv") | ||||
|   | ||||
| @@ -7,6 +7,7 @@ | ||||
| #include <freertos/task.h> | ||||
| #include <esp_idf_version.h> | ||||
| #include <esp_task_wdt.h> | ||||
| #include <esp_timer.h> | ||||
| #include <soc/rtc.h> | ||||
|  | ||||
| #if ESP_IDF_VERSION_MAJOR >= 4 | ||||
|   | ||||
| @@ -9,10 +9,9 @@ CODEOWNERS = ["@jesserockz"] | ||||
| CONFLICTS_WITH = ["esp32_ble_beacon"] | ||||
|  | ||||
| CONF_BLE_ID = "ble_id" | ||||
| CONF_IO_CAPABILITY = "io_capability" | ||||
|  | ||||
| NO_BLUTOOTH_VARIANTS = [const.VARIANT_ESP32S2] | ||||
|  | ||||
| NO_BLUTOOTH_VARIANTS = [const.VARIANT_ESP32S2] | ||||
| NO_BLUETOOTH_VARIANTS = [const.VARIANT_ESP32S2] | ||||
|  | ||||
| esp32_ble_ns = cg.esphome_ns.namespace("esp32_ble") | ||||
| ESP32BLE = esp32_ble_ns.class_("ESP32BLE", cg.Component) | ||||
| @@ -21,17 +20,28 @@ GAPEventHandler = esp32_ble_ns.class_("GAPEventHandler") | ||||
| GATTcEventHandler = esp32_ble_ns.class_("GATTcEventHandler") | ||||
| GATTsEventHandler = esp32_ble_ns.class_("GATTsEventHandler") | ||||
|  | ||||
| IoCapability = esp32_ble_ns.enum("IoCapability") | ||||
| IO_CAPABILITY = { | ||||
|     "none": IoCapability.IO_CAP_NONE, | ||||
|     "keyboard_only": IoCapability.IO_CAP_IN, | ||||
|     "keyboard_display": IoCapability.IO_CAP_KBDISP, | ||||
|     "display_only": IoCapability.IO_CAP_OUT, | ||||
|     "display_yes_no": IoCapability.IO_CAP_IO, | ||||
| } | ||||
|  | ||||
| CONFIG_SCHEMA = cv.Schema( | ||||
|     { | ||||
|         cv.GenerateID(): cv.declare_id(ESP32BLE), | ||||
|         cv.Optional(CONF_IO_CAPABILITY, default="none"): cv.enum( | ||||
|             IO_CAPABILITY, lower=True | ||||
|         ), | ||||
|     } | ||||
| ).extend(cv.COMPONENT_SCHEMA) | ||||
|  | ||||
|  | ||||
| def validate_variant(_): | ||||
|     variant = get_esp32_variant() | ||||
|     if variant in NO_BLUTOOTH_VARIANTS: | ||||
|     if variant in NO_BLUETOOTH_VARIANTS: | ||||
|         raise cv.Invalid(f"{variant} does not support Bluetooth") | ||||
|  | ||||
|  | ||||
| @@ -41,6 +51,7 @@ FINAL_VALIDATE_SCHEMA = validate_variant | ||||
| async def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     await cg.register_component(var, config) | ||||
|     cg.add(var.set_io_capability(config[CONF_IO_CAPABILITY])) | ||||
|  | ||||
|     if CORE.using_esp_idf: | ||||
|         add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True) | ||||
|   | ||||
| @@ -134,8 +134,7 @@ bool ESP32BLE::ble_setup_() { | ||||
|     return false; | ||||
|   } | ||||
|  | ||||
|   esp_ble_io_cap_t iocap = ESP_IO_CAP_NONE; | ||||
|   err = esp_ble_gap_set_security_param(ESP_BLE_SM_IOCAP_MODE, &iocap, sizeof(uint8_t)); | ||||
|   err = esp_ble_gap_set_security_param(ESP_BLE_SM_IOCAP_MODE, &(this->io_cap_), sizeof(uint8_t)); | ||||
|   if (err != ESP_OK) { | ||||
|     ESP_LOGE(TAG, "esp_ble_gap_set_security_param failed: %d", err); | ||||
|     return false; | ||||
| @@ -215,9 +214,31 @@ float ESP32BLE::get_setup_priority() const { return setup_priority::BLUETOOTH; } | ||||
| void ESP32BLE::dump_config() { | ||||
|   const uint8_t *mac_address = esp_bt_dev_get_address(); | ||||
|   if (mac_address) { | ||||
|     const char *io_capability_s; | ||||
|     switch (this->io_cap_) { | ||||
|       case ESP_IO_CAP_OUT: | ||||
|         io_capability_s = "display_only"; | ||||
|         break; | ||||
|       case ESP_IO_CAP_IO: | ||||
|         io_capability_s = "display_yes_no"; | ||||
|         break; | ||||
|       case ESP_IO_CAP_IN: | ||||
|         io_capability_s = "keyboard_only"; | ||||
|         break; | ||||
|       case ESP_IO_CAP_NONE: | ||||
|         io_capability_s = "none"; | ||||
|         break; | ||||
|       case ESP_IO_CAP_KBDISP: | ||||
|         io_capability_s = "keyboard_display"; | ||||
|         break; | ||||
|       default: | ||||
|         io_capability_s = "invalid"; | ||||
|         break; | ||||
|     } | ||||
|     ESP_LOGCONFIG(TAG, "ESP32 BLE:"); | ||||
|     ESP_LOGCONFIG(TAG, "  MAC address: %02X:%02X:%02X:%02X:%02X:%02X", mac_address[0], mac_address[1], mac_address[2], | ||||
|                   mac_address[3], mac_address[4], mac_address[5]); | ||||
|     ESP_LOGCONFIG(TAG, "  IO Capability: %s", io_capability_s); | ||||
|   } else { | ||||
|     ESP_LOGCONFIG(TAG, "ESP32 BLE: bluetooth stack is not enabled"); | ||||
|   } | ||||
|   | ||||
| @@ -25,6 +25,14 @@ typedef struct { | ||||
|   uint16_t mtu; | ||||
| } conn_status_t; | ||||
|  | ||||
| enum IoCapability { | ||||
|   IO_CAP_OUT = ESP_IO_CAP_OUT, | ||||
|   IO_CAP_IO = ESP_IO_CAP_IO, | ||||
|   IO_CAP_IN = ESP_IO_CAP_IN, | ||||
|   IO_CAP_NONE = ESP_IO_CAP_NONE, | ||||
|   IO_CAP_KBDISP = ESP_IO_CAP_KBDISP, | ||||
| }; | ||||
|  | ||||
| class GAPEventHandler { | ||||
|  public: | ||||
|   virtual void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) = 0; | ||||
| @@ -44,6 +52,8 @@ class GATTsEventHandler { | ||||
|  | ||||
| class ESP32BLE : public Component { | ||||
|  public: | ||||
|   void set_io_capability(IoCapability io_capability) { this->io_cap_ = (esp_ble_io_cap_t) io_capability; } | ||||
|  | ||||
|   void setup() override; | ||||
|   void loop() override; | ||||
|   void dump_config() override; | ||||
| @@ -72,6 +82,7 @@ class ESP32BLE : public Component { | ||||
|  | ||||
|   Queue<BLEEvent> ble_events_; | ||||
|   BLEAdvertising *advertising_; | ||||
|   esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE}; | ||||
| }; | ||||
|  | ||||
| // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) | ||||
|   | ||||
							
								
								
									
										0
									
								
								esphome/components/esp32_rmt_led_strip/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								esphome/components/esp32_rmt_led_strip/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										207
									
								
								esphome/components/esp32_rmt_led_strip/led_strip.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										207
									
								
								esphome/components/esp32_rmt_led_strip/led_strip.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,207 @@ | ||||
| #include "led_strip.h" | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|  | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| #include <esp_attr.h> | ||||
|  | ||||
| namespace esphome { | ||||
| namespace esp32_rmt_led_strip { | ||||
|  | ||||
| static const char *const TAG = "esp32_rmt_led_strip"; | ||||
|  | ||||
| static const uint8_t RMT_CLK_DIV = 2; | ||||
|  | ||||
| void ESP32RMTLEDStripLightOutput::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Setting up ESP32 LED Strip..."); | ||||
|  | ||||
|   size_t buffer_size = this->get_buffer_size_(); | ||||
|  | ||||
|   ExternalRAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE); | ||||
|   this->buf_ = allocator.allocate(buffer_size); | ||||
|   if (this->buf_ == nullptr) { | ||||
|     ESP_LOGE(TAG, "Cannot allocate LED buffer!"); | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   this->effect_data_ = allocator.allocate(this->num_leds_); | ||||
|   if (this->effect_data_ == nullptr) { | ||||
|     ESP_LOGE(TAG, "Cannot allocate effect data!"); | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   ExternalRAMAllocator<rmt_item32_t> rmt_allocator(ExternalRAMAllocator<rmt_item32_t>::ALLOW_FAILURE); | ||||
|   this->rmt_buf_ = rmt_allocator.allocate(buffer_size * 8);  // 8 bits per byte, 1 rmt_item32_t per bit | ||||
|  | ||||
|   rmt_config_t config; | ||||
|   memset(&config, 0, sizeof(config)); | ||||
|   config.channel = this->channel_; | ||||
|   config.rmt_mode = RMT_MODE_TX; | ||||
|   config.gpio_num = gpio_num_t(this->pin_); | ||||
|   config.mem_block_num = 1; | ||||
|   config.clk_div = RMT_CLK_DIV; | ||||
|   config.tx_config.loop_en = false; | ||||
|   config.tx_config.carrier_level = RMT_CARRIER_LEVEL_LOW; | ||||
|   config.tx_config.carrier_en = false; | ||||
|   config.tx_config.idle_level = RMT_IDLE_LEVEL_LOW; | ||||
|   config.tx_config.idle_output_en = true; | ||||
|  | ||||
|   if (rmt_config(&config) != ESP_OK) { | ||||
|     ESP_LOGE(TAG, "Cannot initialize RMT!"); | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
|   if (rmt_driver_install(config.channel, 0, 0) != ESP_OK) { | ||||
|     ESP_LOGE(TAG, "Cannot install RMT driver!"); | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
| } | ||||
|  | ||||
| void ESP32RMTLEDStripLightOutput::set_led_params(uint32_t bit0_high, uint32_t bit0_low, uint32_t bit1_high, | ||||
|                                                  uint32_t bit1_low) { | ||||
|   float ratio = (float) APB_CLK_FREQ / RMT_CLK_DIV / 1e09f; | ||||
|  | ||||
|   // 0-bit | ||||
|   this->bit0_.duration0 = (uint32_t) (ratio * bit0_high); | ||||
|   this->bit0_.level0 = 1; | ||||
|   this->bit0_.duration1 = (uint32_t) (ratio * bit0_low); | ||||
|   this->bit0_.level1 = 0; | ||||
|   // 1-bit | ||||
|   this->bit1_.duration0 = (uint32_t) (ratio * bit1_high); | ||||
|   this->bit1_.level0 = 1; | ||||
|   this->bit1_.duration1 = (uint32_t) (ratio * bit1_low); | ||||
|   this->bit1_.level1 = 0; | ||||
| } | ||||
|  | ||||
| void ESP32RMTLEDStripLightOutput::write_state(light::LightState *state) { | ||||
|   // protect from refreshing too often | ||||
|   uint32_t now = micros(); | ||||
|   if (*this->max_refresh_rate_ != 0 && (now - this->last_refresh_) < *this->max_refresh_rate_) { | ||||
|     // try again next loop iteration, so that this change won't get lost | ||||
|     this->schedule_show(); | ||||
|     return; | ||||
|   } | ||||
|   this->last_refresh_ = now; | ||||
|   this->mark_shown_(); | ||||
|  | ||||
|   ESP_LOGVV(TAG, "Writing RGB values to bus..."); | ||||
|  | ||||
|   if (rmt_wait_tx_done(this->channel_, pdMS_TO_TICKS(1000)) != ESP_OK) { | ||||
|     ESP_LOGE(TAG, "RMT TX timeout"); | ||||
|     this->status_set_warning(); | ||||
|     return; | ||||
|   } | ||||
|   delayMicroseconds(50); | ||||
|  | ||||
|   size_t buffer_size = this->get_buffer_size_(); | ||||
|  | ||||
|   size_t size = 0; | ||||
|   size_t len = 0; | ||||
|   uint8_t *psrc = this->buf_; | ||||
|   rmt_item32_t *pdest = this->rmt_buf_; | ||||
|   while (size < buffer_size) { | ||||
|     uint8_t b = *psrc; | ||||
|     for (int i = 0; i < 8; i++) { | ||||
|       pdest->val = b & (1 << (7 - i)) ? this->bit1_.val : this->bit0_.val; | ||||
|       pdest++; | ||||
|       len++; | ||||
|     } | ||||
|     size++; | ||||
|     psrc++; | ||||
|   } | ||||
|  | ||||
|   if (rmt_write_items(this->channel_, this->rmt_buf_, len, false) != ESP_OK) { | ||||
|     ESP_LOGE(TAG, "RMT TX error"); | ||||
|     this->status_set_warning(); | ||||
|     return; | ||||
|   } | ||||
|   this->status_clear_warning(); | ||||
| } | ||||
|  | ||||
| light::ESPColorView ESP32RMTLEDStripLightOutput::get_view_internal(int32_t index) const { | ||||
|   int32_t r = 0, g = 0, b = 0; | ||||
|   switch (this->rgb_order_) { | ||||
|     case ORDER_RGB: | ||||
|       r = 0; | ||||
|       g = 1; | ||||
|       b = 2; | ||||
|       break; | ||||
|     case ORDER_RBG: | ||||
|       r = 0; | ||||
|       g = 2; | ||||
|       b = 1; | ||||
|       break; | ||||
|     case ORDER_GRB: | ||||
|       r = 1; | ||||
|       g = 0; | ||||
|       b = 2; | ||||
|       break; | ||||
|     case ORDER_GBR: | ||||
|       r = 2; | ||||
|       g = 0; | ||||
|       b = 1; | ||||
|       break; | ||||
|     case ORDER_BGR: | ||||
|       r = 2; | ||||
|       g = 1; | ||||
|       b = 0; | ||||
|       break; | ||||
|     case ORDER_BRG: | ||||
|       r = 1; | ||||
|       g = 2; | ||||
|       b = 0; | ||||
|       break; | ||||
|   } | ||||
|   uint8_t multiplier = this->is_rgbw_ ? 4 : 3; | ||||
|   return {this->buf_ + (index * multiplier) + r, | ||||
|           this->buf_ + (index * multiplier) + g, | ||||
|           this->buf_ + (index * multiplier) + b, | ||||
|           this->is_rgbw_ ? this->buf_ + (index * multiplier) + 3 : nullptr, | ||||
|           &this->effect_data_[index], | ||||
|           &this->correction_}; | ||||
| } | ||||
|  | ||||
| void ESP32RMTLEDStripLightOutput::dump_config() { | ||||
|   ESP_LOGCONFIG(TAG, "ESP32 RMT LED Strip:"); | ||||
|   ESP_LOGCONFIG(TAG, "  Pin: %u", this->pin_); | ||||
|   ESP_LOGCONFIG(TAG, "  Channel: %u", this->channel_); | ||||
|   const char *rgb_order; | ||||
|   switch (this->rgb_order_) { | ||||
|     case ORDER_RGB: | ||||
|       rgb_order = "RGB"; | ||||
|       break; | ||||
|     case ORDER_RBG: | ||||
|       rgb_order = "RBG"; | ||||
|       break; | ||||
|     case ORDER_GRB: | ||||
|       rgb_order = "GRB"; | ||||
|       break; | ||||
|     case ORDER_GBR: | ||||
|       rgb_order = "GBR"; | ||||
|       break; | ||||
|     case ORDER_BGR: | ||||
|       rgb_order = "BGR"; | ||||
|       break; | ||||
|     case ORDER_BRG: | ||||
|       rgb_order = "BRG"; | ||||
|       break; | ||||
|     default: | ||||
|       rgb_order = "UNKNOWN"; | ||||
|       break; | ||||
|   } | ||||
|   ESP_LOGCONFIG(TAG, "  RGB Order: %s", rgb_order); | ||||
|   ESP_LOGCONFIG(TAG, "  Max refresh rate: %u", *this->max_refresh_rate_); | ||||
|   ESP_LOGCONFIG(TAG, "  Number of LEDs: %u", this->num_leds_); | ||||
| } | ||||
|  | ||||
| float ESP32RMTLEDStripLightOutput::get_setup_priority() const { return setup_priority::HARDWARE; } | ||||
|  | ||||
| }  // namespace esp32_rmt_led_strip | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif  // USE_ESP32 | ||||
							
								
								
									
										87
									
								
								esphome/components/esp32_rmt_led_strip/led_strip.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								esphome/components/esp32_rmt_led_strip/led_strip.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,87 @@ | ||||
| #pragma once | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|  | ||||
| #include "esphome/components/light/addressable_light.h" | ||||
| #include "esphome/components/light/light_output.h" | ||||
| #include "esphome/core/color.h" | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/helpers.h" | ||||
|  | ||||
| #include <driver/gpio.h> | ||||
| #include <driver/rmt.h> | ||||
| #include <esp_err.h> | ||||
|  | ||||
| namespace esphome { | ||||
| namespace esp32_rmt_led_strip { | ||||
|  | ||||
| enum RGBOrder : uint8_t { | ||||
|   ORDER_RGB, | ||||
|   ORDER_RBG, | ||||
|   ORDER_GRB, | ||||
|   ORDER_GBR, | ||||
|   ORDER_BGR, | ||||
|   ORDER_BRG, | ||||
| }; | ||||
|  | ||||
| class ESP32RMTLEDStripLightOutput : public light::AddressableLight { | ||||
|  public: | ||||
|   void setup() override; | ||||
|   void write_state(light::LightState *state) override; | ||||
|   float get_setup_priority() const override; | ||||
|  | ||||
|   int32_t size() const override { return this->num_leds_; } | ||||
|   light::LightTraits get_traits() override { | ||||
|     auto traits = light::LightTraits(); | ||||
|     if (this->is_rgbw_) { | ||||
|       traits.set_supported_color_modes({light::ColorMode::RGB, light::ColorMode::RGB_WHITE}); | ||||
|     } else { | ||||
|       traits.set_supported_color_modes({light::ColorMode::RGB}); | ||||
|     } | ||||
|     return traits; | ||||
|   } | ||||
|  | ||||
|   void set_pin(uint8_t pin) { this->pin_ = pin; } | ||||
|   void set_num_leds(uint16_t num_leds) { this->num_leds_ = num_leds; } | ||||
|   void set_is_rgbw(bool is_rgbw) { this->is_rgbw_ = is_rgbw; } | ||||
|  | ||||
|   /// Set a maximum refresh rate in µs as some lights do not like being updated too often. | ||||
|   void set_max_refresh_rate(uint32_t interval_us) { this->max_refresh_rate_ = interval_us; } | ||||
|  | ||||
|   void set_led_params(uint32_t bit0_high, uint32_t bit0_low, uint32_t bit1_high, uint32_t bit1_low); | ||||
|  | ||||
|   void set_rgb_order(RGBOrder rgb_order) { this->rgb_order_ = rgb_order; } | ||||
|   void set_rmt_channel(rmt_channel_t channel) { this->channel_ = channel; } | ||||
|  | ||||
|   void clear_effect_data() override { | ||||
|     for (int i = 0; i < this->size(); i++) | ||||
|       this->effect_data_[i] = 0; | ||||
|   } | ||||
|  | ||||
|   void dump_config() override; | ||||
|  | ||||
|  protected: | ||||
|   light::ESPColorView get_view_internal(int32_t index) const override; | ||||
|  | ||||
|   size_t get_buffer_size_() const { return this->num_leds_ * (3 + this->is_rgbw_); } | ||||
|  | ||||
|   uint8_t *buf_{nullptr}; | ||||
|   uint8_t *effect_data_{nullptr}; | ||||
|   rmt_item32_t *rmt_buf_{nullptr}; | ||||
|  | ||||
|   uint8_t pin_; | ||||
|   uint16_t num_leds_; | ||||
|   bool is_rgbw_; | ||||
|  | ||||
|   rmt_item32_t bit0_, bit1_; | ||||
|   RGBOrder rgb_order_; | ||||
|   rmt_channel_t channel_; | ||||
|  | ||||
|   uint32_t last_refresh_{0}; | ||||
|   optional<uint32_t> max_refresh_rate_{}; | ||||
| }; | ||||
|  | ||||
| }  // namespace esp32_rmt_led_strip | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif  // USE_ESP32 | ||||
							
								
								
									
										151
									
								
								esphome/components/esp32_rmt_led_strip/light.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										151
									
								
								esphome/components/esp32_rmt_led_strip/light.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,151 @@ | ||||
| from dataclasses import dataclass | ||||
|  | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome import pins | ||||
| from esphome.components import esp32, light | ||||
| from esphome.const import ( | ||||
|     CONF_CHIPSET, | ||||
|     CONF_MAX_REFRESH_RATE, | ||||
|     CONF_NUM_LEDS, | ||||
|     CONF_OUTPUT_ID, | ||||
|     CONF_PIN, | ||||
|     CONF_RGB_ORDER, | ||||
| ) | ||||
|  | ||||
| CODEOWNERS = ["@jesserockz"] | ||||
| DEPENDENCIES = ["esp32"] | ||||
|  | ||||
| esp32_rmt_led_strip_ns = cg.esphome_ns.namespace("esp32_rmt_led_strip") | ||||
| ESP32RMTLEDStripLightOutput = esp32_rmt_led_strip_ns.class_( | ||||
|     "ESP32RMTLEDStripLightOutput", light.AddressableLight | ||||
| ) | ||||
|  | ||||
| rmt_channel_t = cg.global_ns.enum("rmt_channel_t") | ||||
|  | ||||
| RGBOrder = esp32_rmt_led_strip_ns.enum("RGBOrder") | ||||
|  | ||||
| RGB_ORDERS = { | ||||
|     "RGB": RGBOrder.ORDER_RGB, | ||||
|     "RBG": RGBOrder.ORDER_RBG, | ||||
|     "GRB": RGBOrder.ORDER_GRB, | ||||
|     "GBR": RGBOrder.ORDER_GBR, | ||||
|     "BGR": RGBOrder.ORDER_BGR, | ||||
|     "BRG": RGBOrder.ORDER_BRG, | ||||
| } | ||||
|  | ||||
|  | ||||
| @dataclass | ||||
| class LEDStripTimings: | ||||
|     bit0_high: int | ||||
|     bit0_low: int | ||||
|     bit1_high: int | ||||
|     bit1_low: int | ||||
|  | ||||
|  | ||||
| CHIPSETS = { | ||||
|     "WS2812": LEDStripTimings(400, 1000, 1000, 400), | ||||
|     "SK6812": LEDStripTimings(300, 900, 600, 600), | ||||
|     "APA106": LEDStripTimings(350, 1360, 1360, 350), | ||||
|     "SM16703": LEDStripTimings(300, 900, 1360, 350), | ||||
| } | ||||
|  | ||||
|  | ||||
| CONF_IS_RGBW = "is_rgbw" | ||||
| CONF_BIT0_HIGH = "bit0_high" | ||||
| CONF_BIT0_LOW = "bit0_low" | ||||
| CONF_BIT1_HIGH = "bit1_high" | ||||
| CONF_BIT1_LOW = "bit1_low" | ||||
| CONF_RMT_CHANNEL = "rmt_channel" | ||||
|  | ||||
| RMT_CHANNELS = { | ||||
|     esp32.const.VARIANT_ESP32: [0, 1, 2, 3, 4, 5, 6, 7], | ||||
|     esp32.const.VARIANT_ESP32S2: [0, 1, 2, 3], | ||||
|     esp32.const.VARIANT_ESP32S3: [0, 1, 2, 3], | ||||
|     esp32.const.VARIANT_ESP32C3: [0, 1], | ||||
| } | ||||
|  | ||||
|  | ||||
| def _validate_rmt_channel(value): | ||||
|     variant = esp32.get_esp32_variant() | ||||
|     if variant not in RMT_CHANNELS: | ||||
|         raise cv.Invalid(f"ESP32 variant {variant} does not support RMT.") | ||||
|     if value not in RMT_CHANNELS[variant]: | ||||
|         raise cv.Invalid( | ||||
|             f"RMT channel {value} is not supported for ESP32 variant {variant}." | ||||
|         ) | ||||
|     return value | ||||
|  | ||||
|  | ||||
| CONFIG_SCHEMA = cv.All( | ||||
|     light.ADDRESSABLE_LIGHT_SCHEMA.extend( | ||||
|         { | ||||
|             cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(ESP32RMTLEDStripLightOutput), | ||||
|             cv.Required(CONF_PIN): pins.internal_gpio_output_pin_number, | ||||
|             cv.Required(CONF_NUM_LEDS): cv.positive_not_null_int, | ||||
|             cv.Required(CONF_RGB_ORDER): cv.enum(RGB_ORDERS, upper=True), | ||||
|             cv.Required(CONF_RMT_CHANNEL): _validate_rmt_channel, | ||||
|             cv.Optional(CONF_MAX_REFRESH_RATE): cv.positive_time_period_microseconds, | ||||
|             cv.Optional(CONF_CHIPSET): cv.one_of(*CHIPSETS, upper=True), | ||||
|             cv.Optional(CONF_IS_RGBW, default=False): cv.boolean, | ||||
|             cv.Inclusive( | ||||
|                 CONF_BIT0_HIGH, | ||||
|                 "custom", | ||||
|             ): cv.positive_time_period_microseconds, | ||||
|             cv.Inclusive( | ||||
|                 CONF_BIT0_LOW, | ||||
|                 "custom", | ||||
|             ): cv.positive_time_period_microseconds, | ||||
|             cv.Inclusive( | ||||
|                 CONF_BIT1_HIGH, | ||||
|                 "custom", | ||||
|             ): cv.positive_time_period_microseconds, | ||||
|             cv.Inclusive( | ||||
|                 CONF_BIT1_LOW, | ||||
|                 "custom", | ||||
|             ): cv.positive_time_period_microseconds, | ||||
|         } | ||||
|     ), | ||||
|     cv.has_exactly_one_key(CONF_CHIPSET, CONF_BIT0_HIGH), | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) | ||||
|     await light.register_light(var, config) | ||||
|     await cg.register_component(var, config) | ||||
|  | ||||
|     cg.add(var.set_num_leds(config[CONF_NUM_LEDS])) | ||||
|     cg.add(var.set_pin(config[CONF_PIN])) | ||||
|  | ||||
|     if CONF_MAX_REFRESH_RATE in config: | ||||
|         cg.add(var.set_max_refresh_rate(config[CONF_MAX_REFRESH_RATE])) | ||||
|  | ||||
|     if CONF_CHIPSET in config: | ||||
|         chipset = CHIPSETS[config[CONF_CHIPSET]] | ||||
|         cg.add( | ||||
|             var.set_led_params( | ||||
|                 chipset.bit0_high, | ||||
|                 chipset.bit0_low, | ||||
|                 chipset.bit1_high, | ||||
|                 chipset.bit1_low, | ||||
|             ) | ||||
|         ) | ||||
|     else: | ||||
|         cg.add( | ||||
|             var.set_led_params( | ||||
|                 config[CONF_BIT0_HIGH], | ||||
|                 config[CONF_BIT0_LOW], | ||||
|                 config[CONF_BIT1_HIGH], | ||||
|                 config[CONF_BIT1_LOW], | ||||
|             ) | ||||
|         ) | ||||
|  | ||||
|     cg.add(var.set_rgb_order(config[CONF_RGB_ORDER])) | ||||
|     cg.add(var.set_is_rgbw(config[CONF_IS_RGBW])) | ||||
|  | ||||
|     cg.add( | ||||
|         var.set_rmt_channel( | ||||
|             getattr(rmt_channel_t, f"RMT_CHANNEL_{config[CONF_RMT_CHANNEL]}") | ||||
|         ) | ||||
|     ) | ||||
| @@ -125,7 +125,7 @@ def _parse_platform_version(value): | ||||
|     try: | ||||
|         # if platform version is a valid version constraint, prefix the default package | ||||
|         cv.platformio_version_constraint(value) | ||||
|         return f"platformio/espressif8266 @ {value}" | ||||
|         return f"platformio/espressif8266@{value}" | ||||
|     except cv.Invalid: | ||||
|         return value | ||||
|  | ||||
| @@ -181,7 +181,7 @@ async def to_code(config): | ||||
|     cg.add_platformio_option("platform", conf[CONF_PLATFORM_VERSION]) | ||||
|     cg.add_platformio_option( | ||||
|         "platform_packages", | ||||
|         [f"platformio/framework-arduinoespressif8266 @ {conf[CONF_SOURCE]}"], | ||||
|         [f"platformio/framework-arduinoespressif8266@{conf[CONF_SOURCE]}"], | ||||
|     ) | ||||
|  | ||||
|     # Default for platformio is LWIP2_LOW_MEMORY with: | ||||
|   | ||||
| @@ -106,20 +106,18 @@ void EZOSensor::loop() { | ||||
|       break; | ||||
|   } | ||||
|  | ||||
|   ESP_LOGV(TAG, "Received buffer \"%s\" for command type %s", buf, EZO_COMMAND_TYPE_STRINGS[to_run->command_type]); | ||||
|   ESP_LOGV(TAG, "Received buffer \"%s\" for command type %s", &buf[1], EZO_COMMAND_TYPE_STRINGS[to_run->command_type]); | ||||
|  | ||||
|   if ((buf[0] == 1) || (to_run->command_type == EzoCommandType::EZO_CALIBRATION)) {  // EZO_CALIBRATION returns 0-3 | ||||
|     // some sensors return multiple comma-separated values, terminate string after first one | ||||
|     for (size_t i = 1; i < sizeof(buf) - 1; i++) { | ||||
|       if (buf[i] == ',') { | ||||
|         buf[i] = '\0'; | ||||
|         break; | ||||
|       } | ||||
|     } | ||||
|   if (buf[0] == 1) { | ||||
|     std::string payload = reinterpret_cast<char *>(&buf[1]); | ||||
|     if (!payload.empty()) { | ||||
|       switch (to_run->command_type) { | ||||
|         case EzoCommandType::EZO_READ: { | ||||
|           // some sensors return multiple comma-separated values, terminate string after first one | ||||
|           int start_location = 0; | ||||
|           if ((start_location = payload.find(',')) != std::string::npos) { | ||||
|             payload.erase(start_location); | ||||
|           } | ||||
|           auto val = parse_number<float>(payload); | ||||
|           if (!val.has_value()) { | ||||
|             ESP_LOGW(TAG, "Can't convert '%s' to number!", payload.c_str()); | ||||
| @@ -154,7 +152,10 @@ void EZOSensor::loop() { | ||||
|           break; | ||||
|         } | ||||
|         case EzoCommandType::EZO_T: { | ||||
|           this->t_callback_.call(payload); | ||||
|           int start_location = 0; | ||||
|           if ((start_location = payload.find(',')) != std::string::npos) { | ||||
|             this->t_callback_.call(payload.substr(start_location + 1)); | ||||
|           } | ||||
|           break; | ||||
|         } | ||||
|         case EzoCommandType::EZO_CUSTOM: { | ||||
|   | ||||
| @@ -41,6 +41,7 @@ void FeedbackCover::setup() { | ||||
|  | ||||
| CoverTraits FeedbackCover::get_traits() { | ||||
|   auto traits = CoverTraits(); | ||||
|   traits.set_supports_stop(true); | ||||
|   traits.set_supports_position(true); | ||||
|   traits.set_supports_toggle(true); | ||||
|   traits.set_is_assumed_state(this->assumed_state_); | ||||
|   | ||||
							
								
								
									
										40
									
								
								esphome/components/gp8403/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								esphome/components/gp8403/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | ||||
| import esphome.config_validation as cv | ||||
| import esphome.codegen as cg | ||||
|  | ||||
| from esphome.components import i2c | ||||
| from esphome.const import CONF_ID, CONF_VOLTAGE | ||||
|  | ||||
| CODEOWNERS = ["@jesserockz"] | ||||
| DEPENDENCIES = ["i2c"] | ||||
| MULTI_CONF = True | ||||
|  | ||||
| gp8403_ns = cg.esphome_ns.namespace("gp8403") | ||||
| GP8403 = gp8403_ns.class_("GP8403", cg.Component, i2c.I2CDevice) | ||||
|  | ||||
| GP8403Voltage = gp8403_ns.enum("GP8403Voltage") | ||||
|  | ||||
| CONF_GP8403_ID = "gp8403_id" | ||||
|  | ||||
| VOLTAGES = { | ||||
|     "5V": GP8403Voltage.GP8403_VOLTAGE_5V, | ||||
|     "10V": GP8403Voltage.GP8403_VOLTAGE_10V, | ||||
| } | ||||
|  | ||||
| CONFIG_SCHEMA = ( | ||||
|     cv.Schema( | ||||
|         { | ||||
|             cv.GenerateID(): cv.declare_id(GP8403), | ||||
|             cv.Required(CONF_VOLTAGE): cv.enum(VOLTAGES, upper=True), | ||||
|         } | ||||
|     ) | ||||
|     .extend(cv.COMPONENT_SCHEMA) | ||||
|     .extend(i2c.i2c_device_schema(0x58)) | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     await cg.register_component(var, config) | ||||
|     await i2c.register_i2c_device(var, config) | ||||
|  | ||||
|     cg.add(var.set_voltage(config[CONF_VOLTAGE])) | ||||
							
								
								
									
										21
									
								
								esphome/components/gp8403/gp8403.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								esphome/components/gp8403/gp8403.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| #include "gp8403.h" | ||||
|  | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace gp8403 { | ||||
|  | ||||
| static const char *const TAG = "gp8403"; | ||||
|  | ||||
| static const uint8_t RANGE_REGISTER = 0x01; | ||||
|  | ||||
| void GP8403::setup() { this->write_register(RANGE_REGISTER, (uint8_t *) (&this->voltage_), 1); } | ||||
|  | ||||
| void GP8403::dump_config() { | ||||
|   ESP_LOGCONFIG(TAG, "GP8403:"); | ||||
|   ESP_LOGCONFIG(TAG, "  Voltage: %dV", this->voltage_ == GP8403_VOLTAGE_5V ? 5 : 10); | ||||
|   LOG_I2C_DEVICE(this); | ||||
| } | ||||
|  | ||||
| }  // namespace gp8403 | ||||
| }  // namespace esphome | ||||
							
								
								
									
										27
									
								
								esphome/components/gp8403/gp8403.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								esphome/components/gp8403/gp8403.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/components/i2c/i2c.h" | ||||
| #include "esphome/core/component.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace gp8403 { | ||||
|  | ||||
| enum GP8403Voltage { | ||||
|   GP8403_VOLTAGE_5V = 0x00, | ||||
|   GP8403_VOLTAGE_10V = 0x11, | ||||
| }; | ||||
|  | ||||
| class GP8403 : public Component, public i2c::I2CDevice { | ||||
|  public: | ||||
|   void setup() override; | ||||
|   void dump_config() override; | ||||
|   float get_setup_priority() const override { return setup_priority::DATA; } | ||||
|  | ||||
|   void set_voltage(gp8403::GP8403Voltage voltage) { this->voltage_ = voltage; } | ||||
|  | ||||
|  protected: | ||||
|   GP8403Voltage voltage_; | ||||
| }; | ||||
|  | ||||
| }  // namespace gp8403 | ||||
| }  // namespace esphome | ||||
							
								
								
									
										31
									
								
								esphome/components/gp8403/output/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								esphome/components/gp8403/output/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | ||||
| import esphome.config_validation as cv | ||||
| import esphome.codegen as cg | ||||
|  | ||||
| from esphome.components import i2c, output | ||||
| from esphome.const import CONF_ID, CONF_CHANNEL | ||||
|  | ||||
| from .. import gp8403_ns, GP8403, CONF_GP8403_ID | ||||
|  | ||||
| DEPENDENCIES = ["gp8403"] | ||||
|  | ||||
| GP8403Output = gp8403_ns.class_( | ||||
|     "GP8403Output", cg.Component, i2c.I2CDevice, output.FloatOutput | ||||
| ) | ||||
|  | ||||
| CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( | ||||
|     { | ||||
|         cv.GenerateID(): cv.declare_id(GP8403Output), | ||||
|         cv.GenerateID(CONF_GP8403_ID): cv.use_id(GP8403), | ||||
|         cv.Required(CONF_CHANNEL): cv.one_of(0, 1), | ||||
|     } | ||||
| ).extend(cv.COMPONENT_SCHEMA) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     await cg.register_component(var, config) | ||||
|     await output.register_output(var, config) | ||||
|  | ||||
|     await cg.register_parented(var, config[CONF_GP8403_ID]) | ||||
|  | ||||
|     cg.add(var.set_channel(config[CONF_CHANNEL])) | ||||
							
								
								
									
										26
									
								
								esphome/components/gp8403/output/gp8403_output.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								esphome/components/gp8403/output/gp8403_output.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | ||||
| #include "gp8403_output.h" | ||||
|  | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace gp8403 { | ||||
|  | ||||
| static const char *const TAG = "gp8403.output"; | ||||
|  | ||||
| static const uint8_t OUTPUT_REGISTER = 0x02; | ||||
|  | ||||
| void GP8403Output::dump_config() { | ||||
|   ESP_LOGCONFIG(TAG, "GP8403 Output:"); | ||||
|   ESP_LOGCONFIG(TAG, "  Channel: %u", this->channel_); | ||||
| } | ||||
|  | ||||
| void GP8403Output::write_state(float state) { | ||||
|   uint16_t value = ((uint16_t) (state * 4095)) << 4; | ||||
|   i2c::ErrorCode err = this->parent_->write_register(OUTPUT_REGISTER + (2 * this->channel_), (uint8_t *) &value, 2); | ||||
|   if (err != i2c::ERROR_OK) { | ||||
|     ESP_LOGE(TAG, "Error writing to GP8403, code %d", err); | ||||
|   } | ||||
| } | ||||
|  | ||||
| }  // namespace gp8403 | ||||
| }  // namespace esphome | ||||
							
								
								
									
										25
									
								
								esphome/components/gp8403/output/gp8403_output.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								esphome/components/gp8403/output/gp8403_output.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/components/output/float_output.h" | ||||
| #include "esphome/core/component.h" | ||||
|  | ||||
| #include "esphome/components/gp8403/gp8403.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace gp8403 { | ||||
|  | ||||
| class GP8403Output : public Component, public output::FloatOutput, public Parented<GP8403> { | ||||
|  public: | ||||
|   void dump_config() override; | ||||
|   float get_setup_priority() const override { return setup_priority::DATA - 1; } | ||||
|  | ||||
|   void set_channel(uint8_t channel) { this->channel_ = channel; } | ||||
|  | ||||
|   void write_state(float state) override; | ||||
|  | ||||
|  protected: | ||||
|   uint8_t channel_; | ||||
| }; | ||||
|  | ||||
| }  // namespace gp8403 | ||||
| }  // namespace esphome | ||||
							
								
								
									
										38
									
								
								esphome/components/host/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								esphome/components/host/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | ||||
| from esphome.const import ( | ||||
|     KEY_CORE, | ||||
|     KEY_FRAMEWORK_VERSION, | ||||
|     KEY_TARGET_FRAMEWORK, | ||||
|     KEY_TARGET_PLATFORM, | ||||
| ) | ||||
| from esphome.core import CORE | ||||
| import esphome.config_validation as cv | ||||
| import esphome.codegen as cg | ||||
|  | ||||
| from .const import KEY_HOST | ||||
|  | ||||
| # force import gpio to register pin schema | ||||
| from .gpio import host_pin_to_code  # noqa | ||||
|  | ||||
|  | ||||
| CODEOWNERS = ["@esphome/core"] | ||||
| AUTO_LOAD = ["network"] | ||||
|  | ||||
|  | ||||
| def set_core_data(config): | ||||
|     CORE.data[KEY_HOST] = {} | ||||
|     CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = "host" | ||||
|     CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = "host" | ||||
|     CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version(1, 0, 0) | ||||
|     return config | ||||
|  | ||||
|  | ||||
| CONFIG_SCHEMA = cv.All( | ||||
|     cv.Schema({}), | ||||
|     set_core_data, | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     cg.add_build_flag("-DUSE_HOST") | ||||
|     cg.add_define("ESPHOME_BOARD", "host") | ||||
|     cg.add_platformio_option("platform", "platformio/native") | ||||
							
								
								
									
										5
									
								
								esphome/components/host/const.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								esphome/components/host/const.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| import esphome.codegen as cg | ||||
|  | ||||
| KEY_HOST = "host" | ||||
|  | ||||
| host_ns = cg.esphome_ns.namespace("host") | ||||
							
								
								
									
										77
									
								
								esphome/components/host/core.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								esphome/components/host/core.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,77 @@ | ||||
| #ifdef USE_HOST | ||||
|  | ||||
| #include "esphome/core/hal.h" | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "preferences.h" | ||||
|  | ||||
| #include <sched.h> | ||||
| #include <time.h> | ||||
| #include <cmath> | ||||
| #include <cstdlib> | ||||
|  | ||||
| namespace esphome { | ||||
|  | ||||
| void IRAM_ATTR HOT yield() { ::sched_yield(); } | ||||
| uint32_t IRAM_ATTR HOT millis() { | ||||
|   struct timespec spec; | ||||
|   clock_gettime(CLOCK_MONOTONIC, &spec); | ||||
|   time_t seconds = spec.tv_sec; | ||||
|   uint32_t ms = round(spec.tv_nsec / 1e6); | ||||
|   return ((uint32_t) seconds) * 1000U + ms; | ||||
| } | ||||
| void IRAM_ATTR HOT delay(uint32_t ms) { | ||||
|   struct timespec ts; | ||||
|   ts.tv_sec = ms / 1000; | ||||
|   ts.tv_nsec = (ms % 1000) * 1000000; | ||||
|   int res; | ||||
|   do { | ||||
|     res = nanosleep(&ts, &ts); | ||||
|   } while (res != 0 && errno == EINTR); | ||||
| } | ||||
| uint32_t IRAM_ATTR HOT micros() { | ||||
|   struct timespec spec; | ||||
|   clock_gettime(CLOCK_MONOTONIC, &spec); | ||||
|   time_t seconds = spec.tv_sec; | ||||
|   uint32_t us = round(spec.tv_nsec / 1e3); | ||||
|   return ((uint32_t) seconds) * 1000000U + us; | ||||
| } | ||||
| void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { | ||||
|   struct timespec ts; | ||||
|   ts.tv_sec = us / 1000000U; | ||||
|   ts.tv_nsec = (us % 1000000U) * 1000U; | ||||
|   int res; | ||||
|   do { | ||||
|     res = nanosleep(&ts, &ts); | ||||
|   } while (res != 0 && errno == EINTR); | ||||
| } | ||||
| void arch_restart() { exit(0); } | ||||
| void arch_init() { | ||||
|   // pass | ||||
| } | ||||
| void IRAM_ATTR HOT arch_feed_wdt() { | ||||
|   // pass | ||||
| } | ||||
|  | ||||
| uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; } | ||||
| uint32_t arch_get_cpu_cycle_count() { | ||||
|   struct timespec spec; | ||||
|   clock_gettime(CLOCK_MONOTONIC, &spec); | ||||
|   time_t seconds = spec.tv_sec; | ||||
|   uint32_t us = spec.tv_nsec; | ||||
|   return ((uint32_t) seconds) * 1000000000U + us; | ||||
| } | ||||
| uint32_t arch_get_cpu_freq_hz() { return 1000000000U; } | ||||
|  | ||||
| }  // namespace esphome | ||||
|  | ||||
| void setup(); | ||||
| void loop(); | ||||
| int main() { | ||||
|   esphome::host::setup_preferences(); | ||||
|   setup(); | ||||
|   while (true) { | ||||
|     loop(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| #endif  // USE_HOST | ||||
							
								
								
									
										59
									
								
								esphome/components/host/gpio.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								esphome/components/host/gpio.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,59 @@ | ||||
| #ifdef USE_HOST | ||||
|  | ||||
| #include "gpio.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace host { | ||||
|  | ||||
| static const char *const TAG = "host"; | ||||
|  | ||||
| struct ISRPinArg { | ||||
|   uint8_t pin; | ||||
|   bool inverted; | ||||
| }; | ||||
|  | ||||
| ISRInternalGPIOPin HostGPIOPin::to_isr() const { | ||||
|   auto *arg = new ISRPinArg{};  // NOLINT(cppcoreguidelines-owning-memory) | ||||
|   arg->pin = pin_; | ||||
|   arg->inverted = inverted_; | ||||
|   return ISRInternalGPIOPin((void *) arg); | ||||
| } | ||||
|  | ||||
| void HostGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const { | ||||
|   ESP_LOGD(TAG, "Attaching interrupt %p to pin %d and mode %d", func, pin_, (uint32_t) type); | ||||
| } | ||||
| void HostGPIOPin::pin_mode(gpio::Flags flags) { ESP_LOGD(TAG, "Setting pin %d mode to %02X", pin_, (uint32_t) flags); } | ||||
|  | ||||
| std::string HostGPIOPin::dump_summary() const { | ||||
|   char buffer[32]; | ||||
|   snprintf(buffer, sizeof(buffer), "GPIO%u", pin_); | ||||
|   return buffer; | ||||
| } | ||||
|  | ||||
| bool HostGPIOPin::digital_read() { return inverted_; } | ||||
| void HostGPIOPin::digital_write(bool value) { | ||||
|   // pass | ||||
|   ESP_LOGD(TAG, "Setting pin %d to %s", pin_, value != inverted_ ? "HIGH" : "LOW"); | ||||
| } | ||||
| void HostGPIOPin::detach_interrupt() const {} | ||||
|  | ||||
| }  // namespace host | ||||
|  | ||||
| using namespace host; | ||||
|  | ||||
| bool IRAM_ATTR ISRInternalGPIOPin::digital_read() { | ||||
|   auto *arg = reinterpret_cast<ISRPinArg *>(arg_); | ||||
|   return arg->inverted; | ||||
| } | ||||
| void IRAM_ATTR ISRInternalGPIOPin::digital_write(bool value) { | ||||
|   // pass | ||||
| } | ||||
| void IRAM_ATTR ISRInternalGPIOPin::clear_interrupt() { | ||||
|   auto *arg = reinterpret_cast<ISRPinArg *>(arg_); | ||||
|   ESP_LOGD(TAG, "Clearing interrupt for pin %d", arg->pin); | ||||
| } | ||||
|  | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif  // USE_HOST | ||||
							
								
								
									
										37
									
								
								esphome/components/host/gpio.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								esphome/components/host/gpio.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | ||||
| #pragma once | ||||
|  | ||||
| #ifdef USE_HOST | ||||
|  | ||||
| #include "esphome/core/hal.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace host { | ||||
|  | ||||
| class HostGPIOPin : public InternalGPIOPin { | ||||
|  public: | ||||
|   void set_pin(uint8_t pin) { pin_ = pin; } | ||||
|   void set_inverted(bool inverted) { inverted_ = inverted; } | ||||
|   void set_flags(gpio::Flags flags) { flags_ = flags; } | ||||
|  | ||||
|   void setup() override { pin_mode(flags_); } | ||||
|   void pin_mode(gpio::Flags flags) override; | ||||
|   bool digital_read() override; | ||||
|   void digital_write(bool value) override; | ||||
|   std::string dump_summary() const override; | ||||
|   void detach_interrupt() const override; | ||||
|   ISRInternalGPIOPin to_isr() const override; | ||||
|   uint8_t get_pin() const override { return pin_; } | ||||
|   bool is_inverted() const override { return inverted_; } | ||||
|  | ||||
|  protected: | ||||
|   void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; | ||||
|  | ||||
|   uint8_t pin_; | ||||
|   bool inverted_; | ||||
|   gpio::Flags flags_; | ||||
| }; | ||||
|  | ||||
| }  // namespace host | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif  // USE_HOST | ||||
							
								
								
									
										73
									
								
								esphome/components/host/gpio.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								esphome/components/host/gpio.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,73 @@ | ||||
| import logging | ||||
|  | ||||
| from esphome.const import ( | ||||
|     CONF_ID, | ||||
|     CONF_INPUT, | ||||
|     CONF_INVERTED, | ||||
|     CONF_MODE, | ||||
|     CONF_NUMBER, | ||||
|     CONF_OPEN_DRAIN, | ||||
|     CONF_OUTPUT, | ||||
|     CONF_PULLDOWN, | ||||
|     CONF_PULLUP, | ||||
| ) | ||||
| from esphome import pins | ||||
| import esphome.config_validation as cv | ||||
| import esphome.codegen as cg | ||||
|  | ||||
| from .const import host_ns | ||||
|  | ||||
|  | ||||
| _LOGGER = logging.getLogger(__name__) | ||||
|  | ||||
|  | ||||
| HostGPIOPin = host_ns.class_("HostGPIOPin", cg.InternalGPIOPin) | ||||
|  | ||||
|  | ||||
| def _translate_pin(value): | ||||
|     if isinstance(value, dict) or value is None: | ||||
|         raise cv.Invalid( | ||||
|             "This variable only supports pin numbers, not full pin schemas " | ||||
|             "(with inverted and mode)." | ||||
|         ) | ||||
|     if isinstance(value, int): | ||||
|         return value | ||||
|     try: | ||||
|         return int(value) | ||||
|     except ValueError: | ||||
|         pass | ||||
|     if value.startswith("GPIO"): | ||||
|         return cv.int_(value[len("GPIO") :].strip()) | ||||
|     return value | ||||
|  | ||||
|  | ||||
| def validate_gpio_pin(value): | ||||
|     return _translate_pin(value) | ||||
|  | ||||
|  | ||||
| HOST_PIN_SCHEMA = cv.All( | ||||
|     { | ||||
|         cv.GenerateID(): cv.declare_id(HostGPIOPin), | ||||
|         cv.Required(CONF_NUMBER): validate_gpio_pin, | ||||
|         cv.Optional(CONF_MODE, default={}): cv.Schema( | ||||
|             { | ||||
|                 cv.Optional(CONF_INPUT, default=False): cv.boolean, | ||||
|                 cv.Optional(CONF_OUTPUT, default=False): cv.boolean, | ||||
|                 cv.Optional(CONF_OPEN_DRAIN, default=False): cv.boolean, | ||||
|                 cv.Optional(CONF_PULLUP, default=False): cv.boolean, | ||||
|                 cv.Optional(CONF_PULLDOWN, default=False): cv.boolean, | ||||
|             } | ||||
|         ), | ||||
|         cv.Optional(CONF_INVERTED, default=False): cv.boolean, | ||||
|     }, | ||||
| ) | ||||
|  | ||||
|  | ||||
| @pins.PIN_SCHEMA_REGISTRY.register("host", HOST_PIN_SCHEMA) | ||||
| async def host_pin_to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     num = config[CONF_NUMBER] | ||||
|     cg.add(var.set_pin(num)) | ||||
|     cg.add(var.set_inverted(config[CONF_INVERTED])) | ||||
|     cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) | ||||
|     return var | ||||
							
								
								
									
										36
									
								
								esphome/components/host/preferences.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								esphome/components/host/preferences.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | ||||
| #ifdef USE_HOST | ||||
|  | ||||
| #include "preferences.h" | ||||
| #include <cstring> | ||||
| #include "esphome/core/preferences.h" | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "esphome/core/log.h" | ||||
| #include "esphome/core/defines.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace host { | ||||
|  | ||||
| static const char *const TAG = "host.preferences"; | ||||
|  | ||||
| class HostPreferences : public ESPPreferences { | ||||
|  public: | ||||
|   ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash) override { return {}; } | ||||
|  | ||||
|   ESPPreferenceObject make_preference(size_t length, uint32_t type) override { return {}; } | ||||
|  | ||||
|   bool sync() override { return true; } | ||||
|   bool reset() override { return true; } | ||||
| }; | ||||
|  | ||||
| void setup_preferences() { | ||||
|   auto *pref = new HostPreferences();  // NOLINT(cppcoreguidelines-owning-memory) | ||||
|   global_preferences = pref; | ||||
| } | ||||
|  | ||||
| }  // namespace host | ||||
|  | ||||
| ESPPreferences *global_preferences;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) | ||||
|  | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif  // USE_HOST | ||||
							
								
								
									
										13
									
								
								esphome/components/host/preferences.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								esphome/components/host/preferences.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| #pragma once | ||||
|  | ||||
| #ifdef USE_HOST | ||||
|  | ||||
| namespace esphome { | ||||
| namespace host { | ||||
|  | ||||
| void setup_preferences(); | ||||
|  | ||||
| }  // namespace host | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif  // USE_HOST | ||||
							
								
								
									
										1
									
								
								esphome/components/hyt271/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								esphome/components/hyt271/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| CODEOWNERS = ["@Philippe12"] | ||||
							
								
								
									
										52
									
								
								esphome/components/hyt271/hyt271.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								esphome/components/hyt271/hyt271.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | ||||
| #include "hyt271.h" | ||||
| #include "esphome/core/log.h" | ||||
| #include "esphome/core/hal.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace hyt271 { | ||||
|  | ||||
| static const char *const TAG = "hyt271"; | ||||
|  | ||||
| static const uint8_t HYT271_ADDRESS = 0x28; | ||||
|  | ||||
| void HYT271Component::dump_config() { | ||||
|   ESP_LOGCONFIG(TAG, "HYT271:"); | ||||
|   LOG_I2C_DEVICE(this); | ||||
|   LOG_UPDATE_INTERVAL(this); | ||||
|   LOG_SENSOR("  ", "Temperature", this->temperature_); | ||||
|   LOG_SENSOR("  ", "Humidity", this->humidity_); | ||||
| } | ||||
| void HYT271Component::update() { | ||||
|   uint8_t raw_data[4]; | ||||
|  | ||||
|   if (this->write(&raw_data[0], 0) != i2c::ERROR_OK) { | ||||
|     this->status_set_warning(); | ||||
|     ESP_LOGE(TAG, "Communication with HYT271 failed! => Ask new values"); | ||||
|     return; | ||||
|   } | ||||
|   this->set_timeout("wait_convert", 50, [this]() { | ||||
|     uint8_t raw_data[4]; | ||||
|     if (this->read(raw_data, 4) != i2c::ERROR_OK) { | ||||
|       this->status_set_warning(); | ||||
|       ESP_LOGE(TAG, "Communication with HYT271 failed! => Read values"); | ||||
|       return; | ||||
|     } | ||||
|     uint16_t raw_temperature = ((raw_data[2] << 8) | raw_data[3]) >> 2; | ||||
|     uint16_t raw_humidity = ((raw_data[0] & 0x3F) << 8) | raw_data[1]; | ||||
|  | ||||
|     float temperature = ((float(raw_temperature)) * (165.0f / 16383.0f)) - 40.0f; | ||||
|     float humidity = (float(raw_humidity)) * (100.0f / 16383.0f); | ||||
|  | ||||
|     ESP_LOGD(TAG, "Got Temperature=%.1f°C Humidity=%.1f%%", temperature, humidity); | ||||
|  | ||||
|     if (this->temperature_ != nullptr) | ||||
|       this->temperature_->publish_state(temperature); | ||||
|     if (this->humidity_ != nullptr) | ||||
|       this->humidity_->publish_state(humidity); | ||||
|     this->status_clear_warning(); | ||||
|   }); | ||||
| } | ||||
| float HYT271Component::get_setup_priority() const { return setup_priority::DATA; } | ||||
|  | ||||
| }  // namespace hyt271 | ||||
| }  // namespace esphome | ||||
							
								
								
									
										27
									
								
								esphome/components/hyt271/hyt271.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								esphome/components/hyt271/hyt271.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/components/sensor/sensor.h" | ||||
| #include "esphome/components/i2c/i2c.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace hyt271 { | ||||
|  | ||||
| class HYT271Component : public PollingComponent, public i2c::I2CDevice { | ||||
|  public: | ||||
|   void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } | ||||
|   void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } | ||||
|  | ||||
|   void dump_config() override; | ||||
|   /// Update the sensor values (temperature+humidity). | ||||
|   void update() override; | ||||
|  | ||||
|   float get_setup_priority() const override; | ||||
|  | ||||
|  protected: | ||||
|   sensor::Sensor *temperature_{nullptr}; | ||||
|   sensor::Sensor *humidity_{nullptr}; | ||||
| }; | ||||
|  | ||||
| }  // namespace hyt271 | ||||
| }  // namespace esphome | ||||
							
								
								
									
										56
									
								
								esphome/components/hyt271/sensor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								esphome/components/hyt271/sensor.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,56 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.components import i2c, sensor | ||||
| from esphome.const import ( | ||||
|     CONF_HUMIDITY, | ||||
|     CONF_ID, | ||||
|     CONF_TEMPERATURE, | ||||
|     DEVICE_CLASS_HUMIDITY, | ||||
|     DEVICE_CLASS_TEMPERATURE, | ||||
|     STATE_CLASS_MEASUREMENT, | ||||
|     UNIT_CELSIUS, | ||||
|     UNIT_PERCENT, | ||||
| ) | ||||
|  | ||||
| DEPENDENCIES = ["i2c"] | ||||
|  | ||||
| hyt271_ns = cg.esphome_ns.namespace("hyt271") | ||||
| HYT271Component = hyt271_ns.class_( | ||||
|     "HYT271Component", cg.PollingComponent, i2c.I2CDevice | ||||
| ) | ||||
|  | ||||
| CONFIG_SCHEMA = ( | ||||
|     cv.Schema( | ||||
|         { | ||||
|             cv.GenerateID(): cv.declare_id(HYT271Component), | ||||
|             cv.Required(CONF_TEMPERATURE): sensor.sensor_schema( | ||||
|                 unit_of_measurement=UNIT_CELSIUS, | ||||
|                 accuracy_decimals=1, | ||||
|                 device_class=DEVICE_CLASS_TEMPERATURE, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|             ), | ||||
|             cv.Required(CONF_HUMIDITY): sensor.sensor_schema( | ||||
|                 unit_of_measurement=UNIT_PERCENT, | ||||
|                 accuracy_decimals=1, | ||||
|                 device_class=DEVICE_CLASS_HUMIDITY, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|             ), | ||||
|         } | ||||
|     ) | ||||
|     .extend(cv.polling_component_schema("60s")) | ||||
|     .extend(i2c.i2c_device_schema(0x28)) | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     await cg.register_component(var, config) | ||||
|     await i2c.register_i2c_device(var, config) | ||||
|  | ||||
|     if CONF_TEMPERATURE in config: | ||||
|         sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) | ||||
|         cg.add(var.set_temperature(sens)) | ||||
|  | ||||
|     if CONF_HUMIDITY in config: | ||||
|         sens = await sensor.new_sensor(config[CONF_HUMIDITY]) | ||||
|         cg.add(var.set_humidity(sens)) | ||||
| @@ -42,8 +42,8 @@ I2S_PORTS = { | ||||
| CONFIG_SCHEMA = cv.Schema( | ||||
|     { | ||||
|         cv.GenerateID(): cv.declare_id(I2SAudioComponent), | ||||
|         cv.Required(CONF_I2S_BCLK_PIN): pins.internal_gpio_output_pin_number, | ||||
|         cv.Required(CONF_I2S_LRCLK_PIN): pins.internal_gpio_output_pin_number, | ||||
|         cv.Optional(CONF_I2S_BCLK_PIN): pins.internal_gpio_output_pin_number, | ||||
|     } | ||||
| ) | ||||
|  | ||||
| @@ -66,5 +66,6 @@ async def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     await cg.register_component(var, config) | ||||
|  | ||||
|     cg.add(var.set_bclk_pin(config[CONF_I2S_BCLK_PIN])) | ||||
|     cg.add(var.set_lrclk_pin(config[CONF_I2S_LRCLK_PIN])) | ||||
|     if CONF_I2S_BCLK_PIN in config: | ||||
|         cg.add(var.set_bclk_pin(config[CONF_I2S_BCLK_PIN])) | ||||
|   | ||||
| @@ -19,15 +19,6 @@ class I2SAudioComponent : public Component { | ||||
|  public: | ||||
|   void setup() override; | ||||
|  | ||||
|   void register_audio_in(I2SAudioIn *in) { | ||||
|     this->audio_in_ = in; | ||||
|     in->set_parent(this); | ||||
|   } | ||||
|   void register_audio_out(I2SAudioOut *out) { | ||||
|     this->audio_out_ = out; | ||||
|     out->set_parent(this); | ||||
|   } | ||||
|  | ||||
|   i2s_pin_config_t get_pin_config() const { | ||||
|     return { | ||||
|         .mck_io_num = I2S_PIN_NO_CHANGE, | ||||
| @@ -38,8 +29,8 @@ class I2SAudioComponent : public Component { | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   void set_bclk_pin(uint8_t pin) { this->bclk_pin_ = pin; } | ||||
|   void set_lrclk_pin(uint8_t pin) { this->lrclk_pin_ = pin; } | ||||
|   void set_bclk_pin(int pin) { this->bclk_pin_ = pin; } | ||||
|   void set_lrclk_pin(int pin) { this->lrclk_pin_ = pin; } | ||||
|  | ||||
|   void lock() { this->lock_.lock(); } | ||||
|   bool try_lock() { return this->lock_.try_lock(); } | ||||
| @@ -53,8 +44,8 @@ class I2SAudioComponent : public Component { | ||||
|   I2SAudioIn *audio_in_{nullptr}; | ||||
|   I2SAudioOut *audio_out_{nullptr}; | ||||
|  | ||||
|   uint8_t bclk_pin_; | ||||
|   uint8_t lrclk_pin_; | ||||
|   int bclk_pin_{I2S_PIN_NO_CHANGE}; | ||||
|   int lrclk_pin_; | ||||
|   i2s_port_t port_{}; | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -84,8 +84,7 @@ async def to_code(config): | ||||
|     await cg.register_component(var, config) | ||||
|     await media_player.register_media_player(var, config) | ||||
|  | ||||
|     parent = await cg.get_variable(config[CONF_I2S_AUDIO_ID]) | ||||
|     cg.add(parent.register_audio_out(var)) | ||||
|     await cg.register_parented(var, config[CONF_I2S_AUDIO_ID]) | ||||
|  | ||||
|     if config[CONF_DAC_TYPE] == "internal": | ||||
|         cg.add(var.set_internal_dac_mode(config[CONF_MODE])) | ||||
| @@ -98,5 +97,5 @@ async def to_code(config): | ||||
|  | ||||
|     cg.add_library("WiFiClientSecure", None) | ||||
|     cg.add_library("HTTPClient", None) | ||||
|     cg.add_library("esphome/ESP32-audioI2S", "2.0.6") | ||||
|     cg.add_library("esphome/ESP32-audioI2S", "2.0.7") | ||||
|     cg.add_build_flag("-DAUDIO_NO_SD_FS") | ||||
|   | ||||
| @@ -22,14 +22,14 @@ void I2SAudioMediaPlayer::control(const media_player::MediaPlayerCall &call) { | ||||
|       this->start(); | ||||
|     } | ||||
|   } | ||||
|   if (this->i2s_state_ != I2S_STATE_RUNNING) { | ||||
|     return; | ||||
|   } | ||||
|   if (call.get_volume().has_value()) { | ||||
|     this->volume = call.get_volume().value(); | ||||
|     this->set_volume_(volume); | ||||
|     this->unmute_(); | ||||
|   } | ||||
|   if (this->i2s_state_ != I2S_STATE_RUNNING) { | ||||
|     return; | ||||
|   } | ||||
|   if (call.get_command().has_value()) { | ||||
|     switch (call.get_command().value()) { | ||||
|       case media_player::MEDIA_PLAYER_COMMAND_PLAY: | ||||
| @@ -97,7 +97,8 @@ void I2SAudioMediaPlayer::unmute_() { | ||||
|   this->muted_ = false; | ||||
| } | ||||
| void I2SAudioMediaPlayer::set_volume_(float volume, bool publish) { | ||||
|   this->audio_->setVolume(remap<uint8_t, float>(volume, 0.0f, 1.0f, 0, 21)); | ||||
|   if (this->audio_ != nullptr) | ||||
|     this->audio_->setVolume(remap<uint8_t, float>(volume, 0.0f, 1.0f, 0, 21)); | ||||
|   if (publish) | ||||
|     this->volume = volume; | ||||
| } | ||||
| @@ -157,13 +158,23 @@ void I2SAudioMediaPlayer::start_() { | ||||
| #endif | ||||
|   this->i2s_state_ = I2S_STATE_RUNNING; | ||||
|   this->high_freq_.start(); | ||||
|   this->audio_->setVolume(remap<uint8_t, float>(this->volume, 0.0f, 1.0f, 0, 21)); | ||||
|   if (this->current_url_.has_value()) { | ||||
|     this->audio_->connecttohost(this->current_url_.value().c_str()); | ||||
|     this->state = media_player::MEDIA_PLAYER_STATE_PLAYING; | ||||
|     this->publish_state(); | ||||
|   } | ||||
| } | ||||
| void I2SAudioMediaPlayer::stop() { this->i2s_state_ = I2S_STATE_STOPPING; } | ||||
| void I2SAudioMediaPlayer::stop() { | ||||
|   if (this->i2s_state_ == I2S_STATE_STOPPED) { | ||||
|     return; | ||||
|   } | ||||
|   if (this->i2s_state_ == I2S_STATE_STARTING) { | ||||
|     this->i2s_state_ = I2S_STATE_STOPPED; | ||||
|     return; | ||||
|   } | ||||
|   this->i2s_state_ = I2S_STATE_STOPPING; | ||||
| } | ||||
| void I2SAudioMediaPlayer::stop_() { | ||||
|   if (this->audio_->isRunning()) { | ||||
|     this->audio_->stopSong(); | ||||
|   | ||||
| @@ -2,8 +2,9 @@ import esphome.config_validation as cv | ||||
| import esphome.codegen as cg | ||||
|  | ||||
| from esphome import pins | ||||
| from esphome.const import CONF_ID | ||||
| from esphome.components import microphone | ||||
| from esphome.const import CONF_ID, CONF_NUMBER | ||||
| from esphome.components import microphone, esp32 | ||||
| from esphome.components.adc import ESP32_VARIANT_ADC1_PIN_TO_CHANNEL, validate_adc_pin | ||||
|  | ||||
| from .. import ( | ||||
|     i2s_audio_ns, | ||||
| @@ -16,26 +17,73 @@ from .. import ( | ||||
| CODEOWNERS = ["@jesserockz"] | ||||
| DEPENDENCIES = ["i2s_audio"] | ||||
|  | ||||
| CONF_ADC_PIN = "adc_pin" | ||||
| CONF_ADC_TYPE = "adc_type" | ||||
| CONF_PDM = "pdm" | ||||
|  | ||||
| I2SAudioMicrophone = i2s_audio_ns.class_( | ||||
|     "I2SAudioMicrophone", I2SAudioIn, microphone.Microphone, cg.Component | ||||
| ) | ||||
|  | ||||
| CONFIG_SCHEMA = microphone.MICROPHONE_SCHEMA.extend( | ||||
| INTERNAL_ADC_VARIANTS = [esp32.const.VARIANT_ESP32] | ||||
| PDM_VARIANTS = [esp32.const.VARIANT_ESP32, esp32.const.VARIANT_ESP32S3] | ||||
|  | ||||
|  | ||||
| def validate_esp32_variant(config): | ||||
|     variant = esp32.get_esp32_variant() | ||||
|     if config[CONF_ADC_TYPE] == "external": | ||||
|         if config[CONF_PDM]: | ||||
|             if variant not in PDM_VARIANTS: | ||||
|                 raise cv.Invalid(f"{variant} does not support PDM") | ||||
|         return config | ||||
|     if config[CONF_ADC_TYPE] == "internal": | ||||
|         if variant not in INTERNAL_ADC_VARIANTS: | ||||
|             raise cv.Invalid(f"{variant} does not have an internal ADC") | ||||
|         return config | ||||
|     raise NotImplementedError | ||||
|  | ||||
|  | ||||
| BASE_SCHEMA = microphone.MICROPHONE_SCHEMA.extend( | ||||
|     { | ||||
|         cv.GenerateID(): cv.declare_id(I2SAudioMicrophone), | ||||
|         cv.GenerateID(CONF_I2S_AUDIO_ID): cv.use_id(I2SAudioComponent), | ||||
|         cv.Required(CONF_I2S_DIN_PIN): pins.internal_gpio_input_pin_number, | ||||
|     } | ||||
| ).extend(cv.COMPONENT_SCHEMA) | ||||
|  | ||||
| CONFIG_SCHEMA = cv.All( | ||||
|     cv.typed_schema( | ||||
|         { | ||||
|             "internal": BASE_SCHEMA.extend( | ||||
|                 { | ||||
|                     cv.Required(CONF_ADC_PIN): validate_adc_pin, | ||||
|                 } | ||||
|             ), | ||||
|             "external": BASE_SCHEMA.extend( | ||||
|                 { | ||||
|                     cv.Required(CONF_I2S_DIN_PIN): pins.internal_gpio_input_pin_number, | ||||
|                     cv.Required(CONF_PDM): cv.boolean, | ||||
|                 } | ||||
|             ), | ||||
|         }, | ||||
|         key=CONF_ADC_TYPE, | ||||
|     ), | ||||
|     validate_esp32_variant, | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     await cg.register_component(var, config) | ||||
|  | ||||
|     parent = await cg.get_variable(config[CONF_I2S_AUDIO_ID]) | ||||
|     cg.add(parent.register_audio_in(var)) | ||||
|     await cg.register_parented(var, config[CONF_I2S_AUDIO_ID]) | ||||
|  | ||||
|     cg.add(var.set_din_pin(config[CONF_I2S_DIN_PIN])) | ||||
|     if config[CONF_ADC_TYPE] == "internal": | ||||
|         variant = esp32.get_esp32_variant() | ||||
|         pin_num = config[CONF_ADC_PIN][CONF_NUMBER] | ||||
|         channel = ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant][pin_num] | ||||
|         cg.add(var.set_adc_channel(channel)) | ||||
|     else: | ||||
|         cg.add(var.set_din_pin(config[CONF_I2S_DIN_PIN])) | ||||
|         cg.add(var.set_pdm(config[CONF_PDM])) | ||||
|  | ||||
|     await microphone.register_microphone(var, config) | ||||
|   | ||||
| @@ -17,15 +17,36 @@ static const char *const TAG = "i2s_audio.microphone"; | ||||
| void I2SAudioMicrophone::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Setting up I2S Audio Microphone..."); | ||||
|   this->buffer_.resize(BUFFER_SIZE); | ||||
|  | ||||
| #if SOC_I2S_SUPPORTS_ADC | ||||
|   if (this->adc_) { | ||||
|     if (this->parent_->get_port() != I2S_NUM_0) { | ||||
|       ESP_LOGE(TAG, "Internal ADC only works on I2S0!"); | ||||
|       this->mark_failed(); | ||||
|       return; | ||||
|     } | ||||
|   } else | ||||
| #endif | ||||
|       if (this->pdm_) { | ||||
|     if (this->parent_->get_port() != I2S_NUM_0) { | ||||
|       ESP_LOGE(TAG, "PDM only works on I2S0!"); | ||||
|       this->mark_failed(); | ||||
|       return; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| void I2SAudioMicrophone::start() { this->state_ = microphone::STATE_STARTING; } | ||||
| void I2SAudioMicrophone::start() { | ||||
|   if (this->is_failed()) | ||||
|     return; | ||||
|   this->state_ = microphone::STATE_STARTING; | ||||
| } | ||||
| void I2SAudioMicrophone::start_() { | ||||
|   if (!this->parent_->try_lock()) { | ||||
|     return;  // Waiting for another i2s to return lock | ||||
|   } | ||||
|   i2s_driver_config_t config = { | ||||
|       .mode = (i2s_mode_t) (I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_PDM), | ||||
|       .mode = (i2s_mode_t) (I2S_MODE_MASTER | I2S_MODE_RX), | ||||
|       .sample_rate = 16000, | ||||
|       .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, | ||||
|       .channel_format = I2S_CHANNEL_FMT_ONLY_RIGHT, | ||||
| @@ -40,19 +61,38 @@ void I2SAudioMicrophone::start_() { | ||||
|       .bits_per_chan = I2S_BITS_PER_CHAN_DEFAULT, | ||||
|   }; | ||||
|  | ||||
|   i2s_driver_install(this->parent_->get_port(), &config, 0, nullptr); | ||||
| #if SOC_I2S_SUPPORTS_ADC | ||||
|   if (this->adc_) { | ||||
|     config.mode = (i2s_mode_t) (config.mode | I2S_MODE_ADC_BUILT_IN); | ||||
|     i2s_driver_install(this->parent_->get_port(), &config, 0, nullptr); | ||||
|  | ||||
|   i2s_pin_config_t pin_config = this->parent_->get_pin_config(); | ||||
|   pin_config.data_in_num = this->din_pin_; | ||||
|     i2s_set_adc_mode(ADC_UNIT_1, this->adc_channel_); | ||||
|     i2s_adc_enable(this->parent_->get_port()); | ||||
|   } else { | ||||
| #endif | ||||
|     if (this->pdm_) | ||||
|       config.mode = (i2s_mode_t) (config.mode | I2S_MODE_PDM); | ||||
|  | ||||
|   i2s_set_pin(this->parent_->get_port(), &pin_config); | ||||
|     i2s_driver_install(this->parent_->get_port(), &config, 0, nullptr); | ||||
|  | ||||
|     i2s_pin_config_t pin_config = this->parent_->get_pin_config(); | ||||
|     pin_config.data_in_num = this->din_pin_; | ||||
|  | ||||
|     i2s_set_pin(this->parent_->get_port(), &pin_config); | ||||
| #if SOC_I2S_SUPPORTS_ADC | ||||
|   } | ||||
| #endif | ||||
|   this->state_ = microphone::STATE_RUNNING; | ||||
|   this->high_freq_.start(); | ||||
| } | ||||
|  | ||||
| void I2SAudioMicrophone::stop() { | ||||
|   if (this->state_ == microphone::STATE_STOPPED) | ||||
|   if (this->state_ == microphone::STATE_STOPPED || this->is_failed()) | ||||
|     return; | ||||
|   if (this->state_ == microphone::STATE_STARTING) { | ||||
|     this->state_ = microphone::STATE_STOPPED; | ||||
|     return; | ||||
|   } | ||||
|   this->state_ = microphone::STATE_STOPPING; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -18,14 +18,27 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub | ||||
|  | ||||
|   void loop() override; | ||||
|  | ||||
|   void set_din_pin(uint8_t pin) { this->din_pin_ = pin; } | ||||
|   void set_din_pin(int8_t pin) { this->din_pin_ = pin; } | ||||
|   void set_pdm(bool pdm) { this->pdm_ = pdm; } | ||||
|  | ||||
| #if SOC_I2S_SUPPORTS_ADC | ||||
|   void set_adc_channel(adc1_channel_t channel) { | ||||
|     this->adc_channel_ = channel; | ||||
|     this->adc_ = true; | ||||
|   } | ||||
| #endif | ||||
|  | ||||
|  protected: | ||||
|   void start_(); | ||||
|   void stop_(); | ||||
|   void read_(); | ||||
|  | ||||
|   uint8_t din_pin_{0}; | ||||
|   int8_t din_pin_{I2S_PIN_NO_CHANGE}; | ||||
| #if SOC_I2S_SUPPORTS_ADC | ||||
|   adc1_channel_t adc_channel_{ADC1_CHANNEL_MAX}; | ||||
|   bool adc_{false}; | ||||
| #endif | ||||
|   bool pdm_{false}; | ||||
|   std::vector<uint8_t> buffer_; | ||||
|  | ||||
|   HighFrequencyLoopRequester high_freq_; | ||||
|   | ||||
							
								
								
									
										87
									
								
								esphome/components/i2s_audio/speaker/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								esphome/components/i2s_audio/speaker/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,87 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome import pins | ||||
| from esphome.const import CONF_ID, CONF_MODE | ||||
| from esphome.components import esp32, speaker | ||||
|  | ||||
| from .. import ( | ||||
|     CONF_I2S_AUDIO_ID, | ||||
|     CONF_I2S_DOUT_PIN, | ||||
|     I2SAudioComponent, | ||||
|     I2SAudioOut, | ||||
|     i2s_audio_ns, | ||||
| ) | ||||
|  | ||||
| CODEOWNERS = ["@jesserockz"] | ||||
| DEPENDENCIES = ["i2s_audio"] | ||||
|  | ||||
| I2SAudioSpeaker = i2s_audio_ns.class_( | ||||
|     "I2SAudioSpeaker", cg.Component, speaker.Speaker, I2SAudioOut | ||||
| ) | ||||
|  | ||||
| i2s_dac_mode_t = cg.global_ns.enum("i2s_dac_mode_t") | ||||
|  | ||||
| CONF_MUTE_PIN = "mute_pin" | ||||
| CONF_DAC_TYPE = "dac_type" | ||||
|  | ||||
| INTERNAL_DAC_OPTIONS = { | ||||
|     "left": i2s_dac_mode_t.I2S_DAC_CHANNEL_LEFT_EN, | ||||
|     "right": i2s_dac_mode_t.I2S_DAC_CHANNEL_RIGHT_EN, | ||||
|     "stereo": i2s_dac_mode_t.I2S_DAC_CHANNEL_BOTH_EN, | ||||
| } | ||||
|  | ||||
| EXTERNAL_DAC_OPTIONS = ["mono", "stereo"] | ||||
|  | ||||
| NO_INTERNAL_DAC_VARIANTS = [esp32.const.VARIANT_ESP32S2] | ||||
|  | ||||
|  | ||||
| def validate_esp32_variant(config): | ||||
|     if config[CONF_DAC_TYPE] != "internal": | ||||
|         return config | ||||
|     variant = esp32.get_esp32_variant() | ||||
|     if variant in NO_INTERNAL_DAC_VARIANTS: | ||||
|         raise cv.Invalid(f"{variant} does not have an internal DAC") | ||||
|     return config | ||||
|  | ||||
|  | ||||
| CONFIG_SCHEMA = cv.All( | ||||
|     cv.typed_schema( | ||||
|         { | ||||
|             "internal": speaker.SPEAKER_SCHEMA.extend( | ||||
|                 { | ||||
|                     cv.GenerateID(): cv.declare_id(I2SAudioSpeaker), | ||||
|                     cv.GenerateID(CONF_I2S_AUDIO_ID): cv.use_id(I2SAudioComponent), | ||||
|                     cv.Required(CONF_MODE): cv.enum(INTERNAL_DAC_OPTIONS, lower=True), | ||||
|                 } | ||||
|             ).extend(cv.COMPONENT_SCHEMA), | ||||
|             "external": speaker.SPEAKER_SCHEMA.extend( | ||||
|                 { | ||||
|                     cv.GenerateID(): cv.declare_id(I2SAudioSpeaker), | ||||
|                     cv.GenerateID(CONF_I2S_AUDIO_ID): cv.use_id(I2SAudioComponent), | ||||
|                     cv.Required( | ||||
|                         CONF_I2S_DOUT_PIN | ||||
|                     ): pins.internal_gpio_output_pin_number, | ||||
|                     cv.Optional(CONF_MODE, default="mono"): cv.one_of( | ||||
|                         *EXTERNAL_DAC_OPTIONS, lower=True | ||||
|                     ), | ||||
|                 } | ||||
|             ).extend(cv.COMPONENT_SCHEMA), | ||||
|         }, | ||||
|         key=CONF_DAC_TYPE, | ||||
|     ), | ||||
|     validate_esp32_variant, | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     await cg.register_component(var, config) | ||||
|     await speaker.register_speaker(var, config) | ||||
|  | ||||
|     await cg.register_parented(var, config[CONF_I2S_AUDIO_ID]) | ||||
|  | ||||
|     if config[CONF_DAC_TYPE] == "internal": | ||||
|         cg.add(var.set_internal_dac_mode(config[CONF_MODE])) | ||||
|     else: | ||||
|         cg.add(var.set_dout_pin(config[CONF_I2S_DOUT_PIN])) | ||||
|         cg.add(var.set_external_dac_channels(2 if config[CONF_MODE] == "stereo" else 1)) | ||||
							
								
								
									
										212
									
								
								esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										212
									
								
								esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,212 @@ | ||||
| #include "i2s_audio_speaker.h" | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|  | ||||
| #include <driver/i2s.h> | ||||
|  | ||||
| #include "esphome/core/application.h" | ||||
| #include "esphome/core/hal.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace i2s_audio { | ||||
|  | ||||
| static const size_t BUFFER_COUNT = 10; | ||||
|  | ||||
| static const char *const TAG = "i2s_audio.speaker"; | ||||
|  | ||||
| void I2SAudioSpeaker::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Setting up I2S Audio Speaker..."); | ||||
|  | ||||
|   this->buffer_queue_ = xQueueCreate(BUFFER_COUNT, sizeof(DataEvent)); | ||||
|   this->event_queue_ = xQueueCreate(20, sizeof(TaskEvent)); | ||||
| } | ||||
|  | ||||
| void I2SAudioSpeaker::start() { this->state_ = speaker::STATE_STARTING; } | ||||
| void I2SAudioSpeaker::start_() { | ||||
|   if (!this->parent_->try_lock()) { | ||||
|     return;  // Waiting for another i2s component to return lock | ||||
|   } | ||||
|   this->state_ = speaker::STATE_RUNNING; | ||||
|  | ||||
|   xTaskCreate(I2SAudioSpeaker::player_task, "speaker_task", 8192, (void *) this, 0, &this->player_task_handle_); | ||||
| } | ||||
|  | ||||
| void I2SAudioSpeaker::player_task(void *params) { | ||||
|   I2SAudioSpeaker *this_speaker = (I2SAudioSpeaker *) params; | ||||
|  | ||||
|   TaskEvent event; | ||||
|   event.type = TaskEventType::STARTING; | ||||
|   xQueueSend(this_speaker->event_queue_, &event, portMAX_DELAY); | ||||
|  | ||||
|   i2s_driver_config_t config = { | ||||
|       .mode = (i2s_mode_t) (I2S_MODE_MASTER | I2S_MODE_TX), | ||||
|       .sample_rate = 16000, | ||||
|       .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, | ||||
|       .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT, | ||||
|       .communication_format = I2S_COMM_FORMAT_STAND_I2S, | ||||
|       .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, | ||||
|       .dma_buf_count = 8, | ||||
|       .dma_buf_len = 1024, | ||||
|       .use_apll = false, | ||||
|       .tx_desc_auto_clear = true, | ||||
|       .fixed_mclk = I2S_PIN_NO_CHANGE, | ||||
|       .mclk_multiple = I2S_MCLK_MULTIPLE_DEFAULT, | ||||
|       .bits_per_chan = I2S_BITS_PER_CHAN_DEFAULT, | ||||
|   }; | ||||
| #if SOC_I2S_SUPPORTS_DAC | ||||
|   if (this_speaker->internal_dac_mode_ != I2S_DAC_CHANNEL_DISABLE) { | ||||
|     config.mode = (i2s_mode_t) (config.mode | I2S_MODE_DAC_BUILT_IN); | ||||
|   } | ||||
| #endif | ||||
|  | ||||
|   i2s_driver_install(this_speaker->parent_->get_port(), &config, 0, nullptr); | ||||
|  | ||||
| #if SOC_I2S_SUPPORTS_DAC | ||||
|   if (this_speaker->internal_dac_mode_ == I2S_DAC_CHANNEL_DISABLE) { | ||||
| #endif | ||||
|     i2s_pin_config_t pin_config = this_speaker->parent_->get_pin_config(); | ||||
|     pin_config.data_out_num = this_speaker->dout_pin_; | ||||
|  | ||||
|     i2s_set_pin(this_speaker->parent_->get_port(), &pin_config); | ||||
| #if SOC_I2S_SUPPORTS_DAC | ||||
|   } else { | ||||
|     i2s_set_dac_mode(this_speaker->internal_dac_mode_); | ||||
|   } | ||||
| #endif | ||||
|  | ||||
|   DataEvent data_event; | ||||
|  | ||||
|   event.type = TaskEventType::STARTED; | ||||
|   xQueueSend(this_speaker->event_queue_, &event, portMAX_DELAY); | ||||
|  | ||||
|   int16_t buffer[BUFFER_SIZE / 2]; | ||||
|  | ||||
|   while (true) { | ||||
|     if (xQueueReceive(this_speaker->buffer_queue_, &data_event, 100 / portTICK_PERIOD_MS) != pdTRUE) { | ||||
|       break;  // End of audio from main thread | ||||
|     } | ||||
|     if (data_event.stop) { | ||||
|       // Stop signal from main thread | ||||
|       while (xQueueReceive(this_speaker->buffer_queue_, &data_event, 0) == pdTRUE) { | ||||
|         // Flush queue | ||||
|       } | ||||
|       break; | ||||
|     } | ||||
|     size_t bytes_written; | ||||
|  | ||||
|     memmove(buffer, data_event.data, data_event.len); | ||||
|     size_t remaining = data_event.len / 2; | ||||
|     size_t current = 0; | ||||
|  | ||||
|     while (remaining > 0) { | ||||
|       uint32_t sample = (buffer[current] << 16) | (buffer[current] & 0xFFFF); | ||||
|  | ||||
|       esp_err_t err = i2s_write(this_speaker->parent_->get_port(), &sample, sizeof(sample), &bytes_written, | ||||
|                                 (100 / portTICK_PERIOD_MS)); | ||||
|       if (err != ESP_OK) { | ||||
|         event = {.type = TaskEventType::WARNING, .err = err}; | ||||
|         xQueueSend(this_speaker->event_queue_, &event, portMAX_DELAY); | ||||
|         continue; | ||||
|       } | ||||
|       remaining--; | ||||
|       current++; | ||||
|     } | ||||
|  | ||||
|     event.type = TaskEventType::PLAYING; | ||||
|     xQueueSend(this_speaker->event_queue_, &event, portMAX_DELAY); | ||||
|   } | ||||
|  | ||||
|   i2s_zero_dma_buffer(this_speaker->parent_->get_port()); | ||||
|  | ||||
|   event.type = TaskEventType::STOPPING; | ||||
|   xQueueSend(this_speaker->event_queue_, &event, portMAX_DELAY); | ||||
|  | ||||
|   i2s_stop(this_speaker->parent_->get_port()); | ||||
|   i2s_driver_uninstall(this_speaker->parent_->get_port()); | ||||
|  | ||||
|   event.type = TaskEventType::STOPPED; | ||||
|   xQueueSend(this_speaker->event_queue_, &event, portMAX_DELAY); | ||||
|  | ||||
|   while (true) { | ||||
|     delay(10); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void I2SAudioSpeaker::stop() { | ||||
|   if (this->state_ == speaker::STATE_STOPPED) | ||||
|     return; | ||||
|   if (this->state_ == speaker::STATE_STARTING) { | ||||
|     this->state_ = speaker::STATE_STOPPED; | ||||
|     return; | ||||
|   } | ||||
|   this->state_ = speaker::STATE_STOPPING; | ||||
|   DataEvent data; | ||||
|   data.stop = true; | ||||
|   xQueueSendToFront(this->buffer_queue_, &data, portMAX_DELAY); | ||||
| } | ||||
|  | ||||
| void I2SAudioSpeaker::watch_() { | ||||
|   TaskEvent event; | ||||
|   if (xQueueReceive(this->event_queue_, &event, 0) == pdTRUE) { | ||||
|     switch (event.type) { | ||||
|       case TaskEventType::STARTING: | ||||
|       case TaskEventType::STARTED: | ||||
|       case TaskEventType::STOPPING: | ||||
|         break; | ||||
|       case TaskEventType::PLAYING: | ||||
|         this->status_clear_warning(); | ||||
|         break; | ||||
|       case TaskEventType::STOPPED: | ||||
|         this->parent_->unlock(); | ||||
|         this->state_ = speaker::STATE_STOPPED; | ||||
|         vTaskDelete(this->player_task_handle_); | ||||
|         this->player_task_handle_ = nullptr; | ||||
|         break; | ||||
|       case TaskEventType::WARNING: | ||||
|         ESP_LOGW(TAG, "Error writing to I2S: %s", esp_err_to_name(event.err)); | ||||
|         this->status_set_warning(); | ||||
|         break; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| void I2SAudioSpeaker::loop() { | ||||
|   switch (this->state_) { | ||||
|     case speaker::STATE_STARTING: | ||||
|       this->start_(); | ||||
|       break; | ||||
|     case speaker::STATE_RUNNING: | ||||
|       this->watch_(); | ||||
|       break; | ||||
|     case speaker::STATE_STOPPING: | ||||
|     case speaker::STATE_STOPPED: | ||||
|       break; | ||||
|   } | ||||
| } | ||||
|  | ||||
| bool I2SAudioSpeaker::play(const uint8_t *data, size_t length) { | ||||
|   if (this->state_ != speaker::STATE_RUNNING && this->state_ != speaker::STATE_STARTING) { | ||||
|     this->start(); | ||||
|   } | ||||
|   size_t remaining = length; | ||||
|   size_t index = 0; | ||||
|   while (remaining > 0) { | ||||
|     DataEvent event; | ||||
|     event.stop = false; | ||||
|     size_t to_send_length = std::min(remaining, BUFFER_SIZE); | ||||
|     event.len = to_send_length; | ||||
|     memcpy(event.data, data + index, to_send_length); | ||||
|     if (xQueueSend(this->buffer_queue_, &event, 100 / portTICK_PERIOD_MS) == pdTRUE) { | ||||
|       remaining -= to_send_length; | ||||
|       index += to_send_length; | ||||
|     } | ||||
|     App.feed_wdt(); | ||||
|   } | ||||
|   return true; | ||||
| } | ||||
|  | ||||
| }  // namespace i2s_audio | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif  // USE_ESP32 | ||||
							
								
								
									
										81
									
								
								esphome/components/i2s_audio/speaker/i2s_audio_speaker.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								esphome/components/i2s_audio/speaker/i2s_audio_speaker.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,81 @@ | ||||
| #pragma once | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|  | ||||
| #include "../i2s_audio.h" | ||||
|  | ||||
| #include <driver/i2s.h> | ||||
| #include <freertos/FreeRTOS.h> | ||||
| #include <freertos/queue.h> | ||||
|  | ||||
| #include "esphome/components/speaker/speaker.h" | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/gpio.h" | ||||
| #include "esphome/core/helpers.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace i2s_audio { | ||||
|  | ||||
| static const size_t BUFFER_SIZE = 1024; | ||||
|  | ||||
| enum class TaskEventType : uint8_t { | ||||
|   STARTING = 0, | ||||
|   STARTED, | ||||
|   PLAYING, | ||||
|   STOPPING, | ||||
|   STOPPED, | ||||
|   WARNING = 255, | ||||
| }; | ||||
|  | ||||
| struct TaskEvent { | ||||
|   TaskEventType type; | ||||
|   esp_err_t err; | ||||
| }; | ||||
|  | ||||
| struct DataEvent { | ||||
|   bool stop; | ||||
|   size_t len; | ||||
|   uint8_t data[BUFFER_SIZE]; | ||||
| }; | ||||
|  | ||||
| class I2SAudioSpeaker : public Component, public speaker::Speaker, public I2SAudioOut { | ||||
|  public: | ||||
|   float get_setup_priority() const override { return esphome::setup_priority::LATE; } | ||||
|  | ||||
|   void setup() override; | ||||
|   void loop() override; | ||||
|  | ||||
|   void set_dout_pin(uint8_t pin) { this->dout_pin_ = pin; } | ||||
| #if SOC_I2S_SUPPORTS_DAC | ||||
|   void set_internal_dac_mode(i2s_dac_mode_t mode) { this->internal_dac_mode_ = mode; } | ||||
| #endif | ||||
|   void set_external_dac_channels(uint8_t channels) { this->external_dac_channels_ = channels; } | ||||
|  | ||||
|   void start(); | ||||
|   void stop() override; | ||||
|  | ||||
|   bool play(const uint8_t *data, size_t length) override; | ||||
|  | ||||
|  protected: | ||||
|   void start_(); | ||||
|   // void stop_(); | ||||
|   void watch_(); | ||||
|  | ||||
|   static void player_task(void *params); | ||||
|  | ||||
|   TaskHandle_t player_task_handle_{nullptr}; | ||||
|   QueueHandle_t buffer_queue_; | ||||
|   QueueHandle_t event_queue_; | ||||
|  | ||||
|   uint8_t dout_pin_{0}; | ||||
|  | ||||
| #if SOC_I2S_SUPPORTS_DAC | ||||
|   i2s_dac_mode_t internal_dac_mode_{I2S_DAC_CHANNEL_DISABLE}; | ||||
| #endif | ||||
|   uint8_t external_dac_channels_; | ||||
| }; | ||||
|  | ||||
| }  // namespace i2s_audio | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif  // USE_ESP32 | ||||
| @@ -84,9 +84,18 @@ void ILI9XXXDisplay::fill(Color color) { | ||||
|       break; | ||||
|     case BITS_16: | ||||
|       new_color = display::ColorUtil::color_to_565(color); | ||||
|       for (uint32_t i = 0; i < this->get_buffer_length_() * 2; i = i + 2) { | ||||
|         this->buffer_[i] = (uint8_t) (new_color >> 8); | ||||
|         this->buffer_[i + 1] = (uint8_t) new_color; | ||||
|       { | ||||
|         const uint32_t buffer_length_16_bits = this->get_buffer_length_() * 2; | ||||
|         if (((uint8_t) (new_color >> 8)) == ((uint8_t) new_color)) { | ||||
|           // Upper and lower is equal can use quicker memset operation. Takes ~20ms. | ||||
|           memset(this->buffer_, (uint8_t) new_color, buffer_length_16_bits); | ||||
|         } else { | ||||
|           // Slower set of both buffers. Takes ~30ms. | ||||
|           for (uint32_t i = 0; i < buffer_length_16_bits; i = i + 2) { | ||||
|             this->buffer_[i] = (uint8_t) (new_color >> 8); | ||||
|             this->buffer_[i + 1] = (uint8_t) new_color; | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|       return; | ||||
|       break; | ||||
|   | ||||
| @@ -33,7 +33,7 @@ CONFIG_SCHEMA = cv.All( | ||||
|     cv.COMPONENT_SCHEMA.extend( | ||||
|         { | ||||
|             cv.GenerateID(): cv.declare_id(KeyCollector), | ||||
|             cv.GenerateID(CONF_SOURCE_ID): cv.use_id(key_provider.KeyProvider), | ||||
|             cv.Optional(CONF_SOURCE_ID): cv.use_id(key_provider.KeyProvider), | ||||
|             cv.Optional(CONF_MIN_LENGTH): cv.int_, | ||||
|             cv.Optional(CONF_MAX_LENGTH): cv.int_, | ||||
|             cv.Optional(CONF_START_KEYS): cv.string, | ||||
| @@ -55,8 +55,9 @@ CONFIG_SCHEMA = cv.All( | ||||
| async def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     await cg.register_component(var, config) | ||||
|     source = await cg.get_variable(config[CONF_SOURCE_ID]) | ||||
|     cg.add(var.set_provider(source)) | ||||
|     if CONF_SOURCE_ID in config: | ||||
|         source = await cg.get_variable(config[CONF_SOURCE_ID]) | ||||
|         cg.add(var.set_provider(source)) | ||||
|     if CONF_MIN_LENGTH in config: | ||||
|         cg.add(var.set_min_length(config[CONF_MIN_LENGTH])) | ||||
|     if CONF_MAX_LENGTH in config: | ||||
|   | ||||
| @@ -52,6 +52,8 @@ void KeyCollector::clear(bool progress_update) { | ||||
|     this->progress_trigger_->trigger(this->result_, 0); | ||||
| } | ||||
|  | ||||
| void KeyCollector::send_key(uint8_t key) { this->key_pressed_(key); } | ||||
|  | ||||
| void KeyCollector::key_pressed_(uint8_t key) { | ||||
|   this->last_key_time_ = millis(); | ||||
|   if (!this->start_keys_.empty() && !this->start_key_) { | ||||
|   | ||||
| @@ -27,6 +27,7 @@ class KeyCollector : public Component { | ||||
|   void set_timeout(int timeout) { this->timeout_ = timeout; }; | ||||
|  | ||||
|   void clear(bool progress_update = true); | ||||
|   void send_key(uint8_t key); | ||||
|  | ||||
|  protected: | ||||
|   void key_pressed_(uint8_t key); | ||||
|   | ||||
| @@ -25,7 +25,7 @@ class PulseLightEffect : public LightEffect { | ||||
|       return; | ||||
|     } | ||||
|     auto call = this->state_->turn_on(); | ||||
|     float out = this->on_ ? 1.0 : 0.0; | ||||
|     float out = this->on_ ? this->max_brightness : this->min_brightness; | ||||
|     call.set_brightness_if_supported(out); | ||||
|     this->on_ = !this->on_; | ||||
|     call.set_transition_length_if_supported(this->transition_length_); | ||||
| @@ -41,11 +41,18 @@ class PulseLightEffect : public LightEffect { | ||||
|  | ||||
|   void set_update_interval(uint32_t update_interval) { this->update_interval_ = update_interval; } | ||||
|  | ||||
|   void set_min_max_brightness(float min, float max) { | ||||
|     this->min_brightness = min; | ||||
|     this->max_brightness = max; | ||||
|   } | ||||
|  | ||||
|  protected: | ||||
|   bool on_ = false; | ||||
|   uint32_t last_color_change_{0}; | ||||
|   uint32_t transition_length_{}; | ||||
|   uint32_t update_interval_{}; | ||||
|   float min_brightness{0.0}; | ||||
|   float max_brightness{1.0}; | ||||
| }; | ||||
|  | ||||
| /// Random effect. Sets random colors every 10 seconds and slowly transitions between them. | ||||
|   | ||||
| @@ -28,6 +28,8 @@ from esphome.const import ( | ||||
|     CONF_NUM_LEDS, | ||||
|     CONF_RANDOM, | ||||
|     CONF_SEQUENCE, | ||||
|     CONF_MAX_BRIGHTNESS, | ||||
|     CONF_MIN_BRIGHTNESS, | ||||
| ) | ||||
| from esphome.util import Registry | ||||
| from .types import ( | ||||
| @@ -174,12 +176,19 @@ async def automation_effect_to_code(config, effect_id): | ||||
|         cv.Optional( | ||||
|             CONF_UPDATE_INTERVAL, default="1s" | ||||
|         ): cv.positive_time_period_milliseconds, | ||||
|         cv.Optional(CONF_MIN_BRIGHTNESS, default="0%"): cv.percentage, | ||||
|         cv.Optional(CONF_MAX_BRIGHTNESS, default="100%"): cv.percentage, | ||||
|     }, | ||||
| ) | ||||
| async def pulse_effect_to_code(config, effect_id): | ||||
|     effect = cg.new_Pvariable(effect_id, config[CONF_NAME]) | ||||
|     cg.add(effect.set_transition_length(config[CONF_TRANSITION_LENGTH])) | ||||
|     cg.add(effect.set_update_interval(config[CONF_UPDATE_INTERVAL])) | ||||
|     cg.add( | ||||
|         effect.set_min_max_brightness( | ||||
|             config[CONF_MIN_BRIGHTNESS], config[CONF_MAX_BRIGHTNESS] | ||||
|         ) | ||||
|     ) | ||||
|     return effect | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -17,6 +17,9 @@ from esphome.const import ( | ||||
|     CONF_TAG, | ||||
|     CONF_TRIGGER_ID, | ||||
|     CONF_TX_BUFFER_SIZE, | ||||
|     PLATFORM_ESP32, | ||||
|     PLATFORM_ESP8266, | ||||
|     PLATFORM_RP2040, | ||||
| ) | ||||
| from esphome.core import CORE, EsphomeError, Lambda, coroutine_with_priority | ||||
| from esphome.components.esp32 import add_idf_sdkconfig_option, get_esp32_variant | ||||
| @@ -141,7 +144,10 @@ CONFIG_SCHEMA = cv.All( | ||||
|                 esp8266=UART0, | ||||
|                 esp32=UART0, | ||||
|                 rp2040=USB_CDC, | ||||
|             ): uart_selection, | ||||
|             ): cv.All( | ||||
|                 cv.only_on([PLATFORM_ESP8266, PLATFORM_ESP32, PLATFORM_RP2040]), | ||||
|                 uart_selection, | ||||
|             ), | ||||
|             cv.Optional(CONF_LEVEL, default="DEBUG"): is_log_level, | ||||
|             cv.Optional(CONF_LOGS, default={}): cv.Schema( | ||||
|                 { | ||||
|   | ||||
| @@ -145,6 +145,9 @@ void HOT Logger::log_message_(int level, const char *tag, int offset) { | ||||
|   if (xPortGetFreeHeapSize() < 2048) | ||||
|     return; | ||||
| #endif | ||||
| #ifdef USE_HOST | ||||
|   puts(msg); | ||||
| #endif | ||||
|  | ||||
|   this->log_callback_.call(level, tag, msg); | ||||
| } | ||||
| @@ -262,7 +265,11 @@ void Logger::set_baud_rate(uint32_t baud_rate) { this->baud_rate_ = baud_rate; } | ||||
| void Logger::set_log_level(const std::string &tag, int log_level) { | ||||
|   this->log_levels_.push_back(LogLevelOverride{tag, log_level}); | ||||
| } | ||||
|  | ||||
| #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) | ||||
| UARTSelection Logger::get_uart() const { return this->uart_; } | ||||
| #endif | ||||
|  | ||||
| void Logger::add_on_log_callback(std::function<void(int, const char *, const char *)> &&callback) { | ||||
|   this->log_callback_.add(std::move(callback)); | ||||
| } | ||||
| @@ -294,7 +301,10 @@ void Logger::dump_config() { | ||||
|   ESP_LOGCONFIG(TAG, "Logger:"); | ||||
|   ESP_LOGCONFIG(TAG, "  Level: %s", LOG_LEVELS[ESPHOME_LOG_LEVEL]); | ||||
|   ESP_LOGCONFIG(TAG, "  Log Baud Rate: %" PRIu32, this->baud_rate_); | ||||
| #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) | ||||
|   ESP_LOGCONFIG(TAG, "  Hardware UART: %s", UART_SELECTIONS[this->uart_]); | ||||
| #endif | ||||
|  | ||||
|   for (auto &it : this->log_levels_) { | ||||
|     ESP_LOGCONFIG(TAG, "  Level for '%s': %s", it.tag.c_str(), LOG_LEVELS[it.level]); | ||||
|   } | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user