diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index be82fc826b..b858b40e6f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -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] diff --git a/CODEOWNERS b/CODEOWNERS index 80d493dcc2..8b347460b2 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -84,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 @@ -95,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 @@ -107,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 @@ -139,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 @@ -189,6 +195,7 @@ esphome/components/number/* @esphome/core esphome/components/optolink/* @j0ta29 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 @@ -235,7 +242,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 @@ -243,6 +250,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 diff --git a/docker/Dockerfile b/docker/Dockerfile index a59a470394..720241242f 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -26,7 +26,7 @@ RUN \ 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 \ @@ -63,7 +63,7 @@ RUN \ 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 ======================= diff --git a/esphome/components/adc/__init__.py b/esphome/components/adc/__init__.py index f70ffa9520..cceaa594ef 100644 --- a/esphome/components/adc/__init__.py +++ b/esphome/components/adc/__init__.py @@ -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 diff --git a/esphome/components/adc/sensor.py b/esphome/components/adc/sensor.py index 1a519d7506..4695e96570 100644 --- a/esphome/components/adc/sensor.py +++ b/esphome/components/adc/sensor.py @@ -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": diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index a79444a7e9..013b46695d 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -978,6 +978,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 @@ -996,7 +998,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; } diff --git a/esphome/components/async_tcp/__init__.py b/esphome/components/async_tcp/__init__.py index c693544a2e..1d127623f1 100644 --- a/esphome/components/async_tcp/__init__.py +++ b/esphome/components/async_tcp/__init__.py @@ -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") diff --git a/esphome/components/ble_client/__init__.py b/esphome/components/ble_client/__init__.py index 03e8f0b0b2..8f70ad3417 100644 --- a/esphome/components/ble_client/__init__.py +++ b/esphome/components/ble_client/__init__.py @@ -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) diff --git a/esphome/components/ble_client/automation.h b/esphome/components/ble_client/automation.h index 45ddba9782..423f74b85a 100644 --- a/esphome/components/ble_client/automation.h +++ b/esphome/components/ble_client/automation.h @@ -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, 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, 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 class BLEClientWriteAction : public Action, publ std::function(Ts...)> value_template_{}; }; +template class BLEClientPasskeyReplyAction : public Action { + 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 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 value_template_{}; +}; + +template class BLEClientNumericComparisonReplyAction : public Action { + 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 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 value_template_{}; +}; + +template class BLEClientRemoveBondAction : public Action { + 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 diff --git a/esphome/components/ble_client/ble_client.h b/esphome/components/ble_client/ble_client.h index ceca94c86a..e04f4a8042 100644 --- a/esphome/components/ble_client/ble_client.h +++ b/esphome/components/ble_client/ble_client.h @@ -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; } diff --git a/esphome/components/dfplayer/__init__.py b/esphome/components/dfplayer/__init__.py index caa05c27b5..5ea04b4804 100644 --- a/esphome/components/dfplayer/__init__.py +++ b/esphome/components/dfplayer/__init__.py @@ -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, diff --git a/esphome/components/dfplayer/dfplayer.cpp b/esphome/components/dfplayer/dfplayer.cpp index e16479570f..a6339dc988 100644 --- a/esphome/components/dfplayer/dfplayer.cpp +++ b/esphome/components/dfplayer/dfplayer.cpp @@ -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 { diff --git a/esphome/components/dfplayer/dfplayer.h b/esphome/components/dfplayer/dfplayer.h index ae47cb33f1..26e90fd410 100644 --- a/esphome/components/dfplayer/dfplayer.h +++ b/esphome/components/dfplayer/dfplayer.h @@ -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 class PlayMp3Action : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint16_t, file) + + void play(Ts... x) override { + auto file = this->file_.value(x...); + this->parent_->play_mp3(file); + } +}; + template class PlayFileAction : public Action, public Parented { public: TEMPLATABLE_VALUE(uint16_t, file) diff --git a/esphome/components/display/display_buffer.cpp b/esphome/components/display/display_buffer.cpp index 19751e7355..f4e7785b5e 100644 --- a/esphome/components/display/display_buffer.cpp +++ b/esphome/components/display/display_buffer.cpp @@ -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); + } } } } diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index 7db6fff6b9..f508cecb87 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -9,6 +9,7 @@ CODEOWNERS = ["@jesserockz"] CONFLICTS_WITH = ["esp32_ble_beacon"] CONF_BLE_ID = "ble_id" +CONF_IO_CAPABILITY = "io_capability" NO_BLUETOOTH_VARIANTS = [const.VARIANT_ESP32S2] @@ -19,10 +20,21 @@ 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) @@ -39,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) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index 502399f97a..21ec005e07 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -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"); } diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index 5970b43688..11ae826544 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -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 ble_events_; BLEAdvertising *advertising_; + esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE}; }; // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/esp32_rmt_led_strip/__init__.py b/esphome/components/esp32_rmt_led_strip/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/esp32_rmt_led_strip/led_strip.cpp b/esphome/components/esp32_rmt_led_strip/led_strip.cpp new file mode 100644 index 0000000000..eec1bdc992 --- /dev/null +++ b/esphome/components/esp32_rmt_led_strip/led_strip.cpp @@ -0,0 +1,207 @@ +#include "led_strip.h" + +#ifdef USE_ESP32 + +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +#include + +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 allocator(ExternalRAMAllocator::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_allocator(ExternalRAMAllocator::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 diff --git a/esphome/components/esp32_rmt_led_strip/led_strip.h b/esphome/components/esp32_rmt_led_strip/led_strip.h new file mode 100644 index 0000000000..508f784ec8 --- /dev/null +++ b/esphome/components/esp32_rmt_led_strip/led_strip.h @@ -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 +#include +#include + +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 max_refresh_rate_{}; +}; + +} // namespace esp32_rmt_led_strip +} // namespace esphome + +#endif // USE_ESP32 diff --git a/esphome/components/esp32_rmt_led_strip/light.py b/esphome/components/esp32_rmt_led_strip/light.py new file mode 100644 index 0000000000..3ca758c1e1 --- /dev/null +++ b/esphome/components/esp32_rmt_led_strip/light.py @@ -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]}") + ) + ) diff --git a/esphome/components/ezo/ezo.cpp b/esphome/components/ezo/ezo.cpp index 9d4343e004..8e4486dbf2 100644 --- a/esphome/components/ezo/ezo.cpp +++ b/esphome/components/ezo/ezo.cpp @@ -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(&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(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: { diff --git a/esphome/components/gp8403/__init__.py b/esphome/components/gp8403/__init__.py new file mode 100644 index 0000000000..a7a2b46f58 --- /dev/null +++ b/esphome/components/gp8403/__init__.py @@ -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])) diff --git a/esphome/components/gp8403/gp8403.cpp b/esphome/components/gp8403/gp8403.cpp new file mode 100644 index 0000000000..7a08a18a8f --- /dev/null +++ b/esphome/components/gp8403/gp8403.cpp @@ -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 diff --git a/esphome/components/gp8403/gp8403.h b/esphome/components/gp8403/gp8403.h new file mode 100644 index 0000000000..65182ef301 --- /dev/null +++ b/esphome/components/gp8403/gp8403.h @@ -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 diff --git a/esphome/components/gp8403/output/__init__.py b/esphome/components/gp8403/output/__init__.py new file mode 100644 index 0000000000..1cf95ac6e5 --- /dev/null +++ b/esphome/components/gp8403/output/__init__.py @@ -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])) diff --git a/esphome/components/gp8403/output/gp8403_output.cpp b/esphome/components/gp8403/output/gp8403_output.cpp new file mode 100644 index 0000000000..ff73bb4627 --- /dev/null +++ b/esphome/components/gp8403/output/gp8403_output.cpp @@ -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 diff --git a/esphome/components/gp8403/output/gp8403_output.h b/esphome/components/gp8403/output/gp8403_output.h new file mode 100644 index 0000000000..71e5efb1cb --- /dev/null +++ b/esphome/components/gp8403/output/gp8403_output.h @@ -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 { + 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 diff --git a/esphome/components/host/__init__.py b/esphome/components/host/__init__.py new file mode 100644 index 0000000000..46f763d255 --- /dev/null +++ b/esphome/components/host/__init__.py @@ -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") diff --git a/esphome/components/host/const.py b/esphome/components/host/const.py new file mode 100644 index 0000000000..b6f4c4e277 --- /dev/null +++ b/esphome/components/host/const.py @@ -0,0 +1,5 @@ +import esphome.codegen as cg + +KEY_HOST = "host" + +host_ns = cg.esphome_ns.namespace("host") diff --git a/esphome/components/host/core.cpp b/esphome/components/host/core.cpp new file mode 100644 index 0000000000..164d622dd4 --- /dev/null +++ b/esphome/components/host/core.cpp @@ -0,0 +1,77 @@ +#ifdef USE_HOST + +#include "esphome/core/hal.h" +#include "esphome/core/helpers.h" +#include "preferences.h" + +#include +#include +#include +#include + +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 diff --git a/esphome/components/host/gpio.cpp b/esphome/components/host/gpio.cpp new file mode 100644 index 0000000000..e46f158513 --- /dev/null +++ b/esphome/components/host/gpio.cpp @@ -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(arg_); + return arg->inverted; +} +void IRAM_ATTR ISRInternalGPIOPin::digital_write(bool value) { + // pass +} +void IRAM_ATTR ISRInternalGPIOPin::clear_interrupt() { + auto *arg = reinterpret_cast(arg_); + ESP_LOGD(TAG, "Clearing interrupt for pin %d", arg->pin); +} + +} // namespace esphome + +#endif // USE_HOST diff --git a/esphome/components/host/gpio.h b/esphome/components/host/gpio.h new file mode 100644 index 0000000000..c0920467d6 --- /dev/null +++ b/esphome/components/host/gpio.h @@ -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 diff --git a/esphome/components/host/gpio.py b/esphome/components/host/gpio.py new file mode 100644 index 0000000000..d523d28ee5 --- /dev/null +++ b/esphome/components/host/gpio.py @@ -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 diff --git a/esphome/components/host/preferences.cpp b/esphome/components/host/preferences.cpp new file mode 100644 index 0000000000..bf45893e40 --- /dev/null +++ b/esphome/components/host/preferences.cpp @@ -0,0 +1,36 @@ +#ifdef USE_HOST + +#include "preferences.h" +#include +#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 diff --git a/esphome/components/host/preferences.h b/esphome/components/host/preferences.h new file mode 100644 index 0000000000..7462360ec3 --- /dev/null +++ b/esphome/components/host/preferences.h @@ -0,0 +1,13 @@ +#pragma once + +#ifdef USE_HOST + +namespace esphome { +namespace host { + +void setup_preferences(); + +} // namespace host +} // namespace esphome + +#endif // USE_HOST diff --git a/esphome/components/hyt271/__init__.py b/esphome/components/hyt271/__init__.py new file mode 100644 index 0000000000..2e88d4f366 --- /dev/null +++ b/esphome/components/hyt271/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@Philippe12"] diff --git a/esphome/components/hyt271/hyt271.cpp b/esphome/components/hyt271/hyt271.cpp new file mode 100644 index 0000000000..94558fff04 --- /dev/null +++ b/esphome/components/hyt271/hyt271.cpp @@ -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 diff --git a/esphome/components/hyt271/hyt271.h b/esphome/components/hyt271/hyt271.h new file mode 100644 index 0000000000..64f32a651c --- /dev/null +++ b/esphome/components/hyt271/hyt271.h @@ -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 diff --git a/esphome/components/hyt271/sensor.py b/esphome/components/hyt271/sensor.py new file mode 100644 index 0000000000..2ec2836461 --- /dev/null +++ b/esphome/components/hyt271/sensor.py @@ -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)) diff --git a/esphome/components/i2s_audio/__init__.py b/esphome/components/i2s_audio/__init__.py index 1773d3082f..39d81ef1a1 100644 --- a/esphome/components/i2s_audio/__init__.py +++ b/esphome/components/i2s_audio/__init__.py @@ -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])) diff --git a/esphome/components/i2s_audio/i2s_audio.h b/esphome/components/i2s_audio/i2s_audio.h index 6b3fa10f3c..f030ed4e75 100644 --- a/esphome/components/i2s_audio/i2s_audio.h +++ b/esphome/components/i2s_audio/i2s_audio.h @@ -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_{}; }; diff --git a/esphome/components/i2s_audio/media_player/__init__.py b/esphome/components/i2s_audio/media_player/__init__.py index 4ccb9cfc0a..dfc3fb2be2 100644 --- a/esphome/components/i2s_audio/media_player/__init__.py +++ b/esphome/components/i2s_audio/media_player/__init__.py @@ -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") diff --git a/esphome/components/i2s_audio/media_player/i2s_audio_media_player.cpp b/esphome/components/i2s_audio/media_player/i2s_audio_media_player.cpp index 64f83a5ea6..2e9ded601d 100644 --- a/esphome/components/i2s_audio/media_player/i2s_audio_media_player.cpp +++ b/esphome/components/i2s_audio/media_player/i2s_audio_media_player.cpp @@ -141,7 +141,7 @@ void I2SAudioMediaPlayer::start_() { this->audio_ = make_unique