diff --git a/.github/workflows/ci-docker.yml b/.github/workflows/ci-docker.yml index 1d2a1b5323..eb3a5a945c 100644 --- a/.github/workflows/ci-docker.yml +++ b/.github/workflows/ci-docker.yml @@ -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 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d22c2b7e03..affdf944a7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,6 +23,7 @@ jobs: runs-on: ubuntu-latest strategy: fail-fast: false + max-parallel: 5 matrix: include: - id: ci-custom diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3904834dc9..7ebd04e793 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 diff --git a/.github/workflows/sync-device-classes.yml b/.github/workflows/sync-device-classes.yml index 671fe1f21a..396dd64165 100644 --- a/.github/workflows/sync-device-classes.yml +++ b/.github/workflows/sync-device-classes.yml @@ -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 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 8e606d253a..de6488c3d3 100644 --- a/CODEOWNERS +++ b/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 diff --git a/docker/Dockerfile b/docker/Dockerfile index 383c73565d..720241242f 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -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 ======================= diff --git a/docker/platformio_install_deps.py b/docker/platformio_install_deps.py deleted file mode 100755 index c7b11cf321..0000000000 --- a/docker/platformio_install_deps.py +++ /dev/null @@ -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]) 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/am43/__init__.py b/esphome/components/am43/__init__.py index e69de29bb2..f21a15ce0a 100644 --- a/esphome/components/am43/__init__.py +++ b/esphome/components/am43/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@buxtronix"] diff --git a/esphome/components/am43/cover/__init__.py b/esphome/components/am43/cover/__init__.py index 79eeb2eef3..103ac809e6 100644 --- a/esphome/components/am43/cover/__init__.py +++ b/esphome/components/am43/cover/__init__.py @@ -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) diff --git a/esphome/components/am43/cover/am43_cover.cpp b/esphome/components/am43/cover/am43_cover.cpp index d0ef4a2fbb..93c77ea364 100644 --- a/esphome/components/am43/cover/am43_cover.cpp +++ b/esphome/components/am43/cover/am43_cover.cpp @@ -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); diff --git a/esphome/components/am43/sensor.py b/esphome/components/am43/sensor/__init__.py similarity index 83% rename from esphome/components/am43/sensor.py rename to esphome/components/am43/sensor/__init__.py index 68c85d0e9c..01588f2299 100644 --- a/esphome/components/am43/sensor.py +++ b/esphome/components/am43/sensor/__init__.py @@ -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)) diff --git a/esphome/components/am43/am43.cpp b/esphome/components/am43/sensor/am43_sensor.cpp similarity index 99% rename from esphome/components/am43/am43.cpp rename to esphome/components/am43/sensor/am43_sensor.cpp index 09723496d9..008c7768ed 100644 --- a/esphome/components/am43/am43.cpp +++ b/esphome/components/am43/sensor/am43_sensor.cpp @@ -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 diff --git a/esphome/components/am43/am43.h b/esphome/components/am43/sensor/am43_sensor.h similarity index 100% rename from esphome/components/am43/am43.h rename to esphome/components/am43/sensor/am43_sensor.h diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index f31ef3ffc0..4cc98c91d9 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -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; diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 96fb3ea9fa..013b46695d 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -530,7 +530,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(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 +590,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(msg.fan_mode)); if (msg.has_custom_fan_mode) @@ -944,7 +941,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 +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 @@ -999,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/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 334cde16b3..1dd8c82e00 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -941,6 +941,10 @@ bool ListEntitiesCoverResponse::decode_varint(uint32_t field_id, ProtoVarInt val this->entity_category = value.as_enum(); 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(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(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(8, this->action); buffer.encode_enum(9, this->fan_mode); buffer.encode_enum(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(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: "); diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 9f71c07913..0f4b79de19 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -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}; 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/binary_sensor/binary_sensor.cpp b/esphome/components/binary_sensor/binary_sensor.cpp index bd33b2af2d..20604a0b7e 100644 --- a/esphome/components/binary_sensor/binary_sensor.cpp +++ b/esphome/components/binary_sensor/binary_sensor.cpp @@ -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) { diff --git a/esphome/components/binary_sensor/binary_sensor.h b/esphome/components/binary_sensor/binary_sensor.h index 0bf8cf2cdc..301a472810 100644 --- a/esphome/components/binary_sensor/binary_sensor.h +++ b/esphome/components/binary_sensor/binary_sensor.h @@ -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 &filters); @@ -82,7 +76,6 @@ class BinarySensor : public EntityBase { protected: CallbackManager state_callback_{}; - optional device_class_{}; ///< Stores the override of the device class Filter *filter_list_{nullptr}; bool has_state_{false}; bool publish_initial_state_{false}; diff --git a/esphome/components/binary_sensor_map/binary_sensor_map.cpp b/esphome/components/binary_sensor_map/binary_sensor_map.cpp index 3934e0a99c..0bf6202893 100644 --- a/esphome/components/binary_sensor_map/binary_sensor_map.cpp +++ b/esphome/components/binary_sensor_map/binary_sensor_map.cpp @@ -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 diff --git a/esphome/components/binary_sensor_map/binary_sensor_map.h b/esphome/components/binary_sensor_map/binary_sensor_map.h index a1d6f95009..a07154c0e8 100644 --- a/esphome/components/binary_sensor_map/binary_sensor_map.h +++ b/esphome/components/binary_sensor_map/binary_sensor_map.h @@ -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 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 diff --git a/esphome/components/binary_sensor_map/sensor.py b/esphome/components/binary_sensor_map/sensor.py index 573cce9223..1181905f30 100644 --- a/esphome/components/binary_sensor_map/sensor.py +++ b/esphome/components/binary_sensor_map/sensor.py @@ -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])) 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/button/button.cpp b/esphome/components/button/button.cpp index dfa417de7b..4c4cb7740c 100644 --- a/esphome/components/button/button.cpp +++ b/esphome/components/button/button.cpp @@ -13,8 +13,5 @@ void Button::press() { } void Button::add_on_press_callback(std::function &&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 diff --git a/esphome/components/button/button.h b/esphome/components/button/button.h index a4902810b2..9488eca221 100644 --- a/esphome/components/button/button.h +++ b/esphome/components/button/button.h @@ -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 &&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 press_callback_{}; - std::string device_class_{}; }; } // namespace button diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py index 6734917bf3..bf167fe837 100644 --- a/esphome/components/climate/__init__.py +++ b/esphome/components/climate/__init__.py @@ -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_)) diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index b4d5ee9685..a032596eb3 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -264,25 +264,11 @@ const optional &ClimateCall::get_mode() const { return this->mode_; const optional &ClimateCall::get_target_temperature() const { return this->target_temperature_; } const optional &ClimateCall::get_target_temperature_low() const { return this->target_temperature_low_; } const optional &ClimateCall::get_target_temperature_high() const { return this->target_temperature_high_; } -optional ClimateCall::get_away() const { - if (!this->preset_.has_value()) - return {}; - return *this->preset_ == ClimatePreset::CLIMATE_PRESET_AWAY; -} const optional &ClimateCall::get_fan_mode() const { return this->fan_mode_; } const optional &ClimateCall::get_custom_fan_mode() const { return this->custom_fan_mode_; } const optional &ClimateCall::get_preset() const { return this->preset_; } const optional &ClimateCall::get_custom_preset() const { return this->custom_preset_; } const optional &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 away) { - if (away.has_value()) - this->preset_ = *away ? CLIMATE_PRESET_AWAY : CLIMATE_PRESET_HOME; - return *this; -} ClimateCall &ClimateCall::set_target_temperature_high(optional target_temperature_high) { this->target_temperature_high_ = target_temperature_high; return *this; diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index 43bd71657d..656e1c4852 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -64,10 +64,6 @@ class ClimateCall { * For climate devices with two point target temperature control */ ClimateCall &set_target_temperature_high(optional 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 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 &get_target_temperature() const; const optional &get_target_temperature_low() const; const optional &get_target_temperature_high() const; - ESPDEPRECATED("get_away() is deprecated, please use .get_preset() instead", "v1.20") - optional get_away() const; const optional &get_fan_mode() const; const optional &get_swing_mode() const; const optional &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 fan_mode; diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index ffbd8c5ae0..e8c2db6c06 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -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 modes) { supported_swing_modes_ = std::move(modes); } void add_supported_swing_mode(ClimateSwingMode mode) { supported_swing_modes_.insert(mode); } diff --git a/esphome/components/copy/cover/copy_cover.cpp b/esphome/components/copy/cover/copy_cover.cpp index cf50473018..28f8c9877c 100644 --- a/esphome/components/copy/cover/copy_cover.cpp +++ b/esphome/components/copy/cover/copy_cover.cpp @@ -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()); diff --git a/esphome/components/cover/cover.cpp b/esphome/components/cover/cover.cpp index 24dd88b698..d139bab8ee 100644 --- a/esphome/components/cover/cover.cpp +++ b/esphome/components/cover/cover.cpp @@ -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 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; } diff --git a/esphome/components/cover/cover.h b/esphome/components/cover/cover.h index c6a420fa97..d21fbe02be 100644 --- a/esphome/components/cover/cover.h +++ b/esphome/components/cover/cover.h @@ -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 restore_state_(); CallbackManager state_callback_{}; - optional device_class_override_{}; ESPPreferenceObject rtc_; }; diff --git a/esphome/components/cover/cover_traits.h b/esphome/components/cover/cover_traits.h index fb30883f77..79001c3b03 100644 --- a/esphome/components/cover/cover_traits.h +++ b/esphome/components/cover/cover_traits.h @@ -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 diff --git a/esphome/components/current_based/current_based_cover.cpp b/esphome/components/current_based/current_based_cover.cpp index 7edbdf5a72..ff5ad43997 100644 --- a/esphome/components/current_based/current_based_cover.cpp +++ b/esphome/components/current_based/current_based_cover.cpp @@ -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); diff --git a/esphome/components/dashboard_import/__init__.py b/esphome/components/dashboard_import/__init__.py index 6b6750cbf4..e0994be6a0 100644 --- a/esphome/components/dashboard_import/__init__.py +++ b/esphome/components/dashboard_import/__init__.py @@ -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: diff --git a/esphome/components/demo/demo_cover.h b/esphome/components/demo/demo_cover.h index ab039736fb..ec266d46ab 100644 --- a/esphome/components/demo/demo_cover.h +++ b/esphome/components/demo/demo_cover.h @@ -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; 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/endstop/endstop_cover.cpp b/esphome/components/endstop/endstop_cover.cpp index f468d13492..1190acc46b 100644 --- a/esphome/components/endstop/endstop_cover.cpp +++ b/esphome/components/endstop/endstop_cover.cpp @@ -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); diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index d0f74b7226..3ca140f0d4 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -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") diff --git a/esphome/components/esp32/core.cpp b/esphome/components/esp32/core.cpp index b47392bc6b..512a8857b6 100644 --- a/esphome/components/esp32/core.cpp +++ b/esphome/components/esp32/core.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #if ESP_IDF_VERSION_MAJOR >= 4 diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index f59d944dcf..f508cecb87 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -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) 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/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index 59a1f2cd85..674f433d52 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -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: 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/feedback/feedback_cover.cpp b/esphome/components/feedback/feedback_cover.cpp index 213ce7ff8f..117c626f58 100644 --- a/esphome/components/feedback/feedback_cover.cpp +++ b/esphome/components/feedback/feedback_cover.cpp @@ -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_); 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/microphone/__init__.py b/esphome/components/i2s_audio/microphone/__init__.py index 5abc13a044..48d4d28f8e 100644 --- a/esphome/components/i2s_audio/microphone/__init__.py +++ b/esphome/components/i2s_audio/microphone/__init__.py @@ -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) diff --git a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp index 8883cdc665..2b38853528 100644 --- a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp +++ b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp @@ -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,18 +61,33 @@ 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; this->state_ = microphone::STATE_STOPPING; } diff --git a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h index a36cb7340c..e704ed2915 100644 --- a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h +++ b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h @@ -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 buffer_; HighFrequencyLoopRequester high_freq_; diff --git a/esphome/components/i2s_audio/speaker/__init__.py b/esphome/components/i2s_audio/speaker/__init__.py new file mode 100644 index 0000000000..72455af1b7 --- /dev/null +++ b/esphome/components/i2s_audio/speaker/__init__.py @@ -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)) diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp new file mode 100644 index 0000000000..fa41a70277 --- /dev/null +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp @@ -0,0 +1,208 @@ +#include "i2s_audio_speaker.h" + +#ifdef USE_ESP32 + +#include + +#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; + 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 diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h new file mode 100644 index 0000000000..4f1d2172d7 --- /dev/null +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h @@ -0,0 +1,81 @@ +#pragma once + +#ifdef USE_ESP32 + +#include "../i2s_audio.h" + +#include +#include +#include + +#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 diff --git a/esphome/components/ili9xxx/ili9xxx_display.cpp b/esphome/components/ili9xxx/ili9xxx_display.cpp index 1b5248fa29..67d643fe31 100644 --- a/esphome/components/ili9xxx/ili9xxx_display.cpp +++ b/esphome/components/ili9xxx/ili9xxx_display.cpp @@ -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; diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 9f0fa22fc0..af96b03c8e 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -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( { diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index c77e280711..8fd39265fd 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -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 &&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]); } diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 88d5e8ee97..54a5236cd8 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -25,6 +25,7 @@ namespace esphome { namespace logger { +#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) /** Enum for logging UART selection * * Advanced configuration (pin selection, etc) is not supported. @@ -52,6 +53,7 @@ enum UARTSelection { UART_SELECTION_USB_CDC, #endif // USE_RP2040 }; +#endif // USE_ESP32 || USE_ESP8266 class Logger : public Component { public: @@ -66,10 +68,11 @@ class Logger : public Component { #ifdef USE_ESP_IDF uart_port_t get_uart_num() const { return uart_num_; } #endif - +#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) void set_uart_selection(UARTSelection uart_selection) { uart_ = uart_selection; } /// Get the UART used by the logger. UARTSelection get_uart() const; +#endif /// Set the log level of the specified tag. void set_log_level(const std::string &tag, int log_level); @@ -139,7 +142,9 @@ class Logger : public Component { char *tx_buffer_{nullptr}; int tx_buffer_at_{0}; int tx_buffer_size_{0}; +#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) UARTSelection uart_{UART_SELECTION_UART0}; +#endif #ifdef USE_ARDUINO Stream *hw_serial_{nullptr}; #endif diff --git a/esphome/components/max6956/__init__.py b/esphome/components/max6956/__init__.py new file mode 100644 index 0000000000..77e0d37e76 --- /dev/null +++ b/esphome/components/max6956/__init__.py @@ -0,0 +1,148 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins, automation +from esphome.components import i2c +from esphome.const import ( + CONF_ID, + CONF_NUMBER, + CONF_MODE, + CONF_INVERTED, + CONF_INPUT, + CONF_OUTPUT, + CONF_PULLUP, +) + +CODEOWNERS = ["@looping40"] + +DEPENDENCIES = ["i2c"] +MULTI_CONF = True + +CONF_BRIGHTNESS_MODE = "brightness_mode" +CONF_BRIGHTNESS_GLOBAL = "brightness_global" + + +max6956_ns = cg.esphome_ns.namespace("max6956") + +MAX6956 = max6956_ns.class_("MAX6956", cg.Component, i2c.I2CDevice) +MAX6956GPIOPin = max6956_ns.class_("MAX6956GPIOPin", cg.GPIOPin) + +# Actions +SetCurrentGlobalAction = max6956_ns.class_("SetCurrentGlobalAction", automation.Action) +SetCurrentModeAction = max6956_ns.class_("SetCurrentModeAction", automation.Action) + +MAX6956_CURRENTMODE = max6956_ns.enum("MAX6956CURRENTMODE") +CURRENT_MODES = { + "global": MAX6956_CURRENTMODE.GLOBAL, + "segment": MAX6956_CURRENTMODE.SEGMENT, +} + + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.Required(CONF_ID): cv.declare_id(MAX6956), + cv.Optional(CONF_BRIGHTNESS_GLOBAL, default="0"): cv.int_range( + min=0, max=15 + ), + cv.Optional(CONF_BRIGHTNESS_MODE, default="global"): cv.enum( + CURRENT_MODES, lower=True + ), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(0x40)) +) + + +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_brightness_mode(config[CONF_BRIGHTNESS_MODE])) + cg.add(var.set_brightness_global(config[CONF_BRIGHTNESS_GLOBAL])) + + +def validate_mode(value): + if not (value[CONF_INPUT] or value[CONF_OUTPUT]): + raise cv.Invalid("Mode must be either input or output") + if value[CONF_INPUT] and value[CONF_OUTPUT]: + raise cv.Invalid("Mode must be either input or output") + if value[CONF_PULLUP] and not value[CONF_INPUT]: + raise cv.Invalid("Pullup only available with input") + return value + + +CONF_MAX6956 = "max6956" + +MAX6956_PIN_SCHEMA = cv.All( + { + cv.GenerateID(): cv.declare_id(MAX6956GPIOPin), + cv.Required(CONF_MAX6956): cv.use_id(MAX6956), + cv.Required(CONF_NUMBER): cv.int_range(min=4, max=31), + cv.Optional(CONF_MODE, default={}): cv.All( + { + cv.Optional(CONF_INPUT, default=False): cv.boolean, + cv.Optional(CONF_PULLUP, default=False): cv.boolean, + cv.Optional(CONF_OUTPUT, default=False): cv.boolean, + }, + validate_mode, + ), + cv.Optional(CONF_INVERTED, default=False): cv.boolean, + } +) + + +@pins.PIN_SCHEMA_REGISTRY.register(CONF_MAX6956, MAX6956_PIN_SCHEMA) +async def max6956_pin_to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + parent = await cg.get_variable(config[CONF_MAX6956]) + + cg.add(var.set_parent(parent)) + + 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 + + +@automation.register_action( + "max6956.set_brightness_global", + SetCurrentGlobalAction, + cv.maybe_simple_value( + { + cv.GenerateID(CONF_ID): cv.use_id(MAX6956), + cv.Required(CONF_BRIGHTNESS_GLOBAL): cv.templatable( + cv.int_range(min=0, max=15) + ), + }, + key=CONF_BRIGHTNESS_GLOBAL, + ), +) +async def max6956_set_brightness_global_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) + template_ = await cg.templatable(config[CONF_BRIGHTNESS_GLOBAL], args, float) + cg.add(var.set_brightness_global(template_)) + return var + + +@automation.register_action( + "max6956.set_brightness_mode", + SetCurrentModeAction, + cv.maybe_simple_value( + { + cv.Required(CONF_ID): cv.use_id(MAX6956), + cv.Required(CONF_BRIGHTNESS_MODE): cv.templatable( + cv.enum(CURRENT_MODES, lower=True) + ), + }, + key=CONF_BRIGHTNESS_MODE, + ), +) +async def max6956_set_brightness_mode_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) + template_ = await cg.templatable(config[CONF_BRIGHTNESS_MODE], args, float) + cg.add(var.set_brightness_mode(template_)) + return var diff --git a/esphome/components/max6956/automation.h b/esphome/components/max6956/automation.h new file mode 100644 index 0000000000..c0b491dc7f --- /dev/null +++ b/esphome/components/max6956/automation.h @@ -0,0 +1,40 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/max6956/max6956.h" + +namespace esphome { +namespace max6956 { + +template class SetCurrentGlobalAction : public Action { + public: + SetCurrentGlobalAction(MAX6956 *max6956) : max6956_(max6956) {} + + TEMPLATABLE_VALUE(uint8_t, brightness_global) + + void play(Ts... x) override { + this->max6956_->set_brightness_global(this->brightness_global_.value(x...)); + this->max6956_->write_brightness_global(); + } + + protected: + MAX6956 *max6956_; +}; + +template class SetCurrentModeAction : public Action { + public: + SetCurrentModeAction(MAX6956 *max6956) : max6956_(max6956) {} + + TEMPLATABLE_VALUE(max6956::MAX6956CURRENTMODE, brightness_mode) + + void play(Ts... x) override { + this->max6956_->set_brightness_mode(this->brightness_mode_.value(x...)); + this->max6956_->write_brightness_mode(); + } + + protected: + MAX6956 *max6956_; +}; +} // namespace max6956 +} // namespace esphome diff --git a/esphome/components/max6956/max6956.cpp b/esphome/components/max6956/max6956.cpp new file mode 100644 index 0000000000..c2d9ba0175 --- /dev/null +++ b/esphome/components/max6956/max6956.cpp @@ -0,0 +1,170 @@ +#include "max6956.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace max6956 { + +static const char *const TAG = "max6956"; + +/// Masks for MAX6956 Configuration register +const uint32_t MASK_TRANSITION_DETECTION = 0x80; +const uint32_t MASK_INDIVIDUAL_CURRENT = 0x40; +const uint32_t MASK_NORMAL_OPERATION = 0x01; + +const uint32_t MASK_1PORT_VALUE = 0x03; +const uint32_t MASK_PORT_CONFIG = 0x03; +const uint8_t MASK_CONFIG_CURRENT = 0x40; +const uint8_t MASK_CURRENT_PIN = 0x0F; + +/************************************** + * MAX6956 * + **************************************/ +void MAX6956::setup() { + ESP_LOGCONFIG(TAG, "Setting up MAX6956..."); + uint8_t configuration; + if (!this->read_reg_(MAX6956_CONFIGURATION, &configuration)) { + this->mark_failed(); + return; + } + + write_brightness_global(); + write_brightness_mode(); + + /** TO DO : read transition detection in yaml + TO DO : read indivdual current in yaml **/ + this->read_reg_(MAX6956_CONFIGURATION, &configuration); + ESP_LOGD(TAG, "Initial reg[0x%.2X]=0x%.2X", MAX6956_CONFIGURATION, configuration); + configuration = configuration | MASK_NORMAL_OPERATION; + this->write_reg_(MAX6956_CONFIGURATION, configuration); + + ESP_LOGCONFIG(TAG, "Enabling normal operation"); + ESP_LOGD(TAG, "setup reg[0x%.2X]=0x%.2X", MAX6956_CONFIGURATION, configuration); +} + +bool MAX6956::digital_read(uint8_t pin) { + uint8_t reg_addr = MAX6956_1PORT_VALUE_START + pin; + uint8_t value = 0; + this->read_reg_(reg_addr, &value); + return (value & MASK_1PORT_VALUE); +} + +void MAX6956::digital_write(uint8_t pin, bool value) { + uint8_t reg_addr = MAX6956_1PORT_VALUE_START + pin; + this->write_reg_(reg_addr, value); +} + +void MAX6956::pin_mode(uint8_t pin, gpio::Flags flags) { + uint8_t reg_addr = MAX6956_PORT_CONFIG_START + (pin - MAX6956_MIN) / 4; + uint8_t config = 0; + uint8_t shift = 2 * (pin % 4); + MAX6956GPIOMode mode = MAX6956_INPUT; + + if (flags == gpio::FLAG_INPUT) { + mode = MAX6956GPIOMode::MAX6956_INPUT; + } else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_PULLUP)) { + mode = MAX6956GPIOMode::MAX6956_INPUT_PULLUP; + } else if (flags == gpio::FLAG_OUTPUT) { + mode = MAX6956GPIOMode::MAX6956_OUTPUT; + } + + this->read_reg_(reg_addr, &config); + config &= ~(MASK_PORT_CONFIG << shift); + config |= (mode << shift); + this->write_reg_(reg_addr, config); +} + +void MAX6956::pin_mode(uint8_t pin, max6956::MAX6956GPIOFlag flags) { + uint8_t reg_addr = MAX6956_PORT_CONFIG_START + (pin - MAX6956_MIN) / 4; + uint8_t config = 0; + uint8_t shift = 2 * (pin % 4); + MAX6956GPIOMode mode = MAX6956GPIOMode::MAX6956_LED; + + if (flags == max6956::FLAG_LED) { + mode = MAX6956GPIOMode::MAX6956_LED; + } + + this->read_reg_(reg_addr, &config); + config &= ~(MASK_PORT_CONFIG << shift); + config |= (mode << shift); + this->write_reg_(reg_addr, config); +} + +void MAX6956::set_brightness_global(uint8_t current) { + if (current > 15) { + ESP_LOGE(TAG, "Global brightness out off range (%u)", current); + return; + } + global_brightness_ = current; +} + +void MAX6956::write_brightness_global() { this->write_reg_(MAX6956_GLOBAL_CURRENT, global_brightness_); } + +void MAX6956::set_brightness_mode(max6956::MAX6956CURRENTMODE brightness_mode) { brightness_mode_ = brightness_mode; }; + +void MAX6956::write_brightness_mode() { + uint8_t reg_addr = MAX6956_CONFIGURATION; + uint8_t config = 0; + + this->read_reg_(reg_addr, &config); + config &= ~MASK_CONFIG_CURRENT; + config |= brightness_mode_ << 6; + this->write_reg_(reg_addr, config); +} + +void MAX6956::set_pin_brightness(uint8_t pin, float brightness) { + uint8_t reg_addr = MAX6956_CURRENT_START + (pin - MAX6956_MIN) / 2; + uint8_t config = 0; + uint8_t shift = 4 * (pin % 2); + uint8_t bright = roundf(brightness * 15); + + if (prev_bright_[pin - MAX6956_MIN] == bright) + return; + + prev_bright_[pin - MAX6956_MIN] = bright; + + this->read_reg_(reg_addr, &config); + config &= ~(MASK_CURRENT_PIN << shift); + config |= (bright << shift); + this->write_reg_(reg_addr, config); +} + +bool MAX6956::read_reg_(uint8_t reg, uint8_t *value) { + if (this->is_failed()) + return false; + + return this->read_byte(reg, value); +} + +bool MAX6956::write_reg_(uint8_t reg, uint8_t value) { + if (this->is_failed()) + return false; + + return this->write_byte(reg, value); +} + +void MAX6956::dump_config() { + ESP_LOGCONFIG(TAG, "MAX6956"); + + if (brightness_mode_ == MAX6956CURRENTMODE::GLOBAL) { + ESP_LOGCONFIG(TAG, "current mode: global"); + ESP_LOGCONFIG(TAG, "global brightness: %u", global_brightness_); + } else { + ESP_LOGCONFIG(TAG, "current mode: segment"); + } +} + +/************************************** + * MAX6956GPIOPin * + **************************************/ +void MAX6956GPIOPin::setup() { pin_mode(flags_); } +void MAX6956GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } +bool MAX6956GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } +void MAX6956GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); } +std::string MAX6956GPIOPin::dump_summary() const { + char buffer[32]; + snprintf(buffer, sizeof(buffer), "%u via Max6956", pin_); + return buffer; +} + +} // namespace max6956 +} // namespace esphome diff --git a/esphome/components/max6956/max6956.h b/esphome/components/max6956/max6956.h new file mode 100644 index 0000000000..141164ab30 --- /dev/null +++ b/esphome/components/max6956/max6956.h @@ -0,0 +1,94 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace max6956 { + +/// Modes for MAX6956 pins +enum MAX6956GPIOMode : uint8_t { + MAX6956_LED = 0x00, + MAX6956_OUTPUT = 0x01, + MAX6956_INPUT = 0x02, + MAX6956_INPUT_PULLUP = 0x03 +}; + +/// Range for MAX6956 pins +enum MAX6956GPIORange : uint8_t { + MAX6956_MIN = 4, + MAX6956_MAX = 31, +}; + +enum MAX6956GPIORegisters { + MAX6956_GLOBAL_CURRENT = 0x02, + MAX6956_CONFIGURATION = 0x04, + MAX6956_TRANSITION_DETECT_MASK = 0x06, + MAX6956_DISPLAY_TEST = 0x07, + MAX6956_PORT_CONFIG_START = 0x09, // Port Configuration P7, P6, P5, P4 + MAX6956_CURRENT_START = 0x12, // Current054 + MAX6956_1PORT_VALUE_START = 0x20, // Port 0 only (virtual port, no action) + MAX6956_8PORTS_VALUE_START = 0x44, // 8 ports 4–11 (data bits D0–D7) +}; + +enum MAX6956GPIOFlag { FLAG_LED = 0x20 }; + +enum MAX6956CURRENTMODE { GLOBAL = 0x00, SEGMENT = 0x01 }; + +class MAX6956 : public Component, public i2c::I2CDevice { + public: + MAX6956() = default; + + void setup() override; + + bool digital_read(uint8_t pin); + void digital_write(uint8_t pin, bool value); + void pin_mode(uint8_t pin, gpio::Flags flags); + void pin_mode(uint8_t pin, max6956::MAX6956GPIOFlag flags); + + float get_setup_priority() const override { return setup_priority::HARDWARE; } + + void set_brightness_global(uint8_t current); + void set_brightness_mode(max6956::MAX6956CURRENTMODE brightness_mode); + void set_pin_brightness(uint8_t pin, float brightness); + + void dump_config() override; + + void write_brightness_global(); + void write_brightness_mode(); + + protected: + // read a given register + bool read_reg_(uint8_t reg, uint8_t *value); + // write a value to a given register + bool write_reg_(uint8_t reg, uint8_t value); + max6956::MAX6956CURRENTMODE brightness_mode_; + uint8_t global_brightness_; + + private: + int8_t prev_bright_[28] = {0}; +}; + +class MAX6956GPIOPin : public GPIOPin { + public: + void setup() override; + void pin_mode(gpio::Flags flags) override; + bool digital_read() override; + void digital_write(bool value) override; + std::string dump_summary() const override; + + void set_parent(MAX6956 *parent) { parent_ = parent; } + void set_pin(uint8_t pin) { pin_ = pin; } + void set_inverted(bool inverted) { inverted_ = inverted; } + void set_flags(gpio::Flags flags) { flags_ = flags; } + + protected: + MAX6956 *parent_; + uint8_t pin_; + bool inverted_; + gpio::Flags flags_; +}; + +} // namespace max6956 +} // namespace esphome diff --git a/esphome/components/max6956/output/__init__.py b/esphome/components/max6956/output/__init__.py new file mode 100644 index 0000000000..1caf8c8a44 --- /dev/null +++ b/esphome/components/max6956/output/__init__.py @@ -0,0 +1,28 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import output +from esphome.const import CONF_PIN, CONF_ID +from .. import MAX6956, max6956_ns, CONF_MAX6956 + +DEPENDENCIES = ["max6956"] + +MAX6956LedChannel = max6956_ns.class_( + "MAX6956LedChannel", output.FloatOutput, cg.Component +) + +CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( + { + cv.Required(CONF_ID): cv.declare_id(MAX6956LedChannel), + cv.GenerateID(CONF_MAX6956): cv.use_id(MAX6956), + cv.Required(CONF_PIN): cv.int_range(min=4, max=31), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + parent = await cg.get_variable(config[CONF_MAX6956]) + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await output.register_output(var, config) + cg.add(var.set_pin(config[CONF_PIN])) + cg.add(var.set_parent(parent)) diff --git a/esphome/components/max6956/output/max6956_led_output.cpp b/esphome/components/max6956/output/max6956_led_output.cpp new file mode 100644 index 0000000000..5fa2dd9b34 --- /dev/null +++ b/esphome/components/max6956/output/max6956_led_output.cpp @@ -0,0 +1,26 @@ +#include "max6956_led_output.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace max6956 { + +static const char *const TAG = "max6956_led_channel"; + +void MAX6956LedChannel::write_state(float state) { this->parent_->set_pin_brightness(this->pin_, state); } + +void MAX6956LedChannel::write_state(bool state) { this->parent_->digital_write(this->pin_, state); } + +void MAX6956LedChannel::setup() { + this->parent_->pin_mode(this->pin_, max6956::FLAG_LED); + this->turn_off(); +} + +void MAX6956LedChannel::dump_config() { + ESP_LOGCONFIG(TAG, "MAX6956 current:"); + ESP_LOGCONFIG(TAG, " MAX6956 pin: %d", this->pin_); + LOG_FLOAT_OUTPUT(this); +} + +} // namespace max6956 +} // namespace esphome diff --git a/esphome/components/max6956/output/max6956_led_output.h b/esphome/components/max6956/output/max6956_led_output.h new file mode 100644 index 0000000000..b844a7ceee --- /dev/null +++ b/esphome/components/max6956/output/max6956_led_output.h @@ -0,0 +1,28 @@ +#pragma once + +#include "esphome/components/max6956/max6956.h" +#include "esphome/components/output/float_output.h" + +namespace esphome { +namespace max6956 { + +class MAX6956; + +class MAX6956LedChannel : public output::FloatOutput, public Component { + public: + void set_parent(MAX6956 *parent) { this->parent_ = parent; } + void set_pin(uint8_t pin) { pin_ = pin; } + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::HARDWARE; } + + protected: + void write_state(float state) override; + void write_state(bool state) override; + + MAX6956 *parent_; + uint8_t pin_; +}; + +} // namespace max6956 +} // namespace esphome diff --git a/esphome/components/mdns/__init__.py b/esphome/components/mdns/__init__.py index e27786a98b..66c84da8d8 100644 --- a/esphome/components/mdns/__init__.py +++ b/esphome/components/mdns/__init__.py @@ -4,10 +4,13 @@ from esphome.const import ( CONF_PROTOCOL, CONF_SERVICES, CONF_SERVICE, + KEY_CORE, + KEY_FRAMEWORK_VERSION, ) import esphome.codegen as cg import esphome.config_validation as cv from esphome.core import CORE, coroutine_with_priority +from esphome.components.esp32 import add_idf_component CODEOWNERS = ["@esphome/core"] DEPENDENCIES = ["network"] @@ -79,6 +82,16 @@ async def to_code(config): elif CORE.is_rp2040: cg.add_library("LEAmDNS", None) + if CORE.using_esp_idf and CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] >= cv.Version( + 5, 0, 0 + ): + add_idf_component( + "mdns", + "https://github.com/espressif/esp-protocols.git", + "mdns-v1.0.9", + "components/mdns", + ) + if config[CONF_DISABLED]: return diff --git a/esphome/components/mdns/mdns_host.cpp b/esphome/components/mdns/mdns_host.cpp new file mode 100644 index 0000000000..3f89146f02 --- /dev/null +++ b/esphome/components/mdns/mdns_host.cpp @@ -0,0 +1,18 @@ +#ifdef USE_HOST + +#include "esphome/components/network/ip_address.h" +#include "esphome/components/network/util.h" +#include "esphome/core/log.h" +#include "mdns_component.h" + +namespace esphome { +namespace mdns { + +void MDNSComponent::setup() { this->compile_records_(); } + +void MDNSComponent::on_shutdown() {} + +} // namespace mdns +} // namespace esphome + +#endif diff --git a/esphome/components/mlx90614/__init__.py b/esphome/components/mlx90614/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/mlx90614/mlx90614.cpp b/esphome/components/mlx90614/mlx90614.cpp new file mode 100644 index 0000000000..f681f3cc7e --- /dev/null +++ b/esphome/components/mlx90614/mlx90614.cpp @@ -0,0 +1,122 @@ +#include "mlx90614.h" + +#include "esphome/core/hal.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace mlx90614 { + +static const uint8_t MLX90614_RAW_IR_1 = 0x04; +static const uint8_t MLX90614_RAW_IR_2 = 0x05; +static const uint8_t MLX90614_TEMPERATURE_AMBIENT = 0x06; +static const uint8_t MLX90614_TEMPERATURE_OBJECT_1 = 0x07; +static const uint8_t MLX90614_TEMPERATURE_OBJECT_2 = 0x08; + +static const uint8_t MLX90614_TOMAX = 0x20; +static const uint8_t MLX90614_TOMIN = 0x21; +static const uint8_t MLX90614_PWMCTRL = 0x22; +static const uint8_t MLX90614_TARANGE = 0x23; +static const uint8_t MLX90614_EMISSIVITY = 0x24; +static const uint8_t MLX90614_CONFIG = 0x25; +static const uint8_t MLX90614_ADDR = 0x2E; +static const uint8_t MLX90614_ID1 = 0x3C; +static const uint8_t MLX90614_ID2 = 0x3D; +static const uint8_t MLX90614_ID3 = 0x3E; +static const uint8_t MLX90614_ID4 = 0x3F; + +static const char *const TAG = "mlx90614"; + +void MLX90614Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up MLX90614..."); + if (!this->write_emissivity_()) { + ESP_LOGE(TAG, "Communication with MLX90614 failed!"); + this->mark_failed(); + return; + } +} + +bool MLX90614Component::write_emissivity_() { + if (std::isnan(this->emissivity_)) + return true; + uint16_t value = (uint16_t) (this->emissivity_ * 65535); + if (!this->write_bytes_(MLX90614_EMISSIVITY, 0)) { + return false; + } + delay(10); + if (!this->write_bytes_(MLX90614_EMISSIVITY, value)) { + return false; + } + delay(10); + return true; +} + +uint8_t MLX90614Component::crc8_pec_(const uint8_t *data, uint8_t len) { + uint8_t crc = 0; + for (uint8_t i = 0; i < len; i++) { + uint8_t in = data[i]; + for (uint8_t j = 0; j < 8; j++) { + bool carry = (crc ^ in) & 0x80; + crc <<= 1; + if (carry) + crc ^= 0x07; + in <<= 1; + } + } + return crc; +} + +bool MLX90614Component::write_bytes_(uint8_t reg, uint16_t data) { + uint8_t buf[5]; + buf[0] = this->address_ << 1; + buf[1] = reg; + buf[2] = data & 0xFF; + buf[3] = data >> 8; + buf[4] = this->crc8_pec_(buf, 4); + return this->write_bytes(reg, buf + 2, 3); +} + +void MLX90614Component::dump_config() { + ESP_LOGCONFIG(TAG, "MLX90614:"); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with MLX90614 failed!"); + } + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "Ambient", this->ambient_sensor_); + LOG_SENSOR(" ", "Object", this->object_sensor_); +} + +float MLX90614Component::get_setup_priority() const { return setup_priority::DATA; } + +void MLX90614Component::update() { + uint8_t emissivity[3]; + if (this->read_register(MLX90614_EMISSIVITY, emissivity, 3, false) != i2c::ERROR_OK) { + this->status_set_warning(); + return; + } + uint8_t raw_object[3]; + if (this->read_register(MLX90614_TEMPERATURE_OBJECT_1, raw_object, 3, false) != i2c::ERROR_OK) { + this->status_set_warning(); + return; + } + + uint8_t raw_ambient[3]; + if (this->read_register(MLX90614_TEMPERATURE_AMBIENT, raw_ambient, 3, false) != i2c::ERROR_OK) { + this->status_set_warning(); + return; + } + + float ambient = raw_ambient[1] & 0x80 ? NAN : encode_uint16(raw_ambient[1], raw_ambient[0]) * 0.02f - 273.15f; + float object = raw_object[1] & 0x80 ? NAN : encode_uint16(raw_object[1], raw_object[0]) * 0.02f - 273.15f; + + ESP_LOGD(TAG, "Got Temperature=%.1f°C Ambient=%.1f°C", object, ambient); + + if (this->ambient_sensor_ != nullptr && !std::isnan(ambient)) + this->ambient_sensor_->publish_state(ambient); + if (this->object_sensor_ != nullptr && !std::isnan(object)) + this->object_sensor_->publish_state(object); + this->status_clear_warning(); +} + +} // namespace mlx90614 +} // namespace esphome diff --git a/esphome/components/mlx90614/mlx90614.h b/esphome/components/mlx90614/mlx90614.h new file mode 100644 index 0000000000..b6bd44172d --- /dev/null +++ b/esphome/components/mlx90614/mlx90614.h @@ -0,0 +1,34 @@ +#pragma once + +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace mlx90614 { + +class MLX90614Component : public PollingComponent, public i2c::I2CDevice { + public: + void setup() override; + void dump_config() override; + void update() override; + float get_setup_priority() const override; + + void set_ambient_sensor(sensor::Sensor *ambient_sensor) { ambient_sensor_ = ambient_sensor; } + void set_object_sensor(sensor::Sensor *object_sensor) { object_sensor_ = object_sensor; } + + void set_emissivity(float emissivity) { emissivity_ = emissivity; } + + protected: + bool write_emissivity_(); + + uint8_t crc8_pec_(const uint8_t *data, uint8_t len); + bool write_bytes_(uint8_t reg, uint16_t data); + + sensor::Sensor *ambient_sensor_{nullptr}; + sensor::Sensor *object_sensor_{nullptr}; + + float emissivity_{NAN}; +}; +} // namespace mlx90614 +} // namespace esphome diff --git a/esphome/components/mlx90614/sensor.py b/esphome/components/mlx90614/sensor.py new file mode 100644 index 0000000000..3e90d19e45 --- /dev/null +++ b/esphome/components/mlx90614/sensor.py @@ -0,0 +1,63 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_ID, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, +) + +CODEOWNERS = ["@jesserockz"] +DEPENDENCIES = ["i2c"] + +CONF_AMBIENT = "ambient" +CONF_EMISSIVITY = "emissivity" +CONF_OBJECT = "object" + +mlx90614_ns = cg.esphome_ns.namespace("mlx90614") +MLX90614Component = mlx90614_ns.class_( + "MLX90614Component", cg.PollingComponent, i2c.I2CDevice +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(MLX90614Component), + cv.Optional(CONF_AMBIENT): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_OBJECT): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ).extend( + { + cv.Optional(CONF_EMISSIVITY, default=1.0): cv.percentage, + } + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x5A)) +) + + +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_AMBIENT in config: + sens = await sensor.new_sensor(config[CONF_AMBIENT]) + cg.add(var.set_ambient_sensor(sens)) + + if CONF_OBJECT in config: + sens = await sensor.new_sensor(config[CONF_OBJECT]) + cg.add(var.set_object_sensor(sens)) + + cg.add(var.set_emissivity(config[CONF_OBJECT][CONF_EMISSIVITY])) diff --git a/esphome/components/mqtt/mqtt_climate.cpp b/esphome/components/mqtt/mqtt_climate.cpp index e88ffcc37c..d63885fa04 100644 --- a/esphome/components/mqtt/mqtt_climate.cpp +++ b/esphome/components/mqtt/mqtt_climate.cpp @@ -75,13 +75,8 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo JsonArray presets = root.createNestedArray("preset_modes"); if (traits.supports_preset(CLIMATE_PRESET_HOME)) presets.add("home"); - if (traits.supports_preset(CLIMATE_PRESET_AWAY)) { - // away_mode_command_topic - root[MQTT_AWAY_MODE_COMMAND_TOPIC] = this->get_away_command_topic(); - // away_mode_state_topic - root[MQTT_AWAY_MODE_STATE_TOPIC] = this->get_away_state_topic(); + if (traits.supports_preset(CLIMATE_PRESET_AWAY)) presets.add("away"); - } if (traits.supports_preset(CLIMATE_PRESET_BOOST)) presets.add("boost"); if (traits.supports_preset(CLIMATE_PRESET_COMFORT)) @@ -197,29 +192,6 @@ void MQTTClimateComponent::setup() { }); } - if (traits.supports_preset(CLIMATE_PRESET_AWAY)) { - this->subscribe(this->get_away_command_topic(), [this](const std::string &topic, const std::string &payload) { - auto onoff = parse_on_off(payload.c_str()); - auto call = this->device_->make_call(); - switch (onoff) { - case PARSE_ON: - call.set_preset(CLIMATE_PRESET_AWAY); - break; - case PARSE_OFF: - call.set_preset(CLIMATE_PRESET_HOME); - break; - case PARSE_TOGGLE: - call.set_preset(this->device_->preset == CLIMATE_PRESET_AWAY ? CLIMATE_PRESET_HOME : CLIMATE_PRESET_AWAY); - break; - case PARSE_NONE: - default: - ESP_LOGW(TAG, "Unknown payload '%s'", payload.c_str()); - return; - } - call.perform(); - }); - } - if (traits.get_supports_presets() || !traits.get_supported_custom_presets().empty()) { this->subscribe(this->get_preset_command_topic(), [this](const std::string &topic, const std::string &payload) { auto call = this->device_->make_call(); @@ -301,11 +273,6 @@ bool MQTTClimateComponent::publish_state_() { success = false; } - if (traits.supports_preset(CLIMATE_PRESET_AWAY)) { - std::string payload = ONOFF(this->device_->preset == CLIMATE_PRESET_AWAY); - if (!this->publish(this->get_away_state_topic(), payload)) - success = false; - } if (traits.get_supports_presets() || !traits.get_supported_custom_presets().empty()) { std::string payload; if (this->device_->preset.has_value()) { diff --git a/esphome/components/network/util.cpp b/esphome/components/network/util.cpp index f7ac6b543e..941102d6c1 100644 --- a/esphome/components/network/util.cpp +++ b/esphome/components/network/util.cpp @@ -23,6 +23,9 @@ bool is_connected() { return wifi::global_wifi_component->is_connected(); #endif +#ifdef USE_HOST + return true; // Assume its connected +#endif return false; } diff --git a/esphome/components/number/number_traits.cpp b/esphome/components/number/number_traits.cpp index 1554f8d9c9..89035661f5 100644 --- a/esphome/components/number/number_traits.cpp +++ b/esphome/components/number/number_traits.cpp @@ -6,23 +6,5 @@ namespace number { static const char *const TAG = "number"; -void NumberTraits::set_unit_of_measurement(const std::string &unit_of_measurement) { - this->unit_of_measurement_ = unit_of_measurement; -} - -std::string NumberTraits::get_unit_of_measurement() { - if (this->unit_of_measurement_.has_value()) - return *this->unit_of_measurement_; - return ""; -} - -void NumberTraits::set_device_class(const std::string &device_class) { this->device_class_ = device_class; } - -std::string NumberTraits::get_device_class() { - if (this->device_class_.has_value()) - return *this->device_class_; - return ""; -} - } // namespace number } // namespace esphome diff --git a/esphome/components/number/number_traits.h b/esphome/components/number/number_traits.h index ee10b0010c..fa68c2390a 100644 --- a/esphome/components/number/number_traits.h +++ b/esphome/components/number/number_traits.h @@ -1,5 +1,6 @@ #pragma once +#include "esphome/core/entity_base.h" #include "esphome/core/helpers.h" namespace esphome { @@ -11,7 +12,7 @@ enum NumberMode : uint8_t { NUMBER_MODE_SLIDER = 2, }; -class NumberTraits { +class NumberTraits : public EntityBase_DeviceClass, public EntityBase_UnitOfMeasurement { public: // Set/get the number value boundaries. void set_min_value(float min_value) { min_value_ = min_value; } @@ -23,26 +24,15 @@ class NumberTraits { void set_step(float step) { step_ = step; } float get_step() const { return step_; } - /// Manually set the unit of measurement. - void set_unit_of_measurement(const std::string &unit_of_measurement); - /// Get the unit of measurement, using the manual override if set. - std::string get_unit_of_measurement(); - // Set/get the frontend mode. void set_mode(NumberMode mode) { this->mode_ = mode; } NumberMode get_mode() const { return this->mode_; } - // Set/get the device class. - void set_device_class(const std::string &device_class); - std::string get_device_class(); - protected: float min_value_ = NAN; float max_value_ = NAN; float step_ = NAN; - optional unit_of_measurement_; ///< Unit of measurement override NumberMode mode_{NUMBER_MODE_AUTO}; - optional device_class_; }; } // namespace number diff --git a/esphome/components/ota/ota_backend_esp_idf.cpp b/esphome/components/ota/ota_backend_esp_idf.cpp index 2fdc00c54d..7688629e39 100644 --- a/esphome/components/ota/ota_backend_esp_idf.cpp +++ b/esphome/components/ota/ota_backend_esp_idf.cpp @@ -8,6 +8,10 @@ #include #include "esphome/components/md5/md5.h" +#if ESP_IDF_VERSION_MAJOR >= 5 +#include +#endif + namespace esphome { namespace ota { @@ -16,9 +20,28 @@ OTAResponseTypes IDFOTABackend::begin(size_t image_size) { if (this->partition_ == nullptr) { return OTA_RESPONSE_ERROR_NO_UPDATE_PARTITION; } - esp_task_wdt_init(15, false); // The following function takes longer than the 5 seconds timeout of WDT + + // The following function takes longer than the 5 seconds timeout of WDT +#if ESP_IDF_VERSION_MAJOR >= 5 + esp_task_wdt_config_t wdtc; + wdtc.timeout_ms = 15000; + wdtc.idle_core_mask = 0; + wdtc.trigger_panic = false; + esp_task_wdt_reconfigure(&wdtc); +#else + esp_task_wdt_init(15, false); +#endif + esp_err_t err = esp_ota_begin(this->partition_, image_size, &this->update_handle_); - esp_task_wdt_init(CONFIG_ESP_TASK_WDT_TIMEOUT_S, false); // Set the WDT back to the configured timeout + + // Set the WDT back to the configured timeout +#if ESP_IDF_VERSION_MAJOR >= 5 + wdtc.timeout_ms = CONFIG_ESP_TASK_WDT_TIMEOUT_S; + esp_task_wdt_reconfigure(&wdtc); +#else + esp_task_wdt_init(CONFIG_ESP_TASK_WDT_TIMEOUT_S, false); +#endif + if (err != ESP_OK) { esp_ota_abort(this->update_handle_); this->update_handle_ = 0; diff --git a/esphome/components/pca6416a/__init__.py b/esphome/components/pca6416a/__init__.py new file mode 100644 index 0000000000..574d8dce91 --- /dev/null +++ b/esphome/components/pca6416a/__init__.py @@ -0,0 +1,78 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import i2c +from esphome.const import ( + CONF_ID, + CONF_INPUT, + CONF_NUMBER, + CONF_MODE, + CONF_INVERTED, + CONF_OUTPUT, + CONF_PULLUP, +) + +CODEOWNERS = ["@Mat931"] +DEPENDENCIES = ["i2c"] +MULTI_CONF = True +pca6416a_ns = cg.esphome_ns.namespace("pca6416a") + +PCA6416AComponent = pca6416a_ns.class_("PCA6416AComponent", cg.Component, i2c.I2CDevice) +PCA6416AGPIOPin = pca6416a_ns.class_( + "PCA6416AGPIOPin", cg.GPIOPin, cg.Parented.template(PCA6416AComponent) +) + +CONF_PCA6416A = "pca6416a" +CONFIG_SCHEMA = ( + cv.Schema({cv.Required(CONF_ID): cv.declare_id(PCA6416AComponent)}) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(0x21)) +) + + +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) + + +def validate_mode(value): + if not (value[CONF_INPUT] or value[CONF_OUTPUT]): + raise cv.Invalid("Mode must be either input or output") + if value[CONF_INPUT] and value[CONF_OUTPUT]: + raise cv.Invalid("Mode must be either input or output") + if value[CONF_PULLUP] and not value[CONF_INPUT]: + raise cv.Invalid("Pullup only available with input") + return value + + +PCA6416A_PIN_SCHEMA = cv.All( + { + cv.GenerateID(): cv.declare_id(PCA6416AGPIOPin), + cv.Required(CONF_PCA6416A): cv.use_id(PCA6416AComponent), + cv.Required(CONF_NUMBER): cv.int_range(min=0, max=16), + cv.Optional(CONF_MODE, default={}): cv.All( + { + cv.Optional(CONF_INPUT, default=False): cv.boolean, + cv.Optional(CONF_PULLUP, default=False): cv.boolean, + cv.Optional(CONF_OUTPUT, default=False): cv.boolean, + }, + validate_mode, + ), + cv.Optional(CONF_INVERTED, default=False): cv.boolean, + } +) + + +@pins.PIN_SCHEMA_REGISTRY.register("pca6416a", PCA6416A_PIN_SCHEMA) +async def pca6416a_pin_to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + parent = await cg.get_variable(config[CONF_PCA6416A]) + + cg.add(var.set_parent(parent)) + + 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/pca6416a/pca6416a.cpp b/esphome/components/pca6416a/pca6416a.cpp new file mode 100644 index 0000000000..1f4e315644 --- /dev/null +++ b/esphome/components/pca6416a/pca6416a.cpp @@ -0,0 +1,174 @@ +#include "pca6416a.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pca6416a { + +enum PCA6416AGPIORegisters { + // 0 side + PCA6416A_INPUT0 = 0x00, + PCA6416A_OUTPUT0 = 0x02, + PCA6416A_INVERT0 = 0x04, + PCA6416A_CONFIG0 = 0x06, + PCAL6416A_PULL_EN0 = 0x46, + PCAL6416A_PULL_DIR0 = 0x48, + // 1 side + PCA6416A_INPUT1 = 0x01, + PCA6416A_OUTPUT1 = 0x03, + PCA6416A_INVERT1 = 0x05, + PCA6416A_CONFIG1 = 0x07, + PCAL6416A_PULL_EN1 = 0x47, + PCAL6416A_PULL_DIR1 = 0x49, +}; + +static const char *const TAG = "pca6416a"; + +void PCA6416AComponent::setup() { + ESP_LOGCONFIG(TAG, "Setting up PCA6416A..."); + // Test to see if device exists + uint8_t value; + if (!this->read_register_(PCA6416A_INPUT0, &value)) { + ESP_LOGE(TAG, "PCA6416A not available under 0x%02X", this->address_); + this->mark_failed(); + return; + } + + // Test to see if the device supports pull-up resistors + if (this->read_register(PCAL6416A_PULL_EN0, &value, 1, true) == esphome::i2c::ERROR_OK) { + this->has_pullup_ = true; + } + + // No polarity inversion + this->write_register_(PCA6416A_INVERT0, 0); + this->write_register_(PCA6416A_INVERT1, 0); + // Set all pins to input + this->write_register_(PCA6416A_CONFIG0, 0xff); + this->write_register_(PCA6416A_CONFIG1, 0xff); + // Read current output register state + this->read_register_(PCA6416A_OUTPUT0, &this->output_0_); + this->read_register_(PCA6416A_OUTPUT1, &this->output_1_); + + ESP_LOGD(TAG, "Initialization complete. Warning: %d, Error: %d", this->status_has_warning(), + this->status_has_error()); +} + +void PCA6416AComponent::dump_config() { + if (this->has_pullup_) { + ESP_LOGCONFIG(TAG, "PCAL6416A:"); + } else { + ESP_LOGCONFIG(TAG, "PCA6416A:"); + } + LOG_I2C_DEVICE(this) + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with PCA6416A failed!"); + } +} + +bool PCA6416AComponent::digital_read(uint8_t pin) { + uint8_t bit = pin % 8; + uint8_t reg_addr = pin < 8 ? PCA6416A_INPUT0 : PCA6416A_INPUT1; + uint8_t value = 0; + this->read_register_(reg_addr, &value); + return value & (1 << bit); +} + +void PCA6416AComponent::digital_write(uint8_t pin, bool value) { + uint8_t reg_addr = pin < 8 ? PCA6416A_OUTPUT0 : PCA6416A_OUTPUT1; + this->update_register_(pin, value, reg_addr); +} + +void PCA6416AComponent::pin_mode(uint8_t pin, gpio::Flags flags) { + uint8_t io_dir = pin < 8 ? PCA6416A_CONFIG0 : PCA6416A_CONFIG1; + uint8_t pull_en = pin < 8 ? PCAL6416A_PULL_EN0 : PCAL6416A_PULL_EN1; + uint8_t pull_dir = pin < 8 ? PCAL6416A_PULL_DIR0 : PCAL6416A_PULL_DIR1; + if (flags == gpio::FLAG_INPUT) { + this->update_register_(pin, true, io_dir); + if (has_pullup_) { + this->update_register_(pin, true, pull_dir); + this->update_register_(pin, false, pull_en); + } + } else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_PULLUP)) { + this->update_register_(pin, true, io_dir); + if (has_pullup_) { + this->update_register_(pin, true, pull_dir); + this->update_register_(pin, true, pull_en); + } else { + ESP_LOGW(TAG, "Your PCA6416A does not support pull-up resistors"); + } + } else if (flags == gpio::FLAG_OUTPUT) { + this->update_register_(pin, false, io_dir); + } +} + +bool PCA6416AComponent::read_register_(uint8_t reg, uint8_t *value) { + if (this->is_failed()) { + ESP_LOGD(TAG, "Device marked failed"); + return false; + } + + if ((this->last_error_ = this->read_register(reg, value, 1, true)) != esphome::i2c::ERROR_OK) { + this->status_set_warning(); + ESP_LOGE(TAG, "read_register_(): I2C I/O error: %d", (int) this->last_error_); + return false; + } + + this->status_clear_warning(); + return true; +} + +bool PCA6416AComponent::write_register_(uint8_t reg, uint8_t value) { + if (this->is_failed()) { + ESP_LOGD(TAG, "Device marked failed"); + return false; + } + + if ((this->last_error_ = this->write_register(reg, &value, 1, true)) != esphome::i2c::ERROR_OK) { + this->status_set_warning(); + ESP_LOGE(TAG, "write_register_(): I2C I/O error: %d", (int) this->last_error_); + return false; + } + + this->status_clear_warning(); + return true; +} + +void PCA6416AComponent::update_register_(uint8_t pin, bool pin_value, uint8_t reg_addr) { + uint8_t bit = pin % 8; + uint8_t reg_value = 0; + if (reg_addr == PCA6416A_OUTPUT0) { + reg_value = this->output_0_; + } else if (reg_addr == PCA6416A_OUTPUT1) { + reg_value = this->output_1_; + } else { + this->read_register_(reg_addr, ®_value); + } + + if (pin_value) { + reg_value |= 1 << bit; + } else { + reg_value &= ~(1 << bit); + } + + this->write_register_(reg_addr, reg_value); + + if (reg_addr == PCA6416A_OUTPUT0) { + this->output_0_ = reg_value; + } else if (reg_addr == PCA6416A_OUTPUT1) { + this->output_1_ = reg_value; + } +} + +float PCA6416AComponent::get_setup_priority() const { return setup_priority::IO; } + +void PCA6416AGPIOPin::setup() { pin_mode(flags_); } +void PCA6416AGPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } +bool PCA6416AGPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } +void PCA6416AGPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); } +std::string PCA6416AGPIOPin::dump_summary() const { + char buffer[32]; + snprintf(buffer, sizeof(buffer), "%u via PCA6416A", pin_); + return buffer; +} + +} // namespace pca6416a +} // namespace esphome diff --git a/esphome/components/pca6416a/pca6416a.h b/esphome/components/pca6416a/pca6416a.h new file mode 100644 index 0000000000..247f443e87 --- /dev/null +++ b/esphome/components/pca6416a/pca6416a.h @@ -0,0 +1,63 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace pca6416a { + +class PCA6416AComponent : public Component, public i2c::I2CDevice { + public: + PCA6416AComponent() = default; + + /// Check i2c availability and setup masks + void setup() override; + /// Helper function to read the value of a pin. + bool digital_read(uint8_t pin); + /// Helper function to write the value of a pin. + void digital_write(uint8_t pin, bool value); + /// Helper function to set the pin mode of a pin. + void pin_mode(uint8_t pin, gpio::Flags flags); + + float get_setup_priority() const override; + + void dump_config() override; + + protected: + bool read_register_(uint8_t reg, uint8_t *value); + bool write_register_(uint8_t reg, uint8_t value); + void update_register_(uint8_t pin, bool pin_value, uint8_t reg_addr); + + /// The mask to write as output state - 1 means HIGH, 0 means LOW + uint8_t output_0_{0x00}; + uint8_t output_1_{0x00}; + /// Storage for last I2C error seen + esphome::i2c::ErrorCode last_error_; + /// Only the PCAL6416A has pull-up resistors + bool has_pullup_{false}; +}; + +/// Helper class to expose a PCA6416A pin as an internal input GPIO pin. +class PCA6416AGPIOPin : public GPIOPin { + public: + void setup() override; + void pin_mode(gpio::Flags flags) override; + bool digital_read() override; + void digital_write(bool value) override; + std::string dump_summary() const override; + + void set_parent(PCA6416AComponent *parent) { parent_ = parent; } + void set_pin(uint8_t pin) { pin_ = pin; } + void set_inverted(bool inverted) { inverted_ = inverted; } + void set_flags(gpio::Flags flags) { flags_ = flags; } + + protected: + PCA6416AComponent *parent_; + uint8_t pin_; + bool inverted_; + gpio::Flags flags_; +}; + +} // namespace pca6416a +} // namespace esphome diff --git a/esphome/components/pn532/pn532.cpp b/esphome/components/pn532/pn532.cpp index dc831ef6e0..cc28d7078b 100644 --- a/esphome/components/pn532/pn532.cpp +++ b/esphome/components/pn532/pn532.cpp @@ -81,7 +81,32 @@ void PN532::setup() { this->turn_off_rf_(); } +bool PN532::powerdown() { + updates_enabled_ = false; + requested_read_ = false; + ESP_LOGI(TAG, "Powering down PN532"); + if (!this->write_command_({PN532_COMMAND_POWERDOWN, 0b10100000})) { // enable i2c,spi wakeup + ESP_LOGE(TAG, "Error writing powerdown command to PN532"); + return false; + } + std::vector response; + if (!this->read_response(PN532_COMMAND_POWERDOWN, response)) { + ESP_LOGE(TAG, "Error reading PN532 powerdown response"); + return false; + } + if (response[0] != 0x00) { + ESP_LOGE(TAG, "Error on PN532 powerdown: %02x", response[0]); + return false; + } + ESP_LOGV(TAG, "Powerdown successful"); + delay(1); + return true; +} + void PN532::update() { + if (!updates_enabled_) + return; + for (auto *obj : this->binary_sensors_) obj->on_scan_end(); diff --git a/esphome/components/pn532/pn532.h b/esphome/components/pn532/pn532.h index fee94a29b8..73b349e328 100644 --- a/esphome/components/pn532/pn532.h +++ b/esphome/components/pn532/pn532.h @@ -17,6 +17,7 @@ static const uint8_t PN532_COMMAND_SAMCONFIGURATION = 0x14; static const uint8_t PN532_COMMAND_RFCONFIGURATION = 0x32; static const uint8_t PN532_COMMAND_INDATAEXCHANGE = 0x40; static const uint8_t PN532_COMMAND_INLISTPASSIVETARGET = 0x4A; +static const uint8_t PN532_COMMAND_POWERDOWN = 0x16; class PN532BinarySensor; @@ -30,6 +31,7 @@ class PN532 : public PollingComponent { float get_setup_priority() const override; void loop() override; + void on_shutdown() override { powerdown(); } void register_tag(PN532BinarySensor *tag) { this->binary_sensors_.push_back(tag); } void register_ontag_trigger(nfc::NfcOnTagTrigger *trig) { this->triggers_ontag_.push_back(trig); } @@ -45,6 +47,7 @@ class PN532 : public PollingComponent { void clean_mode(); void format_mode(); void write_mode(nfc::NdefMessage *message); + bool powerdown(); protected: void turn_off_rf_(); @@ -79,6 +82,7 @@ class PN532 : public PollingComponent { bool write_mifare_ultralight_tag_(std::vector &uid, nfc::NdefMessage *message); bool clean_mifare_ultralight_(); + bool updates_enabled_{true}; bool requested_read_{false}; std::vector binary_sensors_; std::vector triggers_ontag_; diff --git a/esphome/components/rc522/__init__.py b/esphome/components/rc522/__init__.py index d64cf3c085..1a1e641623 100644 --- a/esphome/components/rc522/__init__.py +++ b/esphome/components/rc522/__init__.py @@ -2,7 +2,12 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation, pins from esphome.components import i2c -from esphome.const import CONF_ON_TAG, CONF_TRIGGER_ID, CONF_RESET_PIN +from esphome.const import ( + CONF_ON_TAG, + CONF_ON_TAG_REMOVED, + CONF_TRIGGER_ID, + CONF_RESET_PIN, +) CODEOWNERS = ["@glmnet"] AUTO_LOAD = ["binary_sensor"] @@ -24,6 +29,11 @@ RC522_SCHEMA = cv.Schema( cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(RC522Trigger), } ), + cv.Optional(CONF_ON_TAG_REMOVED): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(RC522Trigger), + } + ), } ).extend(cv.polling_component_schema("1s")) @@ -37,5 +47,10 @@ async def setup_rc522(var, config): for conf in config.get(CONF_ON_TAG, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) - cg.add(var.register_trigger(trigger)) + cg.add(var.register_ontag_trigger(trigger)) + await automation.build_automation(trigger, [(cg.std_string, "x")], conf) + + for conf in config.get(CONF_ON_TAG_REMOVED, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) + cg.add(var.register_ontagremoved_trigger(trigger)) await automation.build_automation(trigger, [(cg.std_string, "x")], conf) diff --git a/esphome/components/rc522/rc522.cpp b/esphome/components/rc522/rc522.cpp index 5bfeb40156..4e74020e4c 100644 --- a/esphome/components/rc522/rc522.cpp +++ b/esphome/components/rc522/rc522.cpp @@ -256,7 +256,7 @@ void RC522::loop() { this->current_uid_ = rfid_uid; - for (auto *trigger : this->triggers_) + for (auto *trigger : this->triggers_ontag_) trigger->process(rfid_uid); if (report) { @@ -265,6 +265,11 @@ void RC522::loop() { break; } case STATE_DONE: { + if (!this->current_uid_.empty()) { + ESP_LOGV(TAG, "Tag '%s' removed", format_uid(this->current_uid_).c_str()); + for (auto *trigger : this->triggers_ontagremoved_) + trigger->process(this->current_uid_); + } this->current_uid_ = {}; state_ = STATE_INIT; break; diff --git a/esphome/components/rc522/rc522.h b/esphome/components/rc522/rc522.h index 5eea3c665e..c6c5e119f0 100644 --- a/esphome/components/rc522/rc522.h +++ b/esphome/components/rc522/rc522.h @@ -24,7 +24,8 @@ class RC522 : public PollingComponent { void loop() override; void register_tag(RC522BinarySensor *tag) { this->binary_sensors_.push_back(tag); } - void register_trigger(RC522Trigger *trig) { this->triggers_.push_back(trig); } + void register_ontag_trigger(RC522Trigger *trig) { this->triggers_ontag_.push_back(trig); } + void register_ontagremoved_trigger(RC522Trigger *trig) { this->triggers_ontagremoved_.push_back(trig); } void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; } @@ -242,7 +243,8 @@ class RC522 : public PollingComponent { uint8_t reset_count_{0}; uint32_t reset_timeout_{0}; std::vector binary_sensors_; - std::vector triggers_; + std::vector triggers_ontag_; + std::vector triggers_ontagremoved_; std::vector current_uid_; enum RC522Error { diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index 4d9196c9c5..2ef33f3711 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -791,6 +791,57 @@ async def raw_action(var, config, args): cg.add(var.set_carrier_frequency(templ)) +# Drayton +( + DraytonData, + DraytonBinarySensor, + DraytonTrigger, + DraytonAction, + DraytonDumper, +) = declare_protocol("Drayton") +DRAYTON_SCHEMA = cv.Schema( + { + cv.Required(CONF_ADDRESS): cv.All(cv.hex_int, cv.Range(min=0, max=0xFFFF)), + cv.Required(CONF_CHANNEL): cv.All(cv.hex_int, cv.Range(min=0, max=0x1F)), + cv.Required(CONF_COMMAND): cv.All(cv.hex_int, cv.Range(min=0, max=0x7F)), + } +) + + +@register_binary_sensor("drayton", DraytonBinarySensor, DRAYTON_SCHEMA) +def drayton_binary_sensor(var, config): + cg.add( + var.set_data( + cg.StructInitializer( + DraytonData, + ("address", config[CONF_ADDRESS]), + ("channel", config[CONF_CHANNEL]), + ("command", config[CONF_COMMAND]), + ) + ) + ) + + +@register_trigger("drayton", DraytonTrigger, DraytonData) +def drayton_trigger(var, config): + pass + + +@register_dumper("drayton", DraytonDumper) +def drayton_dumper(var, config): + pass + + +@register_action("drayton", DraytonAction, DRAYTON_SCHEMA) +async def drayton_action(var, config, args): + template_ = await cg.templatable(config[CONF_ADDRESS], args, cg.uint16) + cg.add(var.set_address(template_)) + template_ = await cg.templatable(config[CONF_CHANNEL], args, cg.uint8) + cg.add(var.set_channel(template_)) + template_ = await cg.templatable(config[CONF_COMMAND], args, cg.uint8) + cg.add(var.set_command(template_)) + + # RC5 RC5Data, RC5BinarySensor, RC5Trigger, RC5Action, RC5Dumper = declare_protocol("RC5") RC5_SCHEMA = cv.Schema( diff --git a/esphome/components/remote_base/drayton_protocol.cpp b/esphome/components/remote_base/drayton_protocol.cpp new file mode 100644 index 0000000000..f5eae49058 --- /dev/null +++ b/esphome/components/remote_base/drayton_protocol.cpp @@ -0,0 +1,213 @@ +#include "drayton_protocol.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace remote_base { + +static const char *const TAG = "remote.drayton"; + +static const uint32_t BIT_TIME_US = 500; +static const uint8_t CARRIER_KHZ = 2; +static const uint8_t NBITS_PREAMBLE = 12; +static const uint8_t NBITS_SYNC = 4; +static const uint8_t NBITS_ADDRESS = 16; +static const uint8_t NBITS_CHANNEL = 5; +static const uint8_t NBITS_COMMAND = 7; +static const uint8_t NBITS = NBITS_ADDRESS + NBITS_CHANNEL + NBITS_COMMAND; + +static const uint8_t CMD_ON = 0x41; +static const uint8_t CMD_OFF = 0x02; + +/* +Drayton Protocol +Using an oscilloscope to capture the data transmitted by the Digistat two +distinct packets for 'On' and 'Off' are transmitted. Each transmitted bit +has a period of 500us, a bit rate of 2000 baud. + +Each packet consists of an initial 1010 pattern to set up the receiver bias. +The number of these bits seen at the receiver varies depending on the state +of the bias when the packet transmission starts. The receiver algoritmn takes +account of this. + +The packet appears to be Manchester encoded, with a '10' tranmitted pair +representing a '1' bit and a '01' pair representing a '0' bit. Each packet is +begun with a '1100' syncronisation symbol which breaks this rule. Following +the sync are 28 '01' or '10' pairs. + +-------------------- + +Boiler On Command as received: +101010101010110001101001010101101001010101010101100101010101101001011001 +ppppppppppppSSSS-0-1-1-0-0-0-0-1-1-0-0-0-0-0-0-0-1-0-0-0-0-0-1-1-0-0-1-0 + +(Where pppp represents the preamble bits and SSSS represents the sync symbol) + +28 bits of data received 01100001100000001000001 10010 (bin) or 6180832 (hex) + +Boiler Off Command as received: +101010101010110001101001010101101001010101010101010101010110011001011001 +ppppppppppppSSSS-0-1-1-0-0-0-0-1-1-0-0-0-0-0-0-0-0-0-0-0-0-1-0-1-0-0-1-0 + +28 bits of data received 0110000110000000000001010010 (bin) or 6180052 (hex) + +-------------------- + +I have used 'RFLink' software (RLink Firmware Version: 1.1 Revision: 48) to +capture and retransmit the Digistat packets. RFLink splits each packet into an +ID, SWITCH, and CMD field. + +0;17;Drayton;ID=c300;SWITCH=12;CMD=ON; +20;18;Drayton;ID=c300;SWITCH=12;CMD=OFF; + +-------------------- + +Spliting my received data into three parts of 16, 7 and 5 bits gives address, +channel and Command values of: + +On 6180832 0110000110000000 1000001 10010 +address: '0x6180' channel: '0x12' command: '0x41' + +Off 6180052 0110000110000000 0000010 10010 +address: '0x6180' channel: '0x12' command: '0x02' + +These values are slightly different to those used by RFLink (the RFLink +ID/Adress value is rotated/manipulated), and I don't know who's interpretation +is correct. A larger data sample would help (I have only found five different +packet captures online) or definitive information from Drayton. + +Splitting each packet in this way works well for me with esphome. Any +corrections or additional data samples would be gratefully received. + +marshn + +*/ + +void DraytonProtocol::encode(RemoteTransmitData *dst, const DraytonData &data) { + uint16_t khz = CARRIER_KHZ; + dst->set_carrier_frequency(khz * 1000); + + // Preamble = 101010101010 + uint32_t out_data = 0x0AAA; + for (uint32_t mask = 1UL << (NBITS_PREAMBLE - 1); mask != 0; mask >>= 1) { + if (out_data & mask) { + dst->mark(BIT_TIME_US); + } else { + dst->space(BIT_TIME_US); + } + } + + // Sync = 1100 + out_data = 0x000C; + for (uint32_t mask = 1UL << (NBITS_SYNC - 1); mask != 0; mask >>= 1) { + if (out_data & mask) { + dst->mark(BIT_TIME_US); + } else { + dst->space(BIT_TIME_US); + } + } + + ESP_LOGD(TAG, "Send Drayton: address=%04x channel=%03x cmd=%02x", data.address, data.channel, data.command); + + out_data = data.address; + out_data <<= NBITS_COMMAND; + out_data |= data.command; + out_data <<= NBITS_CHANNEL; + out_data |= data.channel; + + ESP_LOGV(TAG, "Send Drayton: out_data %08x", out_data); + + for (uint32_t mask = 1UL << (NBITS - 1); mask != 0; mask >>= 1) { + if (out_data & mask) { + dst->mark(BIT_TIME_US); + dst->space(BIT_TIME_US); + } else { + dst->space(BIT_TIME_US); + dst->mark(BIT_TIME_US); + } + } +} + +optional DraytonProtocol::decode(RemoteReceiveData src) { + DraytonData out{ + .address = 0, + .channel = 0, + .command = 0, + }; + + if (src.size() < 45) { + return {}; + } + + ESP_LOGVV(TAG, "Decode Drayton: %d, %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d", src.size(), + src.peek(0), src.peek(1), src.peek(2), src.peek(3), src.peek(4), src.peek(5), src.peek(6), src.peek(7), + src.peek(8), src.peek(9), src.peek(10), src.peek(11), src.peek(12), src.peek(13), src.peek(14), + src.peek(15), src.peek(16), src.peek(17), src.peek(18), src.peek(19)); + + // If first preamble item is a space, skip it + if (src.peek_space_at_least(1)) { + src.advance(1); + } + + // Look for sync pulse, after. If sucessful index points to space of sync symbol + for (uint16_t preamble = 0; preamble <= NBITS_PREAMBLE * 2; preamble += 2) { + ESP_LOGVV(TAG, "Decode Drayton: preamble %d %d %d", preamble, src.peek(preamble), src.peek(preamble + 1)); + if (src.peek_mark(2 * BIT_TIME_US, preamble) && + (src.peek_space(2 * BIT_TIME_US, preamble + 1) || src.peek_space(3 * BIT_TIME_US, preamble + 1))) { + src.advance(preamble + 1); + break; + } + } + + // Read data. Index points to space of sync symbol + // Extract first bit + // Checks next bit to leave index pointing correctly + uint32_t out_data = 0; + uint8_t bit = NBITS_ADDRESS + NBITS_COMMAND + NBITS_CHANNEL - 1; + if (src.expect_space(3 * BIT_TIME_US) && (src.expect_mark(BIT_TIME_US) || src.peek_mark(2 * BIT_TIME_US))) { + out_data |= 0 << bit; + } else if (src.expect_space(2 * BIT_TIME_US) && src.expect_mark(BIT_TIME_US) && + (src.expect_space(BIT_TIME_US) || src.peek_space(2 * BIT_TIME_US))) { + out_data |= 1 << bit; + } else { + ESP_LOGV(TAG, "Decode Drayton: Fail 1, - %d", src.get_index()); + return {}; + } + + // Before/after each bit is read the index points to the transition at the start of the bit period or, + // if there is no transition at the start of the bit period, then the transition in the middle of + // the previous bit period. + while (--bit >= 1) { + ESP_LOGVV(TAG, "Decode Drayton: Data, %2d %08x", bit, out_data); + if ((src.expect_space(BIT_TIME_US) || src.expect_space(2 * BIT_TIME_US)) && + (src.expect_mark(BIT_TIME_US) || src.peek_mark(2 * BIT_TIME_US))) { + out_data |= 0 << bit; + } else if ((src.expect_mark(BIT_TIME_US) || src.expect_mark(2 * BIT_TIME_US)) && + (src.expect_space(BIT_TIME_US) || src.peek_space(2 * BIT_TIME_US))) { + out_data |= 1 << bit; + } else { + ESP_LOGVV(TAG, "Decode Drayton: Fail 2, %2d %08x", bit, out_data); + return {}; + } + } + if (src.expect_space(BIT_TIME_US) || src.expect_space(2 * BIT_TIME_US)) { + out_data |= 0; + } else if (src.expect_mark(BIT_TIME_US) || src.expect_mark(2 * BIT_TIME_US)) { + out_data |= 1; + } + ESP_LOGV(TAG, "Decode Drayton: Data, %2d %08x", bit, out_data); + + out.channel = (uint8_t) (out_data & 0x1F); + out_data >>= NBITS_CHANNEL; + out.command = (uint8_t) (out_data & 0x7F); + out_data >>= NBITS_COMMAND; + out.address = (uint16_t) (out_data & 0xFFFF); + + return out; +} +void DraytonProtocol::dump(const DraytonData &data) { + ESP_LOGD(TAG, "Received Drayton: address=0x%04X (0x%04x), channel=0x%03x command=0x%03X", data.address, + ((data.address << 1) & 0xffff), data.channel, data.command); +} + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/components/remote_base/drayton_protocol.h b/esphome/components/remote_base/drayton_protocol.h new file mode 100644 index 0000000000..f468e7b57e --- /dev/null +++ b/esphome/components/remote_base/drayton_protocol.h @@ -0,0 +1,44 @@ +#pragma once + +#include "esphome/core/component.h" +#include "remote_base.h" + +namespace esphome { +namespace remote_base { + +struct DraytonData { + uint16_t address; + uint8_t channel; + uint8_t command; + + bool operator==(const DraytonData &rhs) const { + return address == rhs.address && channel == rhs.channel && command == rhs.command; + } +}; + +class DraytonProtocol : public RemoteProtocol { + public: + void encode(RemoteTransmitData *dst, const DraytonData &data) override; + optional decode(RemoteReceiveData src) override; + void dump(const DraytonData &data) override; +}; + +DECLARE_REMOTE_PROTOCOL(Drayton) + +template class DraytonAction : public RemoteTransmitterActionBase { + public: + TEMPLATABLE_VALUE(uint16_t, address) + TEMPLATABLE_VALUE(uint8_t, channel) + TEMPLATABLE_VALUE(uint8_t, command) + + void encode(RemoteTransmitData *dst, Ts... x) override { + DraytonData data{}; + data.address = this->address_.value(x...); + data.channel = this->channel_.value(x...); + data.command = this->command_.value(x...); + DraytonProtocol().encode(dst, data); + } +}; + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/components/rp2040/__init__.py b/esphome/components/rp2040/__init__.py index c6fbcf8deb..3d0d6ec060 100644 --- a/esphome/components/rp2040/__init__.py +++ b/esphome/components/rp2040/__init__.py @@ -102,7 +102,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/raspberrypi @ {value}" + return f"platformio/raspberrypi@{value}" except cv.Invalid: return value @@ -148,7 +148,7 @@ async def to_code(config): cg.add_platformio_option("platform", conf[CONF_PLATFORM_VERSION]) cg.add_platformio_option( "platform_packages", - [f"earlephilhower/framework-arduinopico @ {conf[CONF_SOURCE]}"], + [f"earlephilhower/framework-arduinopico@{conf[CONF_SOURCE]}"], ) cg.add_platformio_option("board_build.core", "earlephilhower") diff --git a/esphome/components/sensor/sensor.cpp b/esphome/components/sensor/sensor.cpp index fc66e03d6b..14a8b3d490 100644 --- a/esphome/components/sensor/sensor.cpp +++ b/esphome/components/sensor/sensor.cpp @@ -22,15 +22,6 @@ std::string state_class_to_string(StateClass state_class) { Sensor::Sensor() : state(NAN), raw_state(NAN) {} -std::string Sensor::get_unit_of_measurement() { - if (this->unit_of_measurement_.has_value()) - return *this->unit_of_measurement_; - return ""; -} -void Sensor::set_unit_of_measurement(const std::string &unit_of_measurement) { - this->unit_of_measurement_ = unit_of_measurement; -} - int8_t Sensor::get_accuracy_decimals() { if (this->accuracy_decimals_.has_value()) return *this->accuracy_decimals_; @@ -38,13 +29,6 @@ int8_t Sensor::get_accuracy_decimals() { } void Sensor::set_accuracy_decimals(int8_t accuracy_decimals) { this->accuracy_decimals_ = accuracy_decimals; } -std::string Sensor::get_device_class() { - if (this->device_class_.has_value()) - return *this->device_class_; - return ""; -} -void Sensor::set_device_class(const std::string &device_class) { this->device_class_ = device_class; } - void Sensor::set_state_class(StateClass state_class) { this->state_class_ = state_class; } StateClass Sensor::get_state_class() { if (this->state_class_.has_value()) diff --git a/esphome/components/sensor/sensor.h b/esphome/components/sensor/sensor.h index efcada1411..98356c943d 100644 --- a/esphome/components/sensor/sensor.h +++ b/esphome/components/sensor/sensor.h @@ -54,25 +54,15 @@ std::string state_class_to_string(StateClass state_class); * * A sensor has unit of measurement and can use publish_state to send out a new value with the specified accuracy. */ -class Sensor : public EntityBase { +class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBase_UnitOfMeasurement { public: explicit Sensor(); - /// Get the unit of measurement, using the manual override if set. - std::string get_unit_of_measurement(); - /// Manually set the unit of measurement. - void set_unit_of_measurement(const std::string &unit_of_measurement); - /// Get the accuracy in decimals, using the manual override if set. int8_t get_accuracy_decimals(); /// Manually set the accuracy in decimals. void set_accuracy_decimals(int8_t accuracy_decimals); - /// Get the device class, using the manual override if set. - std::string get_device_class(); - /// Manually set the device class. - void set_device_class(const std::string &device_class); - /// Get the state class, using the manual override if set. StateClass get_state_class(); /// Manually set the state class. @@ -163,9 +153,7 @@ class Sensor : public EntityBase { Filter *filter_list_{nullptr}; ///< Store all active filters. - optional unit_of_measurement_; ///< Unit of measurement override optional accuracy_decimals_; ///< Accuracy in decimals override - optional device_class_; ///< Device class override optional state_class_{STATE_CLASS_NONE}; ///< State class override bool force_update_{false}; ///< Force update mode bool has_state_{false}; diff --git a/esphome/components/sm2135/__init__.py b/esphome/components/sm2135/__init__.py index 68a9094518..ce78d5337f 100644 --- a/esphome/components/sm2135/__init__.py +++ b/esphome/components/sm2135/__init__.py @@ -8,17 +8,49 @@ from esphome.const import ( ) AUTO_LOAD = ["output"] -CODEOWNERS = ["@BoukeHaarsma23"] +CODEOWNERS = ["@BoukeHaarsma23", "@matika77", "@dd32"] sm2135_ns = cg.esphome_ns.namespace("sm2135") SM2135 = sm2135_ns.class_("SM2135", cg.Component) +CONF_RGB_CURRENT = "rgb_current" +CONF_CW_CURRENT = "cw_current" + +SM2135Current = sm2135_ns.enum("SM2135Current") + +DRIVE_STRENGTHS_CW = { + "10mA": SM2135Current.SM2135_CURRENT_10MA, + "15mA": SM2135Current.SM2135_CURRENT_15MA, + "20mA": SM2135Current.SM2135_CURRENT_20MA, + "25mA": SM2135Current.SM2135_CURRENT_25MA, + "30mA": SM2135Current.SM2135_CURRENT_30MA, + "35mA": SM2135Current.SM2135_CURRENT_35MA, + "40mA": SM2135Current.SM2135_CURRENT_40MA, + "45mA": SM2135Current.SM2135_CURRENT_45MA, + "50mA": SM2135Current.SM2135_CURRENT_50MA, + "55mA": SM2135Current.SM2135_CURRENT_55MA, + "60mA": SM2135Current.SM2135_CURRENT_60MA, +} +DRIVE_STRENGTHS_RGB = { + "10mA": SM2135Current.SM2135_CURRENT_10MA, + "15mA": SM2135Current.SM2135_CURRENT_15MA, + "20mA": SM2135Current.SM2135_CURRENT_20MA, + "25mA": SM2135Current.SM2135_CURRENT_25MA, + "30mA": SM2135Current.SM2135_CURRENT_30MA, + "35mA": SM2135Current.SM2135_CURRENT_35MA, + "40mA": SM2135Current.SM2135_CURRENT_40MA, + "45mA": SM2135Current.SM2135_CURRENT_45MA, +} + + MULTI_CONF = True CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(SM2135), cv.Required(CONF_DATA_PIN): pins.gpio_output_pin_schema, cv.Required(CONF_CLOCK_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_RGB_CURRENT, "20mA"): cv.enum(DRIVE_STRENGTHS_RGB), + cv.Optional(CONF_CW_CURRENT, "10mA"): cv.enum(DRIVE_STRENGTHS_CW), } ).extend(cv.COMPONENT_SCHEMA) @@ -31,3 +63,6 @@ async def to_code(config): cg.add(var.set_data_pin(data)) clock = await cg.gpio_pin_expression(config[CONF_CLOCK_PIN]) cg.add(var.set_clock_pin(clock)) + + cg.add(var.set_rgb_current(config[CONF_RGB_CURRENT])) + cg.add(var.set_cw_current(config[CONF_CW_CURRENT])) diff --git a/esphome/components/sm2135/sm2135.cpp b/esphome/components/sm2135/sm2135.cpp index 8bd64972c9..f9cd4235ed 100644 --- a/esphome/components/sm2135/sm2135.cpp +++ b/esphome/components/sm2135/sm2135.cpp @@ -19,63 +19,125 @@ static const uint8_t SM2135_ADDR_W = 0xC6; // Warm static const uint8_t SM2135_RGB = 0x00; // RGB channel static const uint8_t SM2135_CW = 0x80; // CW channel (Chip default) -static const uint8_t SM2135_10MA = 0x00; -static const uint8_t SM2135_15MA = 0x01; -static const uint8_t SM2135_20MA = 0x02; // RGB max current (Chip default) -static const uint8_t SM2135_25MA = 0x03; -static const uint8_t SM2135_30MA = 0x04; // CW max current (Chip default) -static const uint8_t SM2135_35MA = 0x05; -static const uint8_t SM2135_40MA = 0x06; -static const uint8_t SM2135_45MA = 0x07; // Max value for RGB -static const uint8_t SM2135_50MA = 0x08; -static const uint8_t SM2135_55MA = 0x09; -static const uint8_t SM2135_60MA = 0x0A; - -static const uint8_t SM2135_CURRENT = (SM2135_20MA << 4) | SM2135_10MA; - void SM2135::setup() { ESP_LOGCONFIG(TAG, "Setting up SM2135OutputComponent..."); this->data_pin_->setup(); - this->data_pin_->digital_write(true); + this->data_pin_->digital_write(false); + this->data_pin_->pin_mode(gpio::FLAG_OUTPUT); this->clock_pin_->setup(); - this->clock_pin_->digital_write(true); + this->clock_pin_->digital_write(false); + this->data_pin_->pin_mode(gpio::FLAG_OUTPUT); + + this->data_pin_->pin_mode(gpio::FLAG_PULLUP); + this->clock_pin_->pin_mode(gpio::FLAG_PULLUP); + this->pwm_amounts_.resize(5, 0); } + void SM2135::dump_config() { ESP_LOGCONFIG(TAG, "SM2135:"); LOG_PIN(" Data Pin: ", this->data_pin_); LOG_PIN(" Clock Pin: ", this->clock_pin_); + ESP_LOGCONFIG(TAG, " CW Current: %dmA", 10 + (this->cw_current_ * 5)); + ESP_LOGCONFIG(TAG, " RGB Current: %dmA", 10 + (this->rgb_current_ * 5)); +} + +void SM2135::write_byte_(uint8_t data) { + for (uint8_t mask = 0x80; mask; mask >>= 1) { + if (mask & data) { + this->sm2135_set_high_(this->data_pin_); + } else { + this->sm2135_set_low_(this->data_pin_); + } + + this->sm2135_set_high_(this->clock_pin_); + delayMicroseconds(4); + this->sm2135_set_low_(clock_pin_); + } + + this->sm2135_set_high_(this->data_pin_); + this->sm2135_set_high_(this->clock_pin_); + delayMicroseconds(2); + this->sm2135_set_low_(this->clock_pin_); + delayMicroseconds(2); + this->sm2135_set_low_(this->data_pin_); +} + +void SM2135::sm2135_start_() { + this->sm2135_set_low_(this->data_pin_); + delayMicroseconds(4); + this->sm2135_set_low_(this->clock_pin_); +} + +void SM2135::sm2135_stop_() { + this->sm2135_set_low_(this->data_pin_); + delayMicroseconds(4); + this->sm2135_set_high_(this->clock_pin_); + delayMicroseconds(4); + this->sm2135_set_high_(this->data_pin_); + delayMicroseconds(4); +} + +void SM2135::write_buffer_(uint8_t *buffer, uint8_t size) { + this->sm2135_start_(); + + this->data_pin_->digital_write(false); + for (uint32_t i = 0; i < size; i++) { + this->write_byte_(buffer[i]); + } + + this->sm2135_stop_(); } void SM2135::loop() { if (!this->update_) return; - uint8_t data[6]; + this->sm2135_start_(); + this->write_byte_(SM2135_ADDR_MC); + this->write_byte_(current_mask_); + if (this->update_channel_ == 3 || this->update_channel_ == 4) { // No color so must be Cold/Warm - data[0] = SM2135_ADDR_MC; - data[1] = SM2135_CURRENT; - data[2] = SM2135_CW; - this->write_buffer_(data, 3); + + this->write_byte_(SM2135_CW); + this->sm2135_stop_(); delay(1); - data[0] = SM2135_ADDR_C; - data[1] = this->pwm_amounts_[4]; // Warm - data[2] = this->pwm_amounts_[3]; // Cold - this->write_buffer_(data, 3); + this->sm2135_start_(); + this->write_byte_(SM2135_ADDR_C); + this->write_byte_(this->pwm_amounts_[4]); // Warm + this->write_byte_(this->pwm_amounts_[3]); // Cold } else { // Color - data[0] = SM2135_ADDR_MC; - data[1] = SM2135_CURRENT; - data[2] = SM2135_RGB; - data[3] = this->pwm_amounts_[1]; // Green - data[4] = this->pwm_amounts_[0]; // Red - data[5] = this->pwm_amounts_[2]; // Blue - this->write_buffer_(data, 6); + + this->write_byte_(SM2135_RGB); + this->write_byte_(this->pwm_amounts_[1]); // Green + this->write_byte_(this->pwm_amounts_[0]); // Red + this->write_byte_(this->pwm_amounts_[2]); // Blue } + this->sm2135_stop_(); + this->update_ = false; } +void SM2135::set_channel_value_(uint8_t channel, uint8_t value) { + if (this->pwm_amounts_[channel] != value) { + this->update_ = true; + this->update_channel_ = channel; + } + this->pwm_amounts_[channel] = value; +} + +void SM2135::sm2135_set_low_(GPIOPin *pin) { + pin->digital_write(false); + pin->pin_mode(gpio::FLAG_OUTPUT); +} + +void SM2135::sm2135_set_high_(GPIOPin *pin) { + pin->digital_write(true); + pin->pin_mode(gpio::FLAG_PULLUP); +} + } // namespace sm2135 } // namespace esphome diff --git a/esphome/components/sm2135/sm2135.h b/esphome/components/sm2135/sm2135.h index 0277e9ba1c..a557fc3287 100644 --- a/esphome/components/sm2135/sm2135.h +++ b/esphome/components/sm2135/sm2135.h @@ -1,19 +1,43 @@ #pragma once +#include +#include "esphome/components/output/float_output.h" #include "esphome/core/component.h" #include "esphome/core/hal.h" -#include "esphome/components/output/float_output.h" -#include namespace esphome { namespace sm2135 { +enum SM2135Current : uint8_t { + SM2135_CURRENT_10MA = 0x00, + SM2135_CURRENT_15MA = 0x01, + SM2135_CURRENT_20MA = 0x02, + SM2135_CURRENT_25MA = 0x03, + SM2135_CURRENT_30MA = 0x04, + SM2135_CURRENT_35MA = 0x05, + SM2135_CURRENT_40MA = 0x06, + SM2135_CURRENT_45MA = 0x07, // Max value for RGB + SM2135_CURRENT_50MA = 0x08, + SM2135_CURRENT_55MA = 0x09, + SM2135_CURRENT_60MA = 0x0A, +}; + class SM2135 : public Component { public: class Channel; - void set_data_pin(GPIOPin *data_pin) { data_pin_ = data_pin; } - void set_clock_pin(GPIOPin *clock_pin) { clock_pin_ = clock_pin; } + void set_data_pin(GPIOPin *data_pin) { this->data_pin_ = data_pin; } + void set_clock_pin(GPIOPin *clock_pin) { this->clock_pin_ = clock_pin; } + + void set_rgb_current(SM2135Current rgb_current) { + this->rgb_current_ = rgb_current; + this->current_mask_ = (this->rgb_current_ << 4) | this->cw_current_; + } + + void set_cw_current(SM2135Current cw_current) { + this->cw_current_ = cw_current; + this->current_mask_ = (this->rgb_current_ << 4) | this->cw_current_; + } void setup() override; @@ -40,40 +64,20 @@ class SM2135 : public Component { }; protected: - void set_channel_value_(uint8_t channel, uint8_t value) { - if (this->pwm_amounts_[channel] != value) { - this->update_ = true; - this->update_channel_ = channel; - } - this->pwm_amounts_[channel] = value; - } - void write_bit_(bool value) { - this->clock_pin_->digital_write(false); - this->data_pin_->digital_write(value); - this->clock_pin_->digital_write(true); - } + void set_channel_value_(uint8_t channel, uint8_t value); + void sm2135_set_low_(GPIOPin *pin); + void sm2135_set_high_(GPIOPin *pin); - void write_byte_(uint8_t data) { - for (uint8_t mask = 0x80; mask; mask >>= 1) { - this->write_bit_(data & mask); - } - this->clock_pin_->digital_write(false); - this->data_pin_->digital_write(true); - this->clock_pin_->digital_write(true); - } - - void write_buffer_(uint8_t *buffer, uint8_t size) { - this->data_pin_->digital_write(false); - for (uint32_t i = 0; i < size; i++) { - this->write_byte_(buffer[i]); - } - this->clock_pin_->digital_write(false); - this->clock_pin_->digital_write(true); - this->data_pin_->digital_write(true); - } + void sm2135_start_(); + void sm2135_stop_(); + void write_byte_(uint8_t data); + void write_buffer_(uint8_t *buffer, uint8_t size); GPIOPin *data_pin_; GPIOPin *clock_pin_; + uint8_t current_mask_; + SM2135Current rgb_current_; + SM2135Current cw_current_; uint8_t update_channel_; std::vector pwm_amounts_; bool update_{true}; diff --git a/esphome/components/socket/__init__.py b/esphome/components/socket/__init__.py index 81203fdc31..1757ec4668 100644 --- a/esphome/components/socket/__init__.py +++ b/esphome/components/socket/__init__.py @@ -14,6 +14,7 @@ CONFIG_SCHEMA = cv.Schema( esp8266=IMPLEMENTATION_LWIP_TCP, esp32=IMPLEMENTATION_BSD_SOCKETS, rp2040=IMPLEMENTATION_LWIP_TCP, + host=IMPLEMENTATION_BSD_SOCKETS, ): cv.one_of( IMPLEMENTATION_LWIP_TCP, IMPLEMENTATION_BSD_SOCKETS, lower=True, space="_" ), diff --git a/esphome/components/socket/headers.h b/esphome/components/socket/headers.h index 20d8fdb8c9..1922885ac0 100644 --- a/esphome/components/socket/headers.h +++ b/esphome/components/socket/headers.h @@ -130,6 +130,13 @@ struct iovec { #include #include +#ifdef USE_HOST +#include +#include +#include +#include +#endif // USE_HOST + #ifdef USE_ARDUINO // arduino-esp32 declares a global var called INADDR_NONE which is replaced // by the define diff --git a/esphome/components/speaker/__init__.py b/esphome/components/speaker/__init__.py new file mode 100644 index 0000000000..79d5df8c5a --- /dev/null +++ b/esphome/components/speaker/__init__.py @@ -0,0 +1,87 @@ +from esphome import automation +import esphome.config_validation as cv +import esphome.codegen as cg + +from esphome.automation import maybe_simple_id +from esphome.const import CONF_ID, CONF_DATA +from esphome.core import CORE +from esphome.coroutine import coroutine_with_priority + + +CODEOWNERS = ["@jesserockz"] + +IS_PLATFORM_COMPONENT = True + +speaker_ns = cg.esphome_ns.namespace("speaker") + +Speaker = speaker_ns.class_("Speaker") + +PlayAction = speaker_ns.class_( + "PlayAction", automation.Action, cg.Parented.template(Speaker) +) +StopAction = speaker_ns.class_( + "StopAction", automation.Action, cg.Parented.template(Speaker) +) + +IsPlayingCondition = speaker_ns.class_("IsPlayingCondition", automation.Condition) + + +async def setup_speaker_core_(var, config): + pass + + +async def register_speaker(var, config): + if not CORE.has_id(config[CONF_ID]): + var = cg.Pvariable(config[CONF_ID], var) + await setup_speaker_core_(var, config) + + +SPEAKER_SCHEMA = cv.Schema({}) + + +SPEAKER_AUTOMATION_SCHEMA = maybe_simple_id({cv.GenerateID(): cv.use_id(Speaker)}) + + +async def speaker_action(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + +@automation.register_action( + "speaker.play", + PlayAction, + cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(Speaker), + cv.Required(CONF_DATA): cv.templatable(cv.ensure_list(cv.hex_uint8_t)), + }, + key=CONF_DATA, + ), +) +async def speaker_play_action(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + data = config[CONF_DATA] + + if cg.is_template(data): + templ = await cg.templatable(data, args, cg.std_vector.template(cg.uint8)) + cg.add(var.set_data_template(templ)) + else: + cg.add(var.set_data_static(data)) + return var + + +automation.register_action("speaker.stop", StopAction, SPEAKER_AUTOMATION_SCHEMA)( + speaker_action +) + +automation.register_condition( + "speaker.is_playing", IsPlayingCondition, SPEAKER_AUTOMATION_SCHEMA +)(speaker_action) + + +@coroutine_with_priority(100.0) +async def to_code(config): + cg.add_global(speaker_ns.using) + cg.add_define("USE_SPEAKER") diff --git a/esphome/components/speaker/automation.h b/esphome/components/speaker/automation.h new file mode 100644 index 0000000000..e28991a0d1 --- /dev/null +++ b/esphome/components/speaker/automation.h @@ -0,0 +1,48 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "speaker.h" + +#include + +namespace esphome { +namespace speaker { + +template class PlayAction : public Action, public Parented { + public: + void set_data_template(std::function(Ts...)> func) { + this->data_func_ = func; + this->static_ = false; + } + void set_data_static(const std::vector &data) { + this->data_static_ = data; + this->static_ = true; + } + + void play(Ts... x) override { + if (this->static_) { + this->parent_->play(this->data_static_); + } else { + auto val = this->data_func_(x...); + this->parent_->play(val); + } + } + + protected: + bool static_{false}; + std::function(Ts...)> data_func_{}; + std::vector data_static_{}; +}; + +template class StopAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->stop(); } +}; + +template class IsPlayingCondition : public Condition, public Parented { + public: + bool check(Ts... x) override { return this->parent_->is_running(); } +}; + +} // namespace speaker +} // namespace esphome diff --git a/esphome/components/speaker/speaker.h b/esphome/components/speaker/speaker.h new file mode 100644 index 0000000000..5dfabfa40e --- /dev/null +++ b/esphome/components/speaker/speaker.h @@ -0,0 +1,27 @@ +#pragma once + +namespace esphome { +namespace speaker { + +enum State : uint8_t { + STATE_STOPPED = 0, + STATE_STARTING, + STATE_RUNNING, + STATE_STOPPING, +}; + +class Speaker { + public: + virtual bool play(const uint8_t *data, size_t length) = 0; + virtual bool play(const std::vector &data) { return this->play(data.data(), data.size()); } + + virtual void stop() = 0; + + bool is_running() const { return this->state_ == STATE_RUNNING; } + + protected: + State state_{STATE_STOPPED}; +}; + +} // namespace speaker +} // namespace esphome diff --git a/esphome/components/sun/sun.cpp b/esphome/components/sun/sun.cpp index 54aaf0942b..5f9179682a 100644 --- a/esphome/components/sun/sun.cpp +++ b/esphome/components/sun/sun.cpp @@ -287,18 +287,17 @@ HorizontalCoordinate Sun::calc_coords_() { */ return sun.true_coordinate(m); } -optional Sun::calc_event_(bool rising, double zenith) { +optional Sun::calc_event_(time::ESPTime date, bool rising, double zenith) { SunAtLocation sun{location_}; - auto now = this->time_->utcnow(); - if (!now.is_valid()) + if (!date.is_valid()) return {}; // Calculate UT1 timestamp at 0h - auto today = now; + auto today = date; today.hour = today.minute = today.second = 0; today.recalc_timestamp_utc(); auto it = sun.event(rising, today, zenith); - if (it.has_value() && it->timestamp < now.timestamp) { + if (it.has_value() && it->timestamp < date.timestamp) { // We're calculating *next* sunrise/sunset, but calculated event // is today, so try again tomorrow time_t new_timestamp = today.timestamp + 24 * 60 * 60; @@ -307,9 +306,19 @@ optional Sun::calc_event_(bool rising, double zenith) { } return it; } +optional Sun::calc_event_(bool rising, double zenith) { + auto it = Sun::calc_event_(this->time_->utcnow(), rising, zenith); + return it; +} optional Sun::sunrise(double elevation) { return this->calc_event_(true, 90 - elevation); } optional Sun::sunset(double elevation) { return this->calc_event_(false, 90 - elevation); } +optional Sun::sunrise(time::ESPTime date, double elevation) { + return this->calc_event_(date, true, 90 - elevation); +} +optional Sun::sunset(time::ESPTime date, double elevation) { + return this->calc_event_(date, false, 90 - elevation); +} double Sun::elevation() { return this->calc_coords_().elevation; } double Sun::azimuth() { return this->calc_coords_().azimuth; } diff --git a/esphome/components/sun/sun.h b/esphome/components/sun/sun.h index efc6a1ab0a..9547b2f280 100644 --- a/esphome/components/sun/sun.h +++ b/esphome/components/sun/sun.h @@ -59,6 +59,8 @@ class Sun { optional sunrise(double elevation); optional sunset(double elevation); + optional sunrise(time::ESPTime date, double elevation); + optional sunset(time::ESPTime date, double elevation); double elevation(); double azimuth(); @@ -66,6 +68,7 @@ class Sun { protected: internal::HorizontalCoordinate calc_coords_(); optional calc_event_(bool rising, double zenith); + optional calc_event_(time::ESPTime date, bool rising, double zenith); time::RealTimeClock *time_; internal::GeoLocation location_; diff --git a/esphome/components/switch/switch.cpp b/esphome/components/switch/switch.cpp index 72e7add158..96611b0b87 100644 --- a/esphome/components/switch/switch.cpp +++ b/esphome/components/switch/switch.cpp @@ -63,13 +63,6 @@ void Switch::add_on_state_callback(std::function &&callback) { void Switch::set_inverted(bool inverted) { this->inverted_ = inverted; } bool Switch::is_inverted() const { return this->inverted_; } -std::string Switch::get_device_class() { - if (this->device_class_.has_value()) - return *this->device_class_; - return ""; -} -void Switch::set_device_class(const std::string &device_class) { this->device_class_ = device_class; } - void log_switch(const char *tag, const char *prefix, const char *type, Switch *obj) { if (obj != nullptr) { ESP_LOGCONFIG(tag, "%s%s '%s'", prefix, type, obj->get_name().c_str()); diff --git a/esphome/components/switch/switch.h b/esphome/components/switch/switch.h index 8bea3b36db..9daac4ee23 100644 --- a/esphome/components/switch/switch.h +++ b/esphome/components/switch/switch.h @@ -29,7 +29,7 @@ enum SwitchRestoreMode { * A switch is basically just a combination of a binary sensor (for reporting switch values) * and a write_state method that writes a state to the hardware. */ -class Switch : public EntityBase { +class Switch : public EntityBase, public EntityBase_DeviceClass { public: explicit Switch(); @@ -103,10 +103,6 @@ class Switch : public EntityBase { bool is_inverted() const; - /// Get the device class for this switch. - std::string get_device_class(); - /// Set the Home Assistant device class for this switch. - void set_device_class(const std::string &device_class); void set_restore_mode(SwitchRestoreMode restore_mode) { this->restore_mode = restore_mode; } protected: @@ -124,7 +120,6 @@ class Switch : public EntityBase { bool inverted_{false}; Deduplicator publish_dedup_; ESPPreferenceObject rtc_; - optional device_class_; }; #define LOG_SWITCH(prefix, type, obj) log_switch((TAG), (prefix), LOG_STR_LITERAL(type), (obj)) diff --git a/esphome/components/template/cover/__init__.py b/esphome/components/template/cover/__init__.py index a628da70d2..8844ddd6ab 100644 --- a/esphome/components/template/cover/__init__.py +++ b/esphome/components/template/cover/__init__.py @@ -73,6 +73,7 @@ async def to_code(config): await automation.build_automation( var.get_stop_trigger(), [], config[CONF_STOP_ACTION] ) + cg.add(var.set_has_stop(True)) if CONF_TILT_ACTION in config: await automation.build_automation( var.get_tilt_trigger(), [(float, "tilt")], config[CONF_TILT_ACTION] diff --git a/esphome/components/template/cover/template_cover.cpp b/esphome/components/template/cover/template_cover.cpp index 47c651e643..b16e439943 100644 --- a/esphome/components/template/cover/template_cover.cpp +++ b/esphome/components/template/cover/template_cover.cpp @@ -109,6 +109,7 @@ void TemplateCover::control(const CoverCall &call) { CoverTraits TemplateCover::get_traits() { auto traits = CoverTraits(); traits.set_is_assumed_state(this->assumed_state_); + traits.set_supports_stop(this->has_stop_); traits.set_supports_position(this->has_position_); traits.set_supports_tilt(this->has_tilt_); return traits; @@ -116,6 +117,7 @@ CoverTraits TemplateCover::get_traits() { Trigger *TemplateCover::get_position_trigger() const { return this->position_trigger_; } Trigger *TemplateCover::get_tilt_trigger() const { return this->tilt_trigger_; } void TemplateCover::set_tilt_lambda(std::function()> &&tilt_f) { this->tilt_f_ = tilt_f; } +void TemplateCover::set_has_stop(bool has_stop) { this->has_stop_ = has_stop; } void TemplateCover::set_has_position(bool has_position) { this->has_position_ = has_position; } void TemplateCover::set_has_tilt(bool has_tilt) { this->has_tilt_ = has_tilt; } void TemplateCover::stop_prev_trigger_() { diff --git a/esphome/components/template/cover/template_cover.h b/esphome/components/template/cover/template_cover.h index 3b9dcea50b..4ff5caf1db 100644 --- a/esphome/components/template/cover/template_cover.h +++ b/esphome/components/template/cover/template_cover.h @@ -26,6 +26,7 @@ class TemplateCover : public cover::Cover, public Component { void set_optimistic(bool optimistic); void set_assumed_state(bool assumed_state); void set_tilt_lambda(std::function()> &&tilt_f); + void set_has_stop(bool has_stop); void set_has_position(bool has_position); void set_has_tilt(bool has_tilt); void set_restore_mode(TemplateCoverRestoreMode restore_mode) { restore_mode_ = restore_mode; } @@ -48,6 +49,7 @@ class TemplateCover : public cover::Cover, public Component { bool optimistic_{false}; Trigger<> *open_trigger_; Trigger<> *close_trigger_; + bool has_stop_{false}; Trigger<> *stop_trigger_; Trigger<> *prev_command_trigger_{nullptr}; Trigger *position_trigger_; diff --git a/esphome/components/template/sensor/template_sensor.cpp b/esphome/components/template/sensor/template_sensor.cpp index b28eb3fed2..f2d0e7363e 100644 --- a/esphome/components/template/sensor/template_sensor.cpp +++ b/esphome/components/template/sensor/template_sensor.cpp @@ -8,13 +8,12 @@ namespace template_ { static const char *const TAG = "template.sensor"; void TemplateSensor::update() { - if (this->f_.has_value()) { - auto val = (*this->f_)(); - if (val.has_value()) { - this->publish_state(*val); - } - } else if (!std::isnan(this->get_raw_state())) { - this->publish_state(this->get_raw_state()); + if (!this->f_.has_value()) + return; + + auto val = (*this->f_)(); + if (val.has_value()) { + this->publish_state(*val); } } float TemplateSensor::get_setup_priority() const { return setup_priority::HARDWARE; } diff --git a/esphome/components/template/text_sensor/template_text_sensor.cpp b/esphome/components/template/text_sensor/template_text_sensor.cpp index 83bebb5bcf..885ad47bbf 100644 --- a/esphome/components/template/text_sensor/template_text_sensor.cpp +++ b/esphome/components/template/text_sensor/template_text_sensor.cpp @@ -7,13 +7,12 @@ namespace template_ { static const char *const TAG = "template.text_sensor"; void TemplateTextSensor::update() { - if (this->f_.has_value()) { - auto val = (*this->f_)(); - if (val.has_value()) { - this->publish_state(*val); - } - } else if (this->has_state()) { - this->publish_state(this->state); + if (!this->f_.has_value()) + return; + + auto val = (*this->f_)(); + if (val.has_value()) { + this->publish_state(*val); } } float TemplateTextSensor::get_setup_priority() const { return setup_priority::HARDWARE; } diff --git a/esphome/components/time_based/time_based_cover.cpp b/esphome/components/time_based/time_based_cover.cpp index a7ba6d0595..50376224a9 100644 --- a/esphome/components/time_based/time_based_cover.cpp +++ b/esphome/components/time_based/time_based_cover.cpp @@ -51,6 +51,7 @@ void TimeBasedCover::loop() { float TimeBasedCover::get_setup_priority() const { return setup_priority::DATA; } CoverTraits TimeBasedCover::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_); diff --git a/esphome/components/tuya/cover/tuya_cover.cpp b/esphome/components/tuya/cover/tuya_cover.cpp index 11a458449f..fcb961f45e 100644 --- a/esphome/components/tuya/cover/tuya_cover.cpp +++ b/esphome/components/tuya/cover/tuya_cover.cpp @@ -128,6 +128,7 @@ void TuyaCover::dump_config() { cover::CoverTraits TuyaCover::get_traits() { auto traits = cover::CoverTraits(); + traits.set_supports_stop(true); traits.set_supports_position(true); return traits; } diff --git a/esphome/components/voice_assistant/__init__.py b/esphome/components/voice_assistant/__init__.py index 20698a1b82..624fcdf52c 100644 --- a/esphome/components/voice_assistant/__init__.py +++ b/esphome/components/voice_assistant/__init__.py @@ -1,10 +1,10 @@ import esphome.config_validation as cv import esphome.codegen as cg -from esphome.const import CONF_ID, CONF_MICROPHONE +from esphome.const import CONF_ID, CONF_MICROPHONE, CONF_SPEAKER from esphome import automation from esphome.automation import register_action -from esphome.components import microphone +from esphome.components import microphone, speaker AUTO_LOAD = ["socket"] DEPENDENCIES = ["api", "microphone"] @@ -34,6 +34,7 @@ CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(VoiceAssistant), cv.GenerateID(CONF_MICROPHONE): cv.use_id(microphone.Microphone), + cv.Optional(CONF_SPEAKER): cv.use_id(speaker.Speaker), cv.Optional(CONF_ON_START): automation.validate_automation(single=True), cv.Optional(CONF_ON_STT_END): automation.validate_automation(single=True), cv.Optional(CONF_ON_TTS_START): automation.validate_automation(single=True), @@ -51,6 +52,10 @@ async def to_code(config): mic = await cg.get_variable(config[CONF_MICROPHONE]) cg.add(var.set_microphone(mic)) + if CONF_SPEAKER in config: + spkr = await cg.get_variable(config[CONF_SPEAKER]) + cg.add(var.set_speaker(spkr)) + if CONF_ON_START in config: await automation.build_automation( var.get_start_trigger(), [], config[CONF_ON_START] diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index e2d5bea90a..fb96d484d4 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -1,7 +1,11 @@ #include "voice_assistant.h" +#ifdef USE_VOICE_ASSISTANT + #include "esphome/core/log.h" +#include + namespace esphome { namespace voice_assistant { @@ -33,6 +37,27 @@ void VoiceAssistant::setup() { return; } +#ifdef USE_SPEAKER + if (this->speaker_ != nullptr) { + struct sockaddr_storage server; + + socklen_t sl = socket::set_sockaddr_any((struct sockaddr *) &server, sizeof(server), 6055); + if (sl == 0) { + ESP_LOGW(TAG, "Socket unable to set sockaddr: errno %d", errno); + this->mark_failed(); + return; + } + server.ss_family = AF_INET; + + err = socket_->bind((struct sockaddr *) &server, sizeof(server)); + if (err != 0) { + ESP_LOGW(TAG, "Socket unable to bind: errno %d", errno); + this->mark_failed(); + return; + } + } +#endif + this->mic_->add_data_callback([this](const std::vector &data) { if (!this->running_) { return; @@ -41,6 +66,21 @@ void VoiceAssistant::setup() { }); } +void VoiceAssistant::loop() { +#ifdef USE_SPEAKER + if (this->speaker_ == nullptr) { + return; + } + + uint8_t buf[1024]; + auto len = this->socket_->read(buf, sizeof(buf)); + if (len == -1) { + return; + } + this->speaker_->play(buf, len); +#endif +} + void VoiceAssistant::start(struct sockaddr_storage *addr, uint16_t port) { ESP_LOGD(TAG, "Starting..."); @@ -154,3 +194,5 @@ VoiceAssistant *global_voice_assistant = nullptr; // NOLINT(cppcoreguidelines-a } // namespace voice_assistant } // namespace esphome + +#endif // USE_VOICE_ASSISTANT diff --git a/esphome/components/voice_assistant/voice_assistant.h b/esphome/components/voice_assistant/voice_assistant.h index 813c006e98..c1a6e8883b 100644 --- a/esphome/components/voice_assistant/voice_assistant.h +++ b/esphome/components/voice_assistant/voice_assistant.h @@ -1,24 +1,49 @@ #pragma once +#include "esphome/core/defines.h" + +#ifdef USE_VOICE_ASSISTANT + #include "esphome/core/automation.h" #include "esphome/core/component.h" +#include "esphome/core/defines.h" #include "esphome/core/helpers.h" #include "esphome/components/api/api_pb2.h" #include "esphome/components/api/api_server.h" #include "esphome/components/microphone/microphone.h" +#ifdef USE_SPEAKER +#include "esphome/components/speaker/speaker.h" +#endif #include "esphome/components/socket/socket.h" namespace esphome { namespace voice_assistant { +// Version 1: Initial version +// Version 2: Adds raw speaker support +static const uint32_t INITIAL_VERSION = 1; +static const uint32_t SPEAKER_SUPPORT = 2; + class VoiceAssistant : public Component { public: void setup() override; + void loop() override; float get_setup_priority() const override; void start(struct sockaddr_storage *addr, uint16_t port); void set_microphone(microphone::Microphone *mic) { this->mic_ = mic; } +#ifdef USE_SPEAKER + void set_speaker(speaker::Speaker *speaker) { this->speaker_ = speaker; } +#endif + + uint32_t get_version() const { +#ifdef USE_SPEAKER + if (this->speaker_ != nullptr) + return SPEAKER_SUPPORT; +#endif + return INITIAL_VERSION; + } void request_start(); void signal_stop(); @@ -44,6 +69,9 @@ class VoiceAssistant : public Component { Trigger *error_trigger_ = new Trigger(); microphone::Microphone *mic_{nullptr}; +#ifdef USE_SPEAKER + speaker::Speaker *speaker_{nullptr}; +#endif bool running_{false}; }; @@ -62,3 +90,5 @@ extern VoiceAssistant *global_voice_assistant; // NOLINT(cppcoreguidelines-avoi } // namespace voice_assistant } // namespace esphome + +#endif // USE_VOICE_ASSISTANT diff --git a/esphome/components/waveshare_epaper/display.py b/esphome/components/waveshare_epaper/display.py index 747794b631..d0276f119a 100644 --- a/esphome/components/waveshare_epaper/display.py +++ b/esphome/components/waveshare_epaper/display.py @@ -39,6 +39,9 @@ WaveshareEPaper4P2InBV2 = waveshare_epaper_ns.class_( WaveshareEPaper5P8In = waveshare_epaper_ns.class_( "WaveshareEPaper5P8In", WaveshareEPaper ) +WaveshareEPaper5P8InV2 = waveshare_epaper_ns.class_( + "WaveshareEPaper5P8InV2", WaveshareEPaper +) WaveshareEPaper7P5In = waveshare_epaper_ns.class_( "WaveshareEPaper7P5In", WaveshareEPaper ) @@ -80,6 +83,7 @@ MODELS = { "4.20in": ("b", WaveshareEPaper4P2In), "4.20in-bv2": ("b", WaveshareEPaper4P2InBV2), "5.83in": ("b", WaveshareEPaper5P8In), + "5.83inv2": ("b", WaveshareEPaper5P8InV2), "7.50in": ("b", WaveshareEPaper7P5In), "7.50in-bv2": ("b", WaveshareEPaper7P5InBV2), "7.50in-bc": ("b", WaveshareEPaper7P5InBC), diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.cpp b/esphome/components/waveshare_epaper/waveshare_epaper.cpp index 8c4b137514..42f5bc54e3 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.cpp +++ b/esphome/components/waveshare_epaper/waveshare_epaper.cpp @@ -1037,6 +1037,88 @@ void WaveshareEPaper5P8In::dump_config() { LOG_PIN(" Busy Pin: ", this->busy_pin_); LOG_UPDATE_INTERVAL(this); } + +// ======================================================== +// 5.83in V2 +// Datasheet/Specification/Reference: +// - https://www.waveshare.com/w/upload/3/37/5.83inch_e-Paper_V2_Specification.pdf +// - https://github.com/waveshare/e-Paper/blob/master/Arduino/epd5in83_V2/epd5in83_V2.cpp +// ======================================================== +void WaveshareEPaper5P8InV2::initialize() { + // COMMAND POWER SETTING + this->command(0x01); + this->data(0x07); + this->data(0x07); + this->data(0x3f); + this->data(0x3f); + + // COMMAND POWER ON + this->command(0x04); + delay(10); + this->wait_until_idle_(); + + // PANNEL SETTING + this->command(0x00); + this->data(0x1F); + + // COMMAND RESOLUTION SETTING + this->command(0x61); + this->data(0x02); + this->data(0x88); + this->data(0x01); + this->data(0xE0); + + this->command(0x15); + this->data(0x00); + + // COMMAND TCON SETTING + this->command(0x60); + this->data(0x22); + + // Do we need this? + // COMMAND PLL CONTROL + this->command(0x30); + this->data(0x3C); // 3A 100HZ 29 150Hz 39 200HZ 31 171HZ +} +void HOT WaveshareEPaper5P8InV2::display() { + // Reuse the code from WaveshareEPaper4P2In::display() + // COMMAND VCM DC SETTING REGISTER + this->command(0x82); + this->data(0x12); + + // COMMAND VCOM AND DATA INTERVAL SETTING + this->command(0x50); + this->data(0x97); + + // COMMAND DATA START TRANSMISSION 1 + this->command(0x10); + delay(2); + this->start_data_(); + this->write_array(this->buffer_, this->get_buffer_length_()); + this->end_data_(); + delay(2); + + // COMMAND DATA START TRANSMISSION 2 + this->command(0x13); + delay(2); + this->start_data_(); + this->write_array(this->buffer_, this->get_buffer_length_()); + this->end_data_(); + + // COMMAND DISPLAY REFRESH + this->command(0x12); +} +int WaveshareEPaper5P8InV2::get_width_internal() { return 648; } +int WaveshareEPaper5P8InV2::get_height_internal() { return 480; } +void WaveshareEPaper5P8InV2::dump_config() { + LOG_DISPLAY("", "Waveshare E-Paper", this); + ESP_LOGCONFIG(TAG, " Model: 5.83inv2"); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + LOG_PIN(" DC Pin: ", this->dc_pin_); + LOG_PIN(" Busy Pin: ", this->busy_pin_); + LOG_UPDATE_INTERVAL(this); +} + void WaveshareEPaper7P5InBV2::initialize() { // COMMAND POWER SETTING this->command(0x01); diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.h b/esphome/components/waveshare_epaper/waveshare_epaper.h index a674d3af0c..1cb46bdb9d 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.h +++ b/esphome/components/waveshare_epaper/waveshare_epaper.h @@ -284,6 +284,49 @@ class WaveshareEPaper5P8In : public WaveshareEPaper { int get_height_internal() override; }; +class WaveshareEPaper5P8InV2 : public WaveshareEPaper { + public: + void initialize() override; + + void display() override; + + void dump_config() override; + + void deep_sleep() override { + // COMMAND VCOM AND DATA INTERVAL SETTING + this->command(0x50); + this->data(0x17); // border floating + + // COMMAND VCM DC SETTING + this->command(0x82); + // COMMAND PANEL SETTING + this->command(0x00); + + delay(100); // NOLINT + + // COMMAND POWER SETTING + this->command(0x01); + this->data(0x00); + this->data(0x00); + this->data(0x00); + this->data(0x00); + this->data(0x00); + delay(100); // NOLINT + + // COMMAND POWER OFF + this->command(0x02); + this->wait_until_idle_(); + // COMMAND DEEP SLEEP + this->command(0x07); + this->data(0xA5); // check byte + } + + protected: + int get_width_internal() override; + + int get_height_internal() override; +}; + class WaveshareEPaper7P5In : public WaveshareEPaper { public: void initialize() override; diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 80a53a7515..00b2e20015 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -428,6 +428,9 @@ void WebServer::on_switch_update(switch_::Switch *obj, bool state) { std::string WebServer::switch_json(switch_::Switch *obj, bool value, JsonDetail start_config) { return json::build_json([obj, value, start_config](JsonObject root) { set_json_icon_state_value(root, obj, "switch-" + obj->get_object_id(), value ? "ON" : "OFF", value, start_config); + if (start_config == DETAIL_ALL) { + root["assumed_state"] = obj->assumed_state(); + } }); } void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlMatch &match) { diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index f5684f06f7..c9da07795c 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -252,6 +252,7 @@ def _validate(config): CONF_OUTPUT_POWER = "output_power" +CONF_PASSIVE_SCAN = "passive_scan" CONFIG_SCHEMA = cv.All( cv.Schema( { @@ -280,6 +281,7 @@ CONFIG_SCHEMA = cv.All( cv.SplitDefault(CONF_ENABLE_RRM, esp32_idf=False): cv.All( cv.boolean, cv.only_with_esp_idf ), + cv.Optional(CONF_PASSIVE_SCAN, default=False): cv.boolean, cv.Optional("enable_mdns"): cv.invalid( "This option has been removed. Please use the [disabled] option under the " "new mdns component instead." @@ -379,6 +381,7 @@ async def to_code(config): cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT])) cg.add(var.set_power_save_mode(config[CONF_POWER_SAVE_MODE])) cg.add(var.set_fast_connect(config[CONF_FAST_CONNECT])) + cg.add(var.set_passive_scan(config[CONF_PASSIVE_SCAN])) if CONF_OUTPUT_POWER in config: cg.add(var.set_output_power(config[CONF_OUTPUT_POWER])) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index efb1af171d..9f047dd5ed 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -385,7 +385,7 @@ void WiFiComponent::print_connect_params_() { void WiFiComponent::start_scanning() { this->action_started_ = millis(); ESP_LOGD(TAG, "Starting scan..."); - this->wifi_scan_start_(); + this->wifi_scan_start_(this->passive_scan_); this->state_ = WIFI_COMPONENT_STATE_STA_SCANNING; } @@ -615,6 +615,8 @@ bool WiFiComponent::is_connected() { } void WiFiComponent::set_power_save_mode(WiFiPowerSaveMode power_save) { this->power_save_ = power_save; } +void WiFiComponent::set_passive_scan(bool passive) { this->passive_scan_ = passive; } + std::string WiFiComponent::format_mac_addr(const uint8_t *mac) { char buf[20]; sprintf(buf, "%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 544cb3dc61..3f81b94cce 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -217,6 +217,8 @@ class WiFiComponent : public Component { void set_power_save_mode(WiFiPowerSaveMode power_save); void set_output_power(float output_power) { output_power_ = output_power; } + void set_passive_scan(bool passive); + void save_wifi_sta(const std::string &ssid, const std::string &password); // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) @@ -294,7 +296,7 @@ class WiFiComponent : public Component { bool wifi_sta_connect_(const WiFiAP &ap); void wifi_pre_setup_(); WiFiSTAConnectStatus wifi_sta_connect_status_(); - bool wifi_scan_start_(); + bool wifi_scan_start_(bool passive); bool wifi_ap_ip_config_(optional manual_ip); bool wifi_start_ap_(const WiFiAP &ap); bool wifi_disconnect_(); @@ -349,6 +351,7 @@ class WiFiComponent : public Component { bool scan_done_{false}; bool ap_setup_{false}; optional output_power_; + bool passive_scan_{false}; ESPPreferenceObject pref_; bool has_saved_wifi_settings_{false}; #ifdef USE_WIFI_11KV_SUPPORT diff --git a/esphome/components/wifi/wifi_component_esp32_arduino.cpp b/esphome/components/wifi/wifi_component_esp32_arduino.cpp index ab04224161..f35f5dfc43 100644 --- a/esphome/components/wifi/wifi_component_esp32_arduino.cpp +++ b/esphome/components/wifi/wifi_component_esp32_arduino.cpp @@ -618,13 +618,13 @@ WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() { } return WiFiSTAConnectStatus::IDLE; } -bool WiFiComponent::wifi_scan_start_() { +bool WiFiComponent::wifi_scan_start_(bool passive) { // enable STA if (!this->wifi_mode_(true, {})) return false; // need to use WiFi because of WiFiScanClass allocations :( - int16_t err = WiFi.scanNetworks(true, true, false, 200); + int16_t err = WiFi.scanNetworks(true, true, passive, 200); if (err != WIFI_SCAN_RUNNING) { ESP_LOGV(TAG, "WiFi.scanNetworks failed! %d", err); return false; diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index de4253fe41..a28aa8b858 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -601,7 +601,7 @@ WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() { return WiFiSTAConnectStatus::IDLE; } } -bool WiFiComponent::wifi_scan_start_() { +bool WiFiComponent::wifi_scan_start_(bool passive) { static bool first_scan = false; // enable STA @@ -615,13 +615,21 @@ bool WiFiComponent::wifi_scan_start_() { config.channel = 0; config.show_hidden = 1; #if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 4, 0) - config.scan_type = WIFI_SCAN_TYPE_ACTIVE; + config.scan_type = passive ? WIFI_SCAN_TYPE_PASSIVE : WIFI_SCAN_TYPE_ACTIVE; if (first_scan) { - config.scan_time.active.min = 100; - config.scan_time.active.max = 200; + if (passive) { + config.scan_time.passive = 200; + } else { + config.scan_time.active.min = 100; + config.scan_time.active.max = 200; + } } else { - config.scan_time.active.min = 400; - config.scan_time.active.max = 500; + if (passive) { + config.scan_time.passive = 500; + } else { + config.scan_time.active.min = 400; + config.scan_time.active.max = 500; + } } #endif first_scan = false; @@ -698,6 +706,7 @@ bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { #endif struct dhcps_lease lease {}; + lease.enable = true; network::IPAddress start_address = info.ip.addr; start_address[3] += 99; lease.start_ip.addr = static_cast(start_address); diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 1edde74743..1c70f33040 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -17,6 +17,7 @@ #ifdef USE_WIFI_WPA2_EAP #include #endif +#include "dhcpserver/dhcpserver.h" #include "lwip/err.h" #include "lwip/dns.h" @@ -32,7 +33,7 @@ namespace wifi { static const char *const TAG = "wifi_esp32"; static EventGroupHandle_t s_wifi_event_group; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -static xQueueHandle s_event_queue; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +static QueueHandle_t s_event_queue; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) static esp_netif_t *s_sta_netif = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) static esp_netif_t *s_ap_netif = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) static bool s_sta_started = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) @@ -414,17 +415,17 @@ bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { if (!this->wifi_mode_(true, {})) return false; - tcpip_adapter_dhcp_status_t dhcp_status; - esp_err_t err = tcpip_adapter_dhcpc_get_status(TCPIP_ADAPTER_IF_STA, &dhcp_status); + esp_netif_dhcp_status_t dhcp_status; + esp_err_t err = esp_netif_dhcpc_get_status(s_sta_netif, &dhcp_status); if (err != ESP_OK) { - ESP_LOGV(TAG, "tcpip_adapter_dhcpc_get_status failed: %s", esp_err_to_name(err)); + ESP_LOGV(TAG, "esp_netif_dhcpc_get_status failed: %s", esp_err_to_name(err)); return false; } if (!manual_ip.has_value()) { - // Use DHCP client - if (dhcp_status != TCPIP_ADAPTER_DHCP_STARTED) { - err = tcpip_adapter_dhcpc_start(TCPIP_ADAPTER_IF_STA); + // No manual IP is set; use DHCP client + if (dhcp_status != ESP_NETIF_DHCP_STARTED) { + err = esp_netif_dhcpc_start(s_sta_netif); if (err != ESP_OK) { ESP_LOGV(TAG, "Starting DHCP client failed! %d", err); } @@ -433,43 +434,29 @@ bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { return true; } - tcpip_adapter_ip_info_t info; - memset(&info, 0, sizeof(info)); + esp_netif_ip_info_t info; // struct of ip4_addr_t with ip, netmask, gw info.ip.addr = static_cast(manual_ip->static_ip); info.gw.addr = static_cast(manual_ip->gateway); info.netmask.addr = static_cast(manual_ip->subnet); - - err = tcpip_adapter_dhcpc_stop(TCPIP_ADAPTER_IF_STA); + err = esp_netif_dhcpc_stop(s_sta_netif); if (err != ESP_OK && err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STOPPED) { - ESP_LOGV(TAG, "tcpip_adapter_dhcpc_stop failed: %s", esp_err_to_name(err)); + ESP_LOGV(TAG, "esp_netif_dhcpc_stop failed: %s", esp_err_to_name(err)); return false; } - - err = tcpip_adapter_set_ip_info(TCPIP_ADAPTER_IF_STA, &info); + err = esp_netif_set_ip_info(s_sta_netif, &info); if (err != ESP_OK) { - ESP_LOGV(TAG, "tcpip_adapter_set_ip_info failed: %s", esp_err_to_name(err)); + ESP_LOGV(TAG, "esp_netif_set_ip_info failed: %s", esp_err_to_name(err)); return false; } - ip_addr_t dns; -#if LWIP_IPV6 - dns.type = IPADDR_TYPE_V4; -#endif + esp_netif_dns_info_t dns; if (uint32_t(manual_ip->dns1) != 0) { -#if LWIP_IPV6 - dns.u_addr.ip4.addr = static_cast(manual_ip->dns1); -#else - dns.addr = static_cast(manual_ip->dns1); -#endif - dns_setserver(0, &dns); + dns.ip.u_addr.ip4.addr = static_cast(manual_ip->dns1); + esp_netif_set_dns_info(s_sta_netif, ESP_NETIF_DNS_MAIN, &dns); } if (uint32_t(manual_ip->dns2) != 0) { -#if LWIP_IPV6 - dns.u_addr.ip4.addr = static_cast(manual_ip->dns2); -#else - dns.addr = static_cast(manual_ip->dns2); -#endif - dns_setserver(1, &dns); + dns.ip.u_addr.ip4.addr = static_cast(manual_ip->dns2); + esp_netif_set_dns_info(s_sta_netif, ESP_NETIF_DNS_BACKUP, &dns); } return true; @@ -478,10 +465,10 @@ bool WiFiComponent::wifi_sta_ip_config_(optional manual_ip) { network::IPAddress WiFiComponent::wifi_sta_ip() { if (!this->has_sta()) return {}; - tcpip_adapter_ip_info_t ip; - esp_err_t err = tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_STA, &ip); + esp_netif_ip_info_t ip; + esp_err_t err = esp_netif_get_ip_info(s_sta_netif, &ip); if (err != ESP_OK) { - ESP_LOGV(TAG, "tcpip_adapter_get_ip_info failed: %s", esp_err_to_name(err)); + ESP_LOGV(TAG, "esp_netif_get_ip_info failed: %s", esp_err_to_name(err)); return false; } return {ip.ip.addr}; @@ -601,9 +588,9 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_START) { ESP_LOGV(TAG, "Event: WiFi STA start"); // apply hostname - err = tcpip_adapter_set_hostname(TCPIP_ADAPTER_IF_STA, App.get_name().c_str()); + err = esp_netif_set_hostname(s_sta_netif, App.get_name().c_str()); if (err != ERR_OK) { - ESP_LOGW(TAG, "tcpip_adapter_set_hostname failed: %s", esp_err_to_name(err)); + ESP_LOGW(TAG, "esp_netif_set_hostname failed: %s", esp_err_to_name(err)); } s_sta_started = true; @@ -651,7 +638,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { } else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_STA_GOT_IP) { const auto &it = data->data.ip_got_ip; #if LWIP_IPV6_AUTOCONFIG - tcpip_adapter_create_ip6_linklocal(TCPIP_ADAPTER_IF_STA); + esp_netif_create_ip6_linklocal(s_sta_netif); #endif ESP_LOGV(TAG, "Event: Got IP static_ip=%s gateway=%s", format_ip4_addr(it.ip_info.ip).c_str(), format_ip4_addr(it.ip_info.gw).c_str()); @@ -736,7 +723,7 @@ WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() { } return WiFiSTAConnectStatus::IDLE; } -bool WiFiComponent::wifi_scan_start_() { +bool WiFiComponent::wifi_scan_start_(bool passive) { // enable STA if (!this->wifi_mode_(true, {})) return false; @@ -746,9 +733,13 @@ bool WiFiComponent::wifi_scan_start_() { config.bssid = nullptr; config.channel = 0; config.show_hidden = true; - config.scan_type = WIFI_SCAN_TYPE_ACTIVE; - config.scan_time.active.min = 100; - config.scan_time.active.max = 300; + config.scan_type = passive ? WIFI_SCAN_TYPE_PASSIVE : WIFI_SCAN_TYPE_ACTIVE; + if (passive) { + config.scan_time.passive = 300; + } else { + config.scan_time.active.min = 100; + config.scan_time.active.max = 300; + } esp_err_t err = esp_wifi_scan_start(&config, false); if (err != ESP_OK) { @@ -766,8 +757,7 @@ bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { if (!this->wifi_mode_({}, true)) return false; - tcpip_adapter_ip_info_t info; - memset(&info, 0, sizeof(info)); + esp_netif_ip_info_t info; if (manual_ip.has_value()) { info.ip.addr = static_cast(manual_ip->static_ip); info.gw.addr = static_cast(manual_ip->gateway); @@ -777,17 +767,17 @@ bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { info.gw.addr = static_cast(network::IPAddress(192, 168, 4, 1)); info.netmask.addr = static_cast(network::IPAddress(255, 255, 255, 0)); } - tcpip_adapter_dhcp_status_t dhcp_status; - tcpip_adapter_dhcps_get_status(TCPIP_ADAPTER_IF_AP, &dhcp_status); - err = tcpip_adapter_dhcps_stop(TCPIP_ADAPTER_IF_AP); + esp_netif_dhcp_status_t dhcp_status; + esp_netif_dhcps_get_status(s_sta_netif, &dhcp_status); + err = esp_netif_dhcps_stop(s_sta_netif); if (err != ESP_OK) { - ESP_LOGV(TAG, "tcpip_adapter_dhcps_stop failed! %d", err); + ESP_LOGV(TAG, "esp_netif_dhcps_stop failed! %d", err); return false; } - err = tcpip_adapter_set_ip_info(TCPIP_ADAPTER_IF_AP, &info); + err = esp_netif_set_ip_info(s_sta_netif, &info); if (err != ESP_OK) { - ESP_LOGV(TAG, "tcpip_adapter_set_ip_info failed! %d", err); + ESP_LOGV(TAG, "esp_netif_set_ip_info failed! %d", err); return false; } @@ -800,17 +790,17 @@ bool WiFiComponent::wifi_ap_ip_config_(optional manual_ip) { start_address[3] += 100; lease.end_ip.addr = static_cast(start_address); ESP_LOGV(TAG, "DHCP server IP lease end: %s", start_address.str().c_str()); - err = tcpip_adapter_dhcps_option(TCPIP_ADAPTER_OP_SET, TCPIP_ADAPTER_REQUESTED_IP_ADDRESS, &lease, sizeof(lease)); + err = esp_netif_dhcps_option(s_sta_netif, ESP_NETIF_OP_SET, ESP_NETIF_REQUESTED_IP_ADDRESS, &lease, sizeof(lease)); if (err != ESP_OK) { - ESP_LOGV(TAG, "tcpip_adapter_dhcps_option failed! %d", err); + ESP_LOGV(TAG, "esp_netif_dhcps_option failed! %d", err); return false; } - err = tcpip_adapter_dhcps_start(TCPIP_ADAPTER_IF_AP); + err = esp_netif_dhcps_start(s_sta_netif); if (err != ESP_OK) { - ESP_LOGV(TAG, "tcpip_adapter_dhcps_start failed! %d", err); + ESP_LOGV(TAG, "esp_netif_dhcps_start failed! %d", err); return false; } @@ -856,8 +846,8 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { return true; } network::IPAddress WiFiComponent::wifi_soft_ap_ip() { - tcpip_adapter_ip_info_t ip; - tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_AP, &ip); + esp_netif_ip_info_t ip; + esp_netif_get_ip_info(s_sta_netif, &ip); return {ip.ip.addr}; } bool WiFiComponent::wifi_disconnect_() { return esp_wifi_disconnect(); } diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp index 8e64878f8e..489ebc3699 100644 --- a/esphome/components/wifi/wifi_component_pico_w.cpp +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -125,10 +125,11 @@ void WiFiComponent::wifi_scan_result(void *env, const cyw43_ev_scan_result_t *re } } -bool WiFiComponent::wifi_scan_start_() { +bool WiFiComponent::wifi_scan_start_(bool passive) { this->scan_result_.clear(); this->scan_done_ = false; cyw43_wifi_scan_options_t scan_options = {0}; + scan_options.scan_type = passive ? 1 : 0; int err = cyw43_wifi_scan(&cyw43_state, &scan_options, nullptr, &s_wifi_scan_result); if (err) { ESP_LOGV(TAG, "cyw43_wifi_scan failed!"); diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 4b822b46c9..2482e5471c 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -1455,6 +1455,7 @@ class SplitDefault(Optional): esp32_arduino=vol.UNDEFINED, esp32_idf=vol.UNDEFINED, rp2040=vol.UNDEFINED, + host=vol.UNDEFINED, ): super().__init__(key) self._esp8266_default = vol.default_factory(esp8266) @@ -1465,6 +1466,7 @@ class SplitDefault(Optional): esp32_idf if esp32 is vol.UNDEFINED else esp32 ) self._rp2040_default = vol.default_factory(rp2040) + self._host_default = vol.default_factory(host) @property def default(self): @@ -1476,6 +1478,8 @@ class SplitDefault(Optional): return self._esp32_idf_default if CORE.is_rp2040: return self._rp2040_default + if CORE.is_host: + return self._host_default raise NotImplementedError @default.setter diff --git a/esphome/const.py b/esphome/const.py index 7525c7acfb..56034bfbf6 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,14 +1,15 @@ """Constants used by esphome.""" -__version__ = "2023.4.4" +__version__ = "2023.5.0b1" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" PLATFORM_ESP32 = "esp32" PLATFORM_ESP8266 = "esp8266" PLATFORM_RP2040 = "rp2040" +PLATFORM_HOST = "host" -TARGET_PLATFORMS = [PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040] +TARGET_PLATFORMS = [PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040, PLATFORM_HOST] SOURCE_FILE_EXTENSIONS = {".cpp", ".hpp", ".h", ".c", ".tcc", ".ino"} HEADER_FILE_EXTENSIONS = {".h", ".hpp", ".tcc"} @@ -659,6 +660,7 @@ CONF_SLEEP_WHEN_DONE = "sleep_when_done" CONF_SONY = "sony" CONF_SOURCE = "source" CONF_SOURCE_ID = "source_id" +CONF_SPEAKER = "speaker" CONF_SPEED = "speed" CONF_SPEED_COMMAND_TOPIC = "speed_command_topic" CONF_SPEED_COUNT = "speed_count" diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index 1866f9c9f5..891936adc3 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -602,6 +602,10 @@ class EsphomeCore: def is_rp2040(self): return self.target_platform == "rp2040" + @property + def is_host(self): + return self.target_platform == "host" + @property def target_framework(self): return self.data[KEY_CORE][KEY_TARGET_FRAMEWORK] diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 77d41e5b58..64edabc878 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -73,6 +73,8 @@ #define USE_WIFI_11KV_SUPPORT #define USE_BLUETOOTH_PROXY #define USE_VOICE_ASSISTANT +#define USE_MICROPHONE +#define USE_SPEAKER #ifdef USE_ARDUINO #define USE_ARDUINO_VERSION_CODE VERSION_CODE(2, 0, 5) @@ -100,6 +102,10 @@ #endif +#ifdef USE_HOST +#define USE_SOCKET_IMPL_BSD_SOCKETS +#endif + // Disabled feature flags //#define USE_BSEC // Requires a library with proprietary license. diff --git a/esphome/core/entity_base.cpp b/esphome/core/entity_base.cpp index 3d61e36fd1..725a8569a3 100644 --- a/esphome/core/entity_base.cpp +++ b/esphome/core/entity_base.cpp @@ -72,6 +72,25 @@ void EntityBase::calc_object_id_() { this->object_id_hash_ = fnv1_hash(this->object_id_c_str_); } } + uint32_t EntityBase::get_object_id_hash() { return this->object_id_hash_; } +std::string EntityBase_DeviceClass::get_device_class() { + if (this->device_class_ == nullptr) { + return ""; + } + return this->device_class_; +} + +void EntityBase_DeviceClass::set_device_class(const char *device_class) { this->device_class_ = device_class; } + +std::string EntityBase_UnitOfMeasurement::get_unit_of_measurement() { + if (this->unit_of_measurement_ == nullptr) + return ""; + return this->unit_of_measurement_; +} +void EntityBase_UnitOfMeasurement::set_unit_of_measurement(const char *unit_of_measurement) { + this->unit_of_measurement_ = unit_of_measurement; +} + } // namespace esphome diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index e40a7013bf..434111de79 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -63,4 +63,26 @@ class EntityBase { EntityCategory entity_category_{ENTITY_CATEGORY_NONE}; }; +class EntityBase_DeviceClass { + public: + /// Get the device class, using the manual override if set. + std::string get_device_class(); + /// Manually set the device class. + void set_device_class(const char *device_class); + + protected: + const char *device_class_{nullptr}; ///< Device class override +}; + +class EntityBase_UnitOfMeasurement { + public: + /// Get the unit of measurement, using the manual override if set. + std::string get_unit_of_measurement(); + /// Manually set the unit of measurement. + void set_unit_of_measurement(const char *unit_of_measurement); + + protected: + const char *unit_of_measurement_{nullptr}; ///< Unit of measurement override +}; + } // namespace esphome diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 7f5c3ad333..4c6ee84dde 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -2,13 +2,14 @@ #include "esphome/core/defines.h" #include "esphome/core/hal.h" +#include "esphome/core/log.h" -#include #include #include #include -#include #include +#include +#include #if defined(USE_ESP8266) #include @@ -18,15 +19,20 @@ #elif defined(USE_ESP32_FRAMEWORK_ARDUINO) #include #elif defined(USE_ESP_IDF) -#include "esp_system.h" #include #include +#include "esp_mac.h" +#include "esp_random.h" +#include "esp_system.h" #elif defined(USE_RP2040) #if defined(USE_WIFI) #include #endif #include #include +#elif defined(USE_HOST) +#include +#include #endif #ifdef USE_ESP32_IGNORE_EFUSE_MAC_CRC @@ -36,6 +42,8 @@ namespace esphome { +static const char *const TAG = "helpers"; + // STL backports #if _GLIBCXX_RELEASE < 7 @@ -104,6 +112,11 @@ uint32_t random_uint32() { result |= rosc_hw->randombit; } return result; +#elif defined(USE_HOST) + std::random_device dev; + std::mt19937 rng(dev()); + std::uniform_int_distribution dist(0, std::numeric_limits::max()); + return dist(rng); #else #error "No random source available for this configuration." #endif @@ -125,6 +138,19 @@ bool random_bytes(uint8_t *data, size_t len) { *data++ = result; } return true; +#elif defined(USE_HOST) + FILE *fp = fopen("/dev/urandom", "r"); + if (fp == nullptr) { + ESP_LOGW(TAG, "Could not open /dev/urandom, errno=%d", errno); + exit(1); + } + size_t read = fread(data, 1, len, fp); + if (read != len) { + ESP_LOGW(TAG, "Not enough data from /dev/urandom"); + exit(1); + } + fclose(fp); + return true; #else #error "No random source available for this configuration." #endif @@ -143,7 +169,7 @@ std::string str_truncate(const std::string &str, size_t length) { return str.length() > length ? str.substr(0, length) : str; } std::string str_until(const char *str, char ch) { - char *pos = strchr(str, ch); + const char *pos = strchr(str, ch); return pos == nullptr ? std::string(str) : std::string(str, pos - str); } std::string str_until(const std::string &str, char ch) { return str.substr(0, str.find(ch)); } @@ -393,7 +419,7 @@ void hsv_to_rgb(int hue, float saturation, float value, float &red, float &green } // System APIs -#if defined(USE_ESP8266) || defined(USE_RP2040) +#if defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_HOST) // ESP8266 doesn't have mutexes, but that shouldn't be an issue as it's single-core and non-preemptive OS. Mutex::Mutex() {} void Mutex::lock() {} @@ -467,6 +493,7 @@ void set_mac_address(uint8_t *mac) { esp_base_mac_addr_set(mac); } void delay_microseconds_safe(uint32_t us) { // avoids CPU locks that could trigger WDT or affect WiFi/BT stability uint32_t start = micros(); + const uint32_t lag = 5000; // microseconds, specifies the maximum time for a CPU busy-loop. // it must be larger than the worst-case duration of a delay(1) call (hardware tasks) // 5ms is conservative, it could be reduced when exact BT/WiFi stack delays are known diff --git a/platformio.ini b/platformio.ini index c8db90bacb..3565b15809 100644 --- a/platformio.ini +++ b/platformio.ini @@ -79,16 +79,16 @@ build_flags = ; This are common settings for the ESP8266 using Arduino. [common:esp8266-arduino] extends = common:arduino -platform = platformio/espressif8266 @ 3.2.0 +platform = platformio/espressif8266@3.2.0 platform_packages = - platformio/framework-arduinoespressif8266 @ ~3.30002.0 + platformio/framework-arduinoespressif8266@~3.30002.0 framework = arduino lib_deps = ${common:arduino.lib_deps} ESP8266WiFi ; wifi (Arduino built-in) Update ; ota (Arduino built-in) - ottowinter/ESPAsyncTCP-esphome@1.2.3 ; async_tcp + esphome/ESPAsyncTCP-esphome@1.2.3 ; async_tcp ESP8266HTTPClient ; http_request (Arduino built-in) ESP8266mDNS ; mdns (Arduino built-in) DNSServer ; captive_portal (Arduino built-in) @@ -103,12 +103,11 @@ extra_scripts = post:esphome/components/esp8266/post_build.py.script ; This are common settings for the ESP32 (all variants) using Arduino. [common:esp32-arduino] extends = common:arduino -platform = platformio/espressif32 @ 5.3.0 +platform = platformio/espressif32@5.3.0 platform_packages = - platformio/framework-arduinoespressif32 @ ~3.20005.0 + platformio/framework-arduinoespressif32@~3.20005.0 framework = arduino -board = nodemcu-32s lib_deps = ; order matters with lib-deps; some of the libs in common:arduino.lib_deps ; don't declare built-in libraries as dependencies, so they have to be declared first @@ -121,7 +120,7 @@ lib_deps = HTTPClient ; http_request,nextion (Arduino built-in) ESPmDNS ; mdns (Arduino built-in) DNSServer ; captive_portal (Arduino built-in) - esphome/ESP32-audioI2S@2.0.6 ; i2s_audio + esphome/ESP32-audioI2S@2.0.7 ; i2s_audio crankyoldgit/IRremoteESP8266@2.7.12 ; heatpumpir build_flags = ${common:arduino.build_flags} @@ -133,9 +132,9 @@ extra_scripts = post:esphome/components/esp32/post_build.py.script ; This are common settings for the ESP32 (all variants) using IDF. [common:esp32-idf] extends = common:idf -platform = platformio/espressif32 @ 5.3.0 +platform = platformio/espressif32@5.3.0 platform_packages = - platformio/framework-espidf @ ~3.40404.0 + platformio/framework-espidf@~3.40404.0 framework = espidf lib_deps = @@ -156,8 +155,8 @@ board_build.filesystem_size = 0.5m platform = https://github.com/maxgerhardt/platform-raspberrypi.git platform_packages = - ; earlephilhower/framework-arduinopico @ ~1.20602.0 ; Cannot use the platformio package until old releases stop getting deleted - earlephilhower/framework-arduinopico @ https://github.com/earlephilhower/arduino-pico/releases/download/2.6.2/rp2040-2.6.2.zip + ; earlephilhower/framework-arduinopico@~1.20602.0 ; Cannot use the platformio package until old releases stop getting deleted + earlephilhower/framework-arduinopico@https://github.com/earlephilhower/arduino-pico/releases/download/2.6.2/rp2040-2.6.2.zip framework = arduino lib_deps = @@ -168,6 +167,9 @@ build_flags = -DUSE_RP2040_FRAMEWORK_ARDUINO ; All the actual environments are defined below. + +;;;;;;;; ESP8266 ;;;;;;;; + [env:esp8266-arduino] extends = common:esp8266-arduino board = nodemcuv2 @@ -182,6 +184,8 @@ build_flags = ${common:esp8266-arduino.build_flags} ${flags:clangtidy.build_flags} +;;;;;;;; ESP32 ;;;;;;;; + [env:esp32-arduino] extends = common:esp32-arduino board = esp32dev @@ -189,6 +193,7 @@ board_build.partitions = huge_app.csv build_flags = ${common:esp32-arduino.build_flags} ${flags:runtime.build_flags} + -DUSE_ESP32_VARIANT_ESP32 [env:esp32-arduino-tidy] extends = common:esp32-arduino @@ -196,6 +201,7 @@ board = esp32dev build_flags = ${common:esp32-arduino.build_flags} ${flags:clangtidy.build_flags} + -DUSE_ESP32_VARIANT_ESP32 [env:esp32-idf] extends = common:esp32-idf @@ -204,6 +210,7 @@ board_build.esp-idf.sdkconfig_path = .temp/sdkconfig-esp32-idf build_flags = ${common:esp32-idf.build_flags} ${flags:runtime.build_flags} + -DUSE_ESP32_VARIANT_ESP32 [env:esp32-idf-tidy] extends = common:esp32-idf @@ -212,6 +219,25 @@ board_build.esp-idf.sdkconfig_path = .temp/sdkconfig-esp32-idf-tidy build_flags = ${common:esp32-idf.build_flags} ${flags:clangtidy.build_flags} + -DUSE_ESP32_VARIANT_ESP32 + +;;;;;;;; ESP32-C3 ;;;;;;;; + +[env:esp32c3-arduino] +extends = common:esp32-arduino +board = esp32-c3-devkitm-1 +build_flags = + ${common:esp32-arduino.build_flags} + ${flags:runtime.build_flags} + -DUSE_ESP32_VARIANT_ESP32C3 + +[env:esp32c3-arduino-tidy] +extends = common:esp32-arduino +board = esp32-c3-devkitm-1 +build_flags = + ${common:esp32-arduino.build_flags} + ${flags:clangtidy.build_flags} + -DUSE_ESP32_VARIANT_ESP32C3 [env:esp32c3-idf] extends = common:esp32-idf @@ -220,6 +246,7 @@ board_build.esp-idf.sdkconfig_path = .temp/sdkconfig-esp32c3-idf build_flags = ${common:esp32-idf.build_flags} ${flags:runtime.build_flags} + -DUSE_ESP32_VARIANT_ESP32C3 [env:esp32c3-idf-tidy] extends = common:esp32-idf @@ -228,6 +255,25 @@ board_build.esp-idf.sdkconfig_path = .temp/sdkconfig-esp32c3-idf-tidy build_flags = ${common:esp32-idf.build_flags} ${flags:clangtidy.build_flags} + -DUSE_ESP32_VARIANT_ESP32C3 + +;;;;;;;; ESP32-S2 ;;;;;;;; + +[env:esp32s2-arduino] +extends = common:esp32-arduino +board = esp32-s2-kaluga-1 +build_flags = + ${common:esp32-arduino.build_flags} + ${flags:runtime.build_flags} + -DUSE_ESP32_VARIANT_ESP32S2 + +[env:esp32s2-arduino-tidy] +extends = common:esp32-arduino +board = esp32-s2-kaluga-1 +build_flags = + ${common:esp32-arduino.build_flags} + ${flags:clangtidy.build_flags} + -DUSE_ESP32_VARIANT_ESP32S2 [env:esp32s2-idf] extends = common:esp32-idf @@ -236,6 +282,7 @@ board_build.esp-idf.sdkconfig_path = .temp/sdkconfig-esp32s2-idf build_flags = ${common:esp32-idf.build_flags} ${flags:runtime.build_flags} + -DUSE_ESP32_VARIANT_ESP32S2 [env:esp32s2-idf-tidy] extends = common:esp32-idf @@ -244,6 +291,45 @@ board_build.esp-idf.sdkconfig_path = .temp/sdkconfig-esp32s2-idf-tidy build_flags = ${common:esp32-idf.build_flags} ${flags:clangtidy.build_flags} + -DUSE_ESP32_VARIANT_ESP32S2 + +;;;;;;;; ESP32-S3 ;;;;;;;; + +[env:esp32s3-arduino] +extends = common:esp32-arduino +board = esp32-s3-devkitc-1 +build_flags = + ${common:esp32-arduino.build_flags} + ${flags:runtime.build_flags} + -DUSE_ESP32_VARIANT_ESP32S3 + +[env:esp32s3-arduino-tidy] +extends = common:esp32-arduino +board = esp32-s3-devkitc-1 +build_flags = + ${common:esp32-arduino.build_flags} + ${flags:clangtidy.build_flags} + -DUSE_ESP32_VARIANT_ESP32S3 + +[env:esp32s3-idf] +extends = common:esp32-idf +board = esp32-s3-devkitc-1 +board_build.esp-idf.sdkconfig_path = .temp/sdkconfig-esp32s3-idf +build_flags = + ${common:esp32-idf.build_flags} + ${flags:runtime.build_flags} + -DUSE_ESP32_VARIANT_ESP32S3 + +[env:esp32s3-idf-tidy] +extends = common:esp32-idf +board = esp32-s3-devkitc-1 +board_build.esp-idf.sdkconfig_path = .temp/sdkconfig-esp32s3-idf-tidy +build_flags = + ${common:esp32-idf.build_flags} + ${flags:clangtidy.build_flags} + -DUSE_ESP32_VARIANT_ESP32S3 + +;;;;;;;; RP2040 ;;;;;;;; [env:rp2040-pico-arduino] extends = common:rp2040-arduino @@ -251,3 +337,12 @@ board = rpipico build_flags = ${common:rp2040-arduino.build_flags} ${flags:runtime.build_flags} + +[env:host] +extends = common +platform = platformio/native +lib_deps = + esphome/noise-c@0.1.1 ; used by api +build_flags = + ${common.build_flags} + -DUSE_HOST diff --git a/requirements.txt b/requirements.txt index 5a57342189..a62c48e235 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ voluptuous==0.13.1 PyYAML==6.0 paho-mqtt==1.6.1 colorama==0.4.6 -tornado==6.2 +tornado==6.3.1 tzlocal==4.2 # from time tzdata>=2021.1 # from time pyserial==3.5 @@ -10,9 +10,12 @@ platformio==6.1.6 # When updating platformio, also update Dockerfile esptool==4.5.1 click==8.1.3 esphome-dashboard==20230214.0 -aioesphomeapi==13.5.1 -zeroconf==0.56.0 +aioesphomeapi==13.7.2 +zeroconf==0.60.0 # esp-idf requires this, but doesn't bundle it by default # https://github.com/espressif/esp-idf/blob/220590d599e134d7a5e7f1e683cc4550349ffbf8/requirements.txt#L24 kconfiglib==13.7.1 + +# esp-idf >= 5.0 requires this +pyparsing >= 3.0 diff --git a/requirements_test.txt b/requirements_test.txt index b063dd2797..55f8da245e 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,11 +1,11 @@ -pylint==2.17.2 +pylint==2.17.3 flake8==6.0.0 # also change in .pre-commit-config.yaml when updating black==23.3.0 # also change in .pre-commit-config.yaml when updating -pyupgrade==3.3.1 # also change in .pre-commit-config.yaml when updating +pyupgrade==3.3.2 # also change in .pre-commit-config.yaml when updating pre-commit # Unit tests -pytest==7.3.0 +pytest==7.3.1 pytest-cov==4.0.0 pytest-mock==3.10.0 pytest-asyncio==0.21.0 diff --git a/script/build_language_schema.py b/script/build_language_schema.py index 4c8639a1b3..dd8eccde93 100644 --- a/script/build_language_schema.py +++ b/script/build_language_schema.py @@ -63,7 +63,7 @@ solve_registry = [] def get_component_names(): from esphome.loader import CORE_COMPONENTS_PATH - component_names = ["esphome", "sensor"] + component_names = ["esphome", "sensor", "esp32", "esp8266"] for d in os.listdir(CORE_COMPONENTS_PATH): if not d.startswith("__") and os.path.isdir( @@ -109,6 +109,13 @@ def write_file(name, obj): print(f"Wrote {full_path}") +def delete_extra_files(keep_names): + for d in os.listdir(args.output_path): + if d.endswith(".json") and not d[:-5] in keep_names: + os.remove(os.path.join(args.output_path, d)) + print(f"Deleted {d}") + + def register_module_schemas(key, module, manifest=None): for name, schema in module_schemas(module): register_known_schema(key, name, schema) @@ -150,7 +157,7 @@ def module_schemas(module): schemas = {} for m_attr_name in dir(module): m_attr_obj = getattr(module, m_attr_name) - if isConvertibleSchema(m_attr_obj): + if is_convertible_schema(m_attr_obj): schemas[module_str.find(m_attr_name)] = [m_attr_name, m_attr_obj] for pos in sorted(schemas.keys()): @@ -240,25 +247,34 @@ def do_pins(): pins_providers.append(pin_registry) +def setBoards(obj, boards): + obj[S_TYPE] = "enum" + obj["values"] = {} + for k, v in boards.items(): + obj["values"][k] = {"docs": v["name"]} + + def do_esp32(): import esphome.components.esp32.boards as esp32_boards - setEnum( + setBoards( output["esp32"]["schemas"]["CONFIG_SCHEMA"]["schema"]["config_vars"]["board"], - list(esp32_boards.BOARDS.keys()), + esp32_boards.BOARDS, ) def do_esp8266(): import esphome.components.esp8266.boards as esp8266_boards - setEnum( + setBoards( output["esp8266"]["schemas"]["CONFIG_SCHEMA"]["schema"]["config_vars"]["board"], - list(esp8266_boards.ESP8266_BOARD_PINS.keys()), + esp8266_boards.BOARDS, ) def fix_remote_receiver(): + if "remote_receiver.binary_sensor" not in output: + return remote_receiver_schema = output["remote_receiver.binary_sensor"]["schemas"] remote_receiver_schema["CONFIG_SCHEMA"] = { "type": "schema", @@ -275,6 +291,8 @@ def fix_remote_receiver(): def fix_script(): + if "script" not in output: + return output["script"][S_SCHEMAS][S_CONFIG_SCHEMA][S_TYPE] = S_SCHEMA config_schema = output["script"][S_SCHEMAS][S_CONFIG_SCHEMA] config_schema[S_SCHEMA][S_CONFIG_VARS]["id"]["id_type"] = { @@ -283,7 +301,17 @@ def fix_script(): config_schema["is_list"] = True +def fix_font(): + if "font" not in output: + return + output["font"][S_SCHEMAS]["FILE_SCHEMA"] = output["font"][S_SCHEMAS].pop( + "TYPED_FILE_SCHEMA" + ) + + def fix_menu(): + if "display_menu_base" not in output: + return # # Menu has a recursive schema which is not kept properly schemas = output["display_menu_base"][S_SCHEMAS] # 1. Move items to a new schema @@ -330,6 +358,8 @@ def get_logger_tags(): def add_logger_tags(): + if "logger" not in output or "schemas" not in output["logger"]: + return tags = get_logger_tags() logs = output["logger"]["schemas"]["CONFIG_SCHEMA"]["schema"]["config_vars"][ "logs" @@ -429,6 +459,12 @@ def merge(source, destination): return destination +def is_platform_schema(schema_name): + # added mostly because of schema_name == "microphone.MICROPHONE_SCHEMA" + # which is shrunk because there is only one component of the schema (i2s_audio) + return schema_name == "microphone.MICROPHONE_SCHEMA" + + def shrink(): """Shrink the extending schemas which has just an end type, e.g. at this point ota / port is type schema with extended pointing to core.port, this should instead be @@ -454,7 +490,7 @@ def shrink(): add_referenced_recursive(referenced_schemas, vvv, [k, kv, kvv]) for x, paths in referenced_schemas.items(): - if len(paths) == 1: + if len(paths) == 1 and not is_platform_schema(x): key_s = get_str_path_schema(x) arr_s = get_arr_path_schema(paths[0]) # key_s |= arr_s @@ -508,6 +544,7 @@ def shrink(): if ( not s.endswith("." + S_CONFIG_SCHEMA) and s not in referenced_schemas.keys() + and not is_platform_schema(s) ): print(f"Removing {s}") output[domain][S_SCHEMAS].pop(schema_name) @@ -589,6 +626,7 @@ def build_schema(): do_esp32() fix_remote_receiver() fix_script() + fix_font() add_logger_tags() shrink() fix_menu() @@ -611,17 +649,13 @@ def build_schema(): for c, s in data.items(): write_file(c, s) + delete_extra_files(data.keys()) -def setEnum(obj, items): - obj[S_TYPE] = "enum" - obj["values"] = items - - -def isConvertibleSchema(schema): +def is_convertible_schema(schema): if schema is None: return False - if isinstance(schema, (cv.Schema, cv.All)): + if isinstance(schema, (cv.Schema, cv.All, cv.Any)): return True if repr(schema) in ejs.hidden_schemas: return True @@ -640,20 +674,23 @@ def isConvertibleSchema(schema): def convert_config(schema, path): converted = {} - convert_1(schema, converted, path) + convert(schema, converted, path) return converted -def convert_1(schema, config_var, path): +def convert(schema, config_var, path): """config_var can be a config_var or a schema: both are dicts config_var has a S_TYPE property, if this is S_SCHEMA, then it has a S_SCHEMA property schema does not have a type property, schema can have optionally both S_CONFIG_VARS and S_EXTENDS """ repr_schema = repr(schema) + if path.startswith("ads1115.sensor") and path.endswith("gain"): + print(path) + if repr_schema in known_schemas: schema_info = known_schemas[(repr_schema)] - for (schema_instance, name) in schema_info: + for schema_instance, name in schema_info: if schema_instance is schema: assert S_CONFIG_VARS not in config_var assert S_EXTENDS not in config_var @@ -665,7 +702,7 @@ def convert_1(schema, config_var, path): config_var[S_SCHEMA] = {} if S_EXTENDS not in config_var[S_SCHEMA]: config_var[S_SCHEMA][S_EXTENDS] = [name] - else: + elif name not in config_var[S_SCHEMA][S_EXTENDS]: config_var[S_SCHEMA][S_EXTENDS].append(name) return @@ -679,25 +716,25 @@ def convert_1(schema, config_var, path): return assert len(extended) == 2 - convert_1(extended[0], config_var, path + "/extL") - convert_1(extended[1], config_var, path + "/extR") + convert(extended[0], config_var, path + "/extL") + convert(extended[1], config_var, path + "/extR") return if isinstance(schema, cv.All): i = 0 for inner in schema.validators: i = i + 1 - convert_1(inner, config_var, path + f"/val {i}") + convert(inner, config_var, path + f"/val {i}") return if hasattr(schema, "validators"): i = 0 for inner in schema.validators: i = i + 1 - convert_1(inner, config_var, path + f"/val {i}") + convert(inner, config_var, path + f"/val {i}") if isinstance(schema, cv.Schema): - convert_1(schema.schema, config_var, path + "/all") + convert(schema.schema, config_var, path + "/all") return if isinstance(schema, dict): @@ -707,7 +744,7 @@ def convert_1(schema, config_var, path): if repr_schema in ejs.list_schemas: config_var["is_list"] = True items_schema = ejs.list_schemas[repr_schema][0] - convert_1(items_schema, config_var, path + "/list") + convert(items_schema, config_var, path + "/list") return if DUMP_RAW: @@ -741,10 +778,10 @@ def convert_1(schema, config_var, path): # enums, e.g. esp32/variant if schema_type == "one_of": config_var[S_TYPE] = "enum" - config_var["values"] = list(data) + config_var["values"] = dict.fromkeys(list(data)) elif schema_type == "enum": config_var[S_TYPE] = "enum" - config_var["values"] = list(data.keys()) + config_var["values"] = dict.fromkeys(list(data.keys())) elif schema_type == "maybe": config_var[S_TYPE] = S_SCHEMA config_var["maybe"] = data[1] @@ -785,13 +822,13 @@ def convert_1(schema, config_var, path): config_var["filter"] = data[0] elif schema_type == "templatable": config_var["templatable"] = True - convert_1(data, config_var, path + "/templat") + convert(data, config_var, path + "/templat") elif schema_type == "triggers": # remote base - convert_1(data, config_var, path + "/trigger") + convert(data, config_var, path + "/trigger") elif schema_type == "sensor": schema = data - convert_1(data, config_var, path + "/trigger") + convert(data, config_var, path + "/trigger") elif schema_type == "declare_id": # pylint: disable=protected-access parents = data._parents @@ -856,6 +893,9 @@ def convert_1(schema, config_var, path): if DUMP_PATH: config_var["path"] = path + if S_TYPE not in config_var: + pass + # print(path) def get_overridden_config(key, converted): @@ -921,7 +961,7 @@ def convert_keys(converted, schema, path): result["default"] = str(default_value) # Do value - convert_1(v, result, path + f"/{str(k)}") + convert(v, result, path + f"/{str(k)}") if "schema" not in converted: converted[S_TYPE] = "schema" converted["schema"] = {S_CONFIG_VARS: {}} diff --git a/script/ci-custom.py b/script/ci-custom.py index f95039576b..20f607f987 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -535,6 +535,7 @@ def lint_relative_py_import(fname): "esphome/components/esp32/core.cpp", "esphome/components/esp8266/core.cpp", "esphome/components/rp2040/core.cpp", + "esphome/components/host/core.cpp", ], ) def lint_namespace(fname, content): diff --git a/script/platformio_install_deps.py b/script/platformio_install_deps.py new file mode 100755 index 0000000000..ed133ecb47 --- /dev/null +++ b/script/platformio_install_deps.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +# This script is used to preinstall +# all platformio libraries in the global storage + +import argparse +import configparser +import subprocess + +config = configparser.ConfigParser(inline_comment_prefixes=(";",)) + +parser = argparse.ArgumentParser(description="") +parser.add_argument("file", help="Path to platformio.ini", nargs=1) +parser.add_argument("-l", "--libraries", help="Install libraries", action="store_true") +parser.add_argument("-p", "--platforms", help="Install platforms", action="store_true") +parser.add_argument("-t", "--tools", help="Install tools", action="store_true") + +args = parser.parse_args() + +config.read(args.file) + + +libs = [] +tools = [] +platforms = [] +# Extract from every lib_deps key in all sections +for section in config.sections(): + conf = config[section] + if "lib_deps" in conf and args.libraries: + 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("-l") + libs.append(lib_dep) + if "platform" in conf and args.platforms: + platforms.append("-p") + platforms.append(conf["platform"]) + if "platform_packages" in conf and args.tools: + for tool in conf["platform_packages"].splitlines(): + if not tool: + # Empty line or comment + continue + if tool.startswith("${"): + # Extending from another section + continue + if tool.find("https://github.com") != -1: + split = tool.find("@") + tool = tool[split + 1 :] + tools.append("-t") + tools.append(tool) + +subprocess.check_call(["platformio", "pkg", "install", "-g", *libs, *platforms, *tools]) diff --git a/script/setup b/script/setup index c650960f05..656e95eba6 100755 --- a/script/setup +++ b/script/setup @@ -14,3 +14,5 @@ pip3 install -r requirements.txt -r requirements_optional.txt -r requirements_te pip3 install --no-use-pep517 -e . pre-commit install + +script/platformio_install_deps.py platformio.ini --libraries --tools --platforms diff --git a/tests/test1.yaml b/tests/test1.yaml index c2a2ed5c95..46c6bb80c6 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -294,6 +294,9 @@ wled: adalight: +esp32_ble: + io_capability: keyboard_only + esp32_ble_tracker: ble_client: @@ -307,6 +310,19 @@ ble_client: on_disconnect: then: - switch.turn_on: ble1_status + on_passkey_request: + then: + - ble_client.passkey_reply: + id: ble_blah + passkey: 123456 + on_passkey_notification: + then: + - logger.log: "Passkey notification received" + on_numeric_comparison_request: + then: + - ble_client.numeric_comparison_reply: + id: ble_blah + accept: True - mac_address: C4:4F:33:11:22:33 id: my_bedjet_ble_client bedjet: @@ -1239,6 +1255,13 @@ sensor: temperature: name: Max9611 Temp update_interval: 1s + - platform: mlx90614 + i2c_id: i2c_bus + ambient: + name: Ambient + object: + name: Object + emissivity: 1.0 - platform: mpl3115a2 i2c_id: i2c_bus temperature: @@ -1269,6 +1292,13 @@ sensor: name: DHT Absolute Humidity temperature: dht_temperature humidity: dht_humidity + - platform: hyt271 + i2c_id: i2c_bus + temperature: + name: "Temperature hyt271" + id: temp_etuve + humidity: + name: "Humidity hyt271" esp32_touch: setup_mode: false @@ -1442,6 +1472,13 @@ binary_sensor: number: 1 mode: INPUT inverted: true + - platform: gpio + name: PCA6416A binary sensor + pin: + pca6416a: pca6416a_hub + number: 15 + mode: INPUT + inverted: true - platform: gpio name: MCP21 binary sensor pin: @@ -2927,6 +2964,11 @@ pca9554: address: 0x3F i2c_id: i2c_bus +pca6416a: + - id: pca6416a_hub + address: 0x21 + i2c_id: i2c_bus + mcp23017: - id: mcp23017_hub open_drain_interrupt: true diff --git a/tests/test3.1.yaml b/tests/test3.1.yaml index 19e4341ee6..0daf4e9671 100644 --- a/tests/test3.1.yaml +++ b/tests/test3.1.yaml @@ -300,6 +300,8 @@ text_sensor: sm2135: data_pin: GPIO12 clock_pin: GPIO14 + rgb_current: 20mA + cw_current: 60mA switch: - platform: template diff --git a/tests/test3.yaml b/tests/test3.yaml index ceb9047d17..c4847725e8 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -368,6 +368,32 @@ sensor: - binary_sensor: bin3 value: 100.0 + - platform: binary_sensor_map + name: Binary Sensor Map + type: sum + channels: + - binary_sensor: bin1 + value: 10.0 + - binary_sensor: bin2 + value: 15.0 + - binary_sensor: bin3 + value: 100.0 + + - platform: binary_sensor_map + name: Binary Sensor Map + type: bayesian + prior: 0.4 + observations: + - binary_sensor: bin1 + prob_given_true: 0.9 + prob_given_false: 0.4 + - binary_sensor: bin2 + prob_given_true: 0.7 + prob_given_false: 0.05 + - binary_sensor: bin3 + prob_given_true: 0.8 + prob_given_false: 0.2 + - platform: bl0939 uart_id: uart_8 voltage: diff --git a/tests/test4.yaml b/tests/test4.yaml index 7b8f139a43..c1d49a4349 100644 --- a/tests/test4.yaml +++ b/tests/test4.yaml @@ -374,6 +374,16 @@ binary_sensor: on_press: - logger.log: Touched + - platform: gpio + name: MaxIn Pin 4 + pin: + max6956: max6956_1 + number: 4 + mode: + input: true + pullup: true + inverted: false + climate: - platform: tuya id: tuya_climate @@ -690,12 +700,26 @@ prometheus: microphone: - platform: i2s_audio - id: mic_id + id: mic_id_adc + adc_pin: GPIO35 + adc_type: internal + + - platform: i2s_audio + id: mic_id_external i2s_din_pin: GPIO23 + adc_type: external + pdm: false + +speaker: + - platform: i2s_audio + id: speaker_id + dac_type: external + i2s_dout_pin: GPIO25 + mode: mono voice_assistant: - microphone: mic_id + microphone: mic_id_external on_start: - logger.log: "Voice assistant started" on_stt_end: @@ -716,3 +740,7 @@ voice_assistant: - logger.log: format: "Voice assistant error - code %s, message: %s" args: [code.c_str(), message.c_str()] + +max6956: + - id: max6956_1 + address: 0x40 diff --git a/tests/test5.yaml b/tests/test5.yaml index 0d044ac241..6b64ef2d15 100644 --- a/tests/test5.yaml +++ b/tests/test5.yaml @@ -208,6 +208,12 @@ tlc5947: clock_pin: GPIO14 lat_pin: GPIO15 +gp8403: + - id: gp8403_5v + voltage: 5V + - id: gp8403_10v + voltage: 10V + output: - platform: gpio pin: GPIO2 @@ -245,6 +251,15 @@ output: id: Led7 led: 7 + - platform: gp8403 + id: gp8403_output_0 + gp8403_id: gp8403_5v + channel: 0 + - platform: gp8403 + gp8403_id: gp8403_10v + id: gp8403_output_1 + channel: 1 + demo: esp32_ble: @@ -629,3 +644,22 @@ key_collector: source_id: keypad min_length: 4 max_length: 4 + +light: + - platform: esp32_rmt_led_strip + id: led_strip + pin: 13 + num_leds: 60 + rmt_channel: 6 + rgb_order: GRB + chipset: ws2812 + - platform: esp32_rmt_led_strip + id: led_strip2 + pin: 15 + num_leds: 60 + rmt_channel: 2 + rgb_order: RGB + bit0_high: 100us + bit0_low: 100us + bit1_high: 100us + bit1_low: 100us