Merge branch 'dev' into bump-2023.5.0b1

This commit is contained in:
Jesse Hills 2023-05-11 09:38:20 +12:00
commit cc576cf1a9
No known key found for this signature in database
GPG Key ID: BEAAE804EFD8E83A
174 changed files with 4997 additions and 695 deletions

View File

@ -11,6 +11,7 @@ on:
- ".github/workflows/**" - ".github/workflows/**"
- "requirements*.txt" - "requirements*.txt"
- "platformio.ini" - "platformio.ini"
- "script/platformio_install_deps.py"
pull_request: pull_request:
paths: paths:
@ -18,6 +19,7 @@ on:
- ".github/workflows/**" - ".github/workflows/**"
- "requirements*.txt" - "requirements*.txt"
- "platformio.ini" - "platformio.ini"
- "script/platformio_install_deps.py"
permissions: permissions:
contents: read contents: read

View File

@ -23,6 +23,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
fail-fast: false fail-fast: false
max-parallel: 5
matrix: matrix:
include: include:
- id: ci-custom - id: ci-custom

View File

@ -117,7 +117,7 @@ jobs:
--suffix "${{ matrix.image.suffix }}" --suffix "${{ matrix.image.suffix }}"
- name: Build and push - name: Build and push
uses: docker/build-push-action@v3 uses: docker/build-push-action@v4
with: with:
context: . context: .
file: ./docker/Dockerfile file: ./docker/Dockerfile

View File

@ -48,7 +48,7 @@ jobs:
echo "$delimiter" >> $GITHUB_OUTPUT echo "$delimiter" >> $GITHUB_OUTPUT
- name: Commit changes - name: Commit changes
uses: peter-evans/create-pull-request@v4 uses: peter-evans/create-pull-request@v5
with: with:
commit-message: "Synchronise Device Classes from Home Assistant" commit-message: "Synchronise Device Classes from Home Assistant"
committer: esphomebot <esphome@nabucasa.com> committer: esphomebot <esphome@nabucasa.com>

View File

@ -27,7 +27,7 @@ repos:
- --branch=release - --branch=release
- --branch=beta - --branch=beta
- repo: https://github.com/asottile/pyupgrade - repo: https://github.com/asottile/pyupgrade
rev: v3.3.1 rev: v3.3.2
hooks: hooks:
- id: pyupgrade - id: pyupgrade
args: [--py39-plus] args: [--py39-plus]

View File

@ -21,6 +21,7 @@ esphome/components/airthings_wave_mini/* @ncareau
esphome/components/airthings_wave_plus/* @jeromelaban esphome/components/airthings_wave_plus/* @jeromelaban
esphome/components/am43/* @buxtronix esphome/components/am43/* @buxtronix
esphome/components/am43/cover/* @buxtronix esphome/components/am43/cover/* @buxtronix
esphome/components/am43/sensor/* @buxtronix
esphome/components/analog_threshold/* @ianchi esphome/components/analog_threshold/* @ianchi
esphome/components/animation/* @syndlex esphome/components/animation/* @syndlex
esphome/components/anova/* @buxtronix esphome/components/anova/* @buxtronix
@ -83,6 +84,7 @@ esphome/components/esp32_ble_server/* @jesserockz
esphome/components/esp32_camera_web_server/* @ayufan esphome/components/esp32_camera_web_server/* @ayufan
esphome/components/esp32_can/* @Sympatron esphome/components/esp32_can/* @Sympatron
esphome/components/esp32_improv/* @jesserockz esphome/components/esp32_improv/* @jesserockz
esphome/components/esp32_rmt_led_strip/* @jesserockz
esphome/components/esp8266/* @esphome/core esphome/components/esp8266/* @esphome/core
esphome/components/ethernet_info/* @gtjadsonsantos esphome/components/ethernet_info/* @gtjadsonsantos
esphome/components/exposure_notifications/* @OttoWinter esphome/components/exposure_notifications/* @OttoWinter
@ -94,6 +96,7 @@ esphome/components/feedback/* @ianchi
esphome/components/fingerprint_grow/* @OnFreund @loongyh esphome/components/fingerprint_grow/* @OnFreund @loongyh
esphome/components/fs3000/* @kahrendt esphome/components/fs3000/* @kahrendt
esphome/components/globals/* @esphome/core esphome/components/globals/* @esphome/core
esphome/components/gp8403/* @jesserockz
esphome/components/gpio/* @esphome/core esphome/components/gpio/* @esphome/core
esphome/components/gps/* @coogle esphome/components/gps/* @coogle
esphome/components/graph/* @synco esphome/components/graph/* @synco
@ -106,13 +109,16 @@ esphome/components/heatpumpir/* @rob-deutsch
esphome/components/hitachi_ac424/* @sourabhjaiswal esphome/components/hitachi_ac424/* @sourabhjaiswal
esphome/components/homeassistant/* @OttoWinter esphome/components/homeassistant/* @OttoWinter
esphome/components/honeywellabp/* @RubyBailey esphome/components/honeywellabp/* @RubyBailey
esphome/components/host/* @esphome/core
esphome/components/hrxl_maxsonar_wr/* @netmikey esphome/components/hrxl_maxsonar_wr/* @netmikey
esphome/components/hte501/* @Stock-M esphome/components/hte501/* @Stock-M
esphome/components/hydreon_rgxx/* @functionpointer esphome/components/hydreon_rgxx/* @functionpointer
esphome/components/hyt271/* @Philippe12
esphome/components/i2c/* @esphome/core esphome/components/i2c/* @esphome/core
esphome/components/i2s_audio/* @jesserockz esphome/components/i2s_audio/* @jesserockz
esphome/components/i2s_audio/media_player/* @jesserockz esphome/components/i2s_audio/media_player/* @jesserockz
esphome/components/i2s_audio/microphone/* @jesserockz esphome/components/i2s_audio/microphone/* @jesserockz
esphome/components/i2s_audio/speaker/* @jesserockz
esphome/components/ili9xxx/* @nielsnl68 esphome/components/ili9xxx/* @nielsnl68
esphome/components/improv_base/* @esphome/core esphome/components/improv_base/* @esphome/core
esphome/components/improv_serial/* @esphome/core esphome/components/improv_serial/* @esphome/core
@ -138,6 +144,7 @@ esphome/components/ltr390/* @sjtrny
esphome/components/matrix_keypad/* @ssieb esphome/components/matrix_keypad/* @ssieb
esphome/components/max31865/* @DAVe3283 esphome/components/max31865/* @DAVe3283
esphome/components/max44009/* @berfenger esphome/components/max44009/* @berfenger
esphome/components/max6956/* @looping40
esphome/components/max7219digit/* @rspaargaren esphome/components/max7219digit/* @rspaargaren
esphome/components/max9611/* @mckaymatthew esphome/components/max9611/* @mckaymatthew
esphome/components/mcp23008/* @jesserockz esphome/components/mcp23008/* @jesserockz
@ -162,6 +169,7 @@ esphome/components/midea/* @dudanov
esphome/components/midea_ir/* @dudanov esphome/components/midea_ir/* @dudanov
esphome/components/mitsubishi/* @RubyBailey esphome/components/mitsubishi/* @RubyBailey
esphome/components/mlx90393/* @functionpointer esphome/components/mlx90393/* @functionpointer
esphome/components/mlx90614/* @jesserockz
esphome/components/mmc5603/* @benhoff esphome/components/mmc5603/* @benhoff
esphome/components/modbus_controller/* @martgras esphome/components/modbus_controller/* @martgras
esphome/components/modbus_controller/binary_sensor/* @martgras esphome/components/modbus_controller/binary_sensor/* @martgras
@ -186,6 +194,7 @@ esphome/components/nfc/* @jesserockz
esphome/components/number/* @esphome/core esphome/components/number/* @esphome/core
esphome/components/ota/* @esphome/core esphome/components/ota/* @esphome/core
esphome/components/output/* @esphome/core esphome/components/output/* @esphome/core
esphome/components/pca6416a/* @Mat931
esphome/components/pca9554/* @hwstar esphome/components/pca9554/* @hwstar
esphome/components/pcf85063/* @brogon esphome/components/pcf85063/* @brogon
esphome/components/pid/* @OttoWinter esphome/components/pid/* @OttoWinter
@ -232,7 +241,7 @@ esphome/components/shutdown/* @esphome/core @jsuanet
esphome/components/sigma_delta_output/* @Cat-Ion esphome/components/sigma_delta_output/* @Cat-Ion
esphome/components/sim800l/* @glmnet esphome/components/sim800l/* @glmnet
esphome/components/sm10bit_base/* @Cossid esphome/components/sm10bit_base/* @Cossid
esphome/components/sm2135/* @BoukeHaarsma23 esphome/components/sm2135/* @BoukeHaarsma23 @dd32 @matika77
esphome/components/sm2235/* @Cossid esphome/components/sm2235/* @Cossid
esphome/components/sm2335/* @Cossid esphome/components/sm2335/* @Cossid
esphome/components/sml/* @alengwenus esphome/components/sml/* @alengwenus
@ -240,6 +249,7 @@ esphome/components/smt100/* @piechade
esphome/components/sn74hc165/* @jesserockz esphome/components/sn74hc165/* @jesserockz
esphome/components/socket/* @esphome/core esphome/components/socket/* @esphome/core
esphome/components/sonoff_d1/* @anatoly-savchenkov esphome/components/sonoff_d1/* @anatoly-savchenkov
esphome/components/speaker/* @jesserockz
esphome/components/spi/* @esphome/core esphome/components/spi/* @esphome/core
esphome/components/sprinkler/* @kbx81 esphome/components/sprinkler/* @kbx81
esphome/components/sps30/* @martgras esphome/components/sps30/* @martgras

View File

@ -24,8 +24,9 @@ RUN \
python3-setuptools=52.0.0-4 \ python3-setuptools=52.0.0-4 \
python3-pil=8.1.2+dfsg-0.3+deb11u1 \ python3-pil=8.1.2+dfsg-0.3+deb11u1 \
python3-cryptography=3.3.2-1 \ python3-cryptography=3.3.2-1 \
python3-venv=3.9.2-3 \
iputils-ping=3:20210202-1 \ 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 \ curl=7.74.0-1.3+deb11u7 \
openssh-client=1:8.4p1-5+deb11u1 \ openssh-client=1:8.4p1-5+deb11u1 \
&& rm -rf \ && rm -rf \
@ -59,10 +60,10 @@ RUN \
# First install requirements to leverage caching when requirements don't change # 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 \ RUN \
pip3 install --no-cache-dir -r /requirements.txt -r /requirements_optional.txt \ 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 ======================= # ======================= docker-type image =======================

View File

@ -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])

View File

@ -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"] 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

View File

@ -1,133 +1,27 @@
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome import pins
from esphome.components import sensor, voltage_sampler from esphome.components import sensor, voltage_sampler
from esphome.components.esp32 import get_esp32_variant
from esphome.const import ( from esphome.const import (
CONF_ATTENUATION, CONF_ATTENUATION,
CONF_RAW,
CONF_ID, CONF_ID,
CONF_INPUT,
CONF_NUMBER, CONF_NUMBER,
CONF_PIN, CONF_PIN,
CONF_RAW,
DEVICE_CLASS_VOLTAGE, DEVICE_CLASS_VOLTAGE,
STATE_CLASS_MEASUREMENT, STATE_CLASS_MEASUREMENT,
UNIT_VOLT, UNIT_VOLT,
) )
from esphome.core import CORE 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"] 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): def validate_config(config):
if config[CONF_RAW] and config.get(CONF_ATTENUATION, None) == "auto": if config[CONF_RAW] and config.get(CONF_ATTENUATION, None) == "auto":

View File

@ -0,0 +1 @@
CODEOWNERS = ["@buxtronix"]

View File

@ -5,7 +5,7 @@ from esphome.const import CONF_ID, CONF_PIN
CODEOWNERS = ["@buxtronix"] CODEOWNERS = ["@buxtronix"]
DEPENDENCIES = ["ble_client"] DEPENDENCIES = ["ble_client"]
AUTO_LOAD = ["am43", "sensor"] AUTO_LOAD = ["am43"]
CONF_INVERT_POSITION = "invert_position" 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]) var = cg.new_Pvariable(config[CONF_ID])
cg.add(var.set_pin(config[CONF_PIN])) cg.add(var.set_pin(config[CONF_PIN]))
cg.add(var.set_invert_position(config[CONF_INVERT_POSITION])) cg.add(var.set_invert_position(config[CONF_INVERT_POSITION]))
yield cg.register_component(var, config) await cg.register_component(var, config)
yield cover.register_cover(var, config) await cover.register_cover(var, config)
yield ble_client.register_ble_node(var, config) await ble_client.register_ble_node(var, config)

View File

@ -40,6 +40,7 @@ void Am43Component::loop() {
CoverTraits Am43Component::get_traits() { CoverTraits Am43Component::get_traits() {
auto traits = CoverTraits(); auto traits = CoverTraits();
traits.set_supports_stop(true);
traits.set_supports_position(true); traits.set_supports_position(true);
traits.set_supports_tilt(false); traits.set_supports_tilt(false);
traits.set_is_assumed_state(false); traits.set_is_assumed_state(false);

View File

@ -11,6 +11,7 @@ from esphome.const import (
UNIT_PERCENT, UNIT_PERCENT,
) )
AUTO_LOAD = ["am43"]
CODEOWNERS = ["@buxtronix"] CODEOWNERS = ["@buxtronix"]
am43_ns = cg.esphome_ns.namespace("am43") 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]) var = cg.new_Pvariable(config[CONF_ID])
yield cg.register_component(var, config) await cg.register_component(var, config)
yield ble_client.register_ble_node(var, config) await ble_client.register_ble_node(var, config)
if CONF_BATTERY_LEVEL in 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)) cg.add(var.set_battery(sens))
if CONF_ILLUMINANCE in config: 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)) cg.add(var.set_illuminance(sens))

View File

@ -1,6 +1,6 @@
#include "am43.h" #include "am43_sensor.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/log.h"
#ifdef USE_ESP32 #ifdef USE_ESP32

View File

@ -288,6 +288,7 @@ message ListEntitiesCoverResponse {
bool disabled_by_default = 9; bool disabled_by_default = 9;
string icon = 10; string icon = 10;
EntityCategory entity_category = 11; EntityCategory entity_category = 11;
bool supports_stop = 12;
} }
enum LegacyCoverState { enum LegacyCoverState {
@ -861,8 +862,7 @@ message ClimateStateResponse {
float target_temperature = 4; float target_temperature = 4;
float target_temperature_low = 5; float target_temperature_low = 5;
float target_temperature_high = 6; float target_temperature_high = 6;
// For older peers, equal to preset == CLIMATE_PRESET_AWAY bool unused_legacy_away = 7;
bool legacy_away = 7;
ClimateAction action = 8; ClimateAction action = 8;
ClimateFanMode fan_mode = 9; ClimateFanMode fan_mode = 9;
ClimateSwingMode swing_mode = 10; ClimateSwingMode swing_mode = 10;
@ -885,9 +885,8 @@ message ClimateCommandRequest {
float target_temperature_low = 7; float target_temperature_low = 7;
bool has_target_temperature_high = 8; bool has_target_temperature_high = 8;
float target_temperature_high = 9; float target_temperature_high = 9;
// legacy, for older peers, newer ones should use CLIMATE_PRESET_AWAY in preset bool unused_has_legacy_away = 10;
bool has_legacy_away = 10; bool unused_legacy_away = 11;
bool legacy_away = 11;
bool has_fan_mode = 12; bool has_fan_mode = 12;
ClimateFanMode fan_mode = 13; ClimateFanMode fan_mode = 13;
bool has_swing_mode = 14; bool has_swing_mode = 14;

View File

@ -530,7 +530,6 @@ bool APIConnection::send_climate_state(climate::Climate *climate) {
resp.custom_fan_mode = climate->custom_fan_mode.value(); resp.custom_fan_mode = climate->custom_fan_mode.value();
if (traits.get_supports_presets() && climate->preset.has_value()) { if (traits.get_supports_presets() && climate->preset.has_value()) {
resp.preset = static_cast<enums::ClimatePreset>(climate->preset.value()); resp.preset = static_cast<enums::ClimatePreset>(climate->preset.value());
resp.legacy_away = resp.preset == enums::CLIMATE_PRESET_AWAY;
} }
if (!traits.get_supported_custom_presets().empty() && climate->custom_preset.has_value()) if (!traits.get_supported_custom_presets().empty() && climate->custom_preset.has_value())
resp.custom_preset = climate->custom_preset.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); call.set_target_temperature_low(msg.target_temperature_low);
if (msg.has_target_temperature_high) if (msg.has_target_temperature_high)
call.set_target_temperature_high(msg.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) if (msg.has_fan_mode)
call.set_fan_mode(static_cast<climate::ClimateFanMode>(msg.fan_mode)); call.set_fan_mode(static_cast<climate::ClimateFanMode>(msg.fan_mode));
if (msg.has_custom_fan_mode) if (msg.has_custom_fan_mode)
@ -944,7 +941,7 @@ HelloResponse APIConnection::hello(const HelloRequest &msg) {
HelloResponse resp; HelloResponse resp;
resp.api_version_major = 1; 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.server_info = App.get_name() + " (esphome v" ESPHOME_VERSION ")";
resp.name = App.get_name(); resp.name = App.get_name();
@ -981,6 +978,8 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) {
resp.manufacturer = "Espressif"; resp.manufacturer = "Espressif";
#elif defined(USE_RP2040) #elif defined(USE_RP2040)
resp.manufacturer = "Raspberry Pi"; resp.manufacturer = "Raspberry Pi";
#elif defined(USE_HOST)
resp.manufacturer = "Host";
#endif #endif
resp.model = ESPHOME_BOARD; resp.model = ESPHOME_BOARD;
#ifdef USE_DEEP_SLEEP #ifdef USE_DEEP_SLEEP
@ -999,7 +998,7 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) {
: bluetooth_proxy::PASSIVE_ONLY_VERSION; : bluetooth_proxy::PASSIVE_ONLY_VERSION;
#endif #endif
#ifdef USE_VOICE_ASSISTANT #ifdef USE_VOICE_ASSISTANT
resp.voice_assistant_version = 1; resp.voice_assistant_version = voice_assistant::global_voice_assistant->get_version();
#endif #endif
return resp; return resp;
} }

View File

@ -941,6 +941,10 @@ bool ListEntitiesCoverResponse::decode_varint(uint32_t field_id, ProtoVarInt val
this->entity_category = value.as_enum<enums::EntityCategory>(); this->entity_category = value.as_enum<enums::EntityCategory>();
return true; return true;
} }
case 12: {
this->supports_stop = value.as_bool();
return true;
}
default: default:
return false; return false;
} }
@ -993,6 +997,7 @@ void ListEntitiesCoverResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_bool(9, this->disabled_by_default); buffer.encode_bool(9, this->disabled_by_default);
buffer.encode_string(10, this->icon); buffer.encode_string(10, this->icon);
buffer.encode_enum<enums::EntityCategory>(11, this->entity_category); buffer.encode_enum<enums::EntityCategory>(11, this->entity_category);
buffer.encode_bool(12, this->supports_stop);
} }
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void ListEntitiesCoverResponse::dump_to(std::string &out) const { 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(" entity_category: ");
out.append(proto_enum_to_string<enums::EntityCategory>(this->entity_category)); out.append(proto_enum_to_string<enums::EntityCategory>(this->entity_category));
out.append("\n"); out.append("\n");
out.append(" supports_stop: ");
out.append(YESNO(this->supports_stop));
out.append("\n");
out.append("}"); out.append("}");
} }
#endif #endif
@ -3649,7 +3658,7 @@ bool ClimateStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
return true; return true;
} }
case 7: { case 7: {
this->legacy_away = value.as_bool(); this->unused_legacy_away = value.as_bool();
return true; return true;
} }
case 8: { case 8: {
@ -3719,7 +3728,7 @@ void ClimateStateResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_float(4, this->target_temperature); buffer.encode_float(4, this->target_temperature);
buffer.encode_float(5, this->target_temperature_low); buffer.encode_float(5, this->target_temperature_low);
buffer.encode_float(6, this->target_temperature_high); buffer.encode_float(6, this->target_temperature_high);
buffer.encode_bool(7, this->legacy_away); buffer.encode_bool(7, this->unused_legacy_away);
buffer.encode_enum<enums::ClimateAction>(8, this->action); buffer.encode_enum<enums::ClimateAction>(8, this->action);
buffer.encode_enum<enums::ClimateFanMode>(9, this->fan_mode); buffer.encode_enum<enums::ClimateFanMode>(9, this->fan_mode);
buffer.encode_enum<enums::ClimateSwingMode>(10, this->swing_mode); buffer.encode_enum<enums::ClimateSwingMode>(10, this->swing_mode);
@ -3760,8 +3769,8 @@ void ClimateStateResponse::dump_to(std::string &out) const {
out.append(buffer); out.append(buffer);
out.append("\n"); out.append("\n");
out.append(" legacy_away: "); out.append(" unused_legacy_away: ");
out.append(YESNO(this->legacy_away)); out.append(YESNO(this->unused_legacy_away));
out.append("\n"); out.append("\n");
out.append(" action: "); out.append(" action: ");
@ -3813,11 +3822,11 @@ bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value)
return true; return true;
} }
case 10: { case 10: {
this->has_legacy_away = value.as_bool(); this->unused_has_legacy_away = value.as_bool();
return true; return true;
} }
case 11: { case 11: {
this->legacy_away = value.as_bool(); this->unused_legacy_away = value.as_bool();
return true; return true;
} }
case 12: { case 12: {
@ -3902,8 +3911,8 @@ void ClimateCommandRequest::encode(ProtoWriteBuffer buffer) const {
buffer.encode_float(7, this->target_temperature_low); buffer.encode_float(7, this->target_temperature_low);
buffer.encode_bool(8, this->has_target_temperature_high); buffer.encode_bool(8, this->has_target_temperature_high);
buffer.encode_float(9, this->target_temperature_high); buffer.encode_float(9, this->target_temperature_high);
buffer.encode_bool(10, this->has_legacy_away); buffer.encode_bool(10, this->unused_has_legacy_away);
buffer.encode_bool(11, this->legacy_away); buffer.encode_bool(11, this->unused_legacy_away);
buffer.encode_bool(12, this->has_fan_mode); buffer.encode_bool(12, this->has_fan_mode);
buffer.encode_enum<enums::ClimateFanMode>(13, this->fan_mode); buffer.encode_enum<enums::ClimateFanMode>(13, this->fan_mode);
buffer.encode_bool(14, this->has_swing_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(buffer);
out.append("\n"); out.append("\n");
out.append(" has_legacy_away: "); out.append(" unused_has_legacy_away: ");
out.append(YESNO(this->has_legacy_away)); out.append(YESNO(this->unused_has_legacy_away));
out.append("\n"); out.append("\n");
out.append(" legacy_away: "); out.append(" unused_legacy_away: ");
out.append(YESNO(this->legacy_away)); out.append(YESNO(this->unused_legacy_away));
out.append("\n"); out.append("\n");
out.append(" has_fan_mode: "); out.append(" has_fan_mode: ");

View File

@ -375,6 +375,7 @@ class ListEntitiesCoverResponse : public ProtoMessage {
bool disabled_by_default{false}; bool disabled_by_default{false};
std::string icon{}; std::string icon{};
enums::EntityCategory entity_category{}; enums::EntityCategory entity_category{};
bool supports_stop{false};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
@ -958,7 +959,7 @@ class ClimateStateResponse : public ProtoMessage {
float target_temperature{0.0f}; float target_temperature{0.0f};
float target_temperature_low{0.0f}; float target_temperature_low{0.0f};
float target_temperature_high{0.0f}; float target_temperature_high{0.0f};
bool legacy_away{false}; bool unused_legacy_away{false};
enums::ClimateAction action{}; enums::ClimateAction action{};
enums::ClimateFanMode fan_mode{}; enums::ClimateFanMode fan_mode{};
enums::ClimateSwingMode swing_mode{}; enums::ClimateSwingMode swing_mode{};
@ -986,8 +987,8 @@ class ClimateCommandRequest : public ProtoMessage {
float target_temperature_low{0.0f}; float target_temperature_low{0.0f};
bool has_target_temperature_high{false}; bool has_target_temperature_high{false};
float target_temperature_high{0.0f}; float target_temperature_high{0.0f};
bool has_legacy_away{false}; bool unused_has_legacy_away{false};
bool legacy_away{false}; bool unused_legacy_away{false};
bool has_fan_mode{false}; bool has_fan_mode{false};
enums::ClimateFanMode fan_mode{}; enums::ClimateFanMode fan_mode{};
bool has_swing_mode{false}; bool has_swing_mode{false};

View File

@ -18,5 +18,5 @@ async def to_code(config):
# https://github.com/esphome/AsyncTCP/blob/master/library.json # https://github.com/esphome/AsyncTCP/blob/master/library.json
cg.add_library("esphome/AsyncTCP-esphome", "1.2.2") cg.add_library("esphome/AsyncTCP-esphome", "1.2.2")
elif CORE.is_esp8266: elif CORE.is_esp8266:
# https://github.com/OttoWinter/ESPAsyncTCP # https://github.com/esphome/ESPAsyncTCP
cg.add_library("ottowinter/ESPAsyncTCP-esphome", "1.2.3") cg.add_library("esphome/ESPAsyncTCP-esphome", "1.2.3")

View File

@ -43,12 +43,7 @@ void BinarySensor::send_state_internal(bool state, bool is_initial) {
} }
BinarySensor::BinarySensor() : state(false) {} 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) { void BinarySensor::add_filter(Filter *filter) {
filter->parent_ = this; filter->parent_ = this;
if (this->filter_list_ == nullptr) { if (this->filter_list_ == nullptr) {

View File

@ -34,7 +34,7 @@ namespace binary_sensor {
* The sub classes should notify the front-end of new states via the publish_state() method which * The sub classes should notify the front-end of new states via the publish_state() method which
* handles inverted inputs for you. * handles inverted inputs for you.
*/ */
class BinarySensor : public EntityBase { class BinarySensor : public EntityBase, public EntityBase_DeviceClass {
public: public:
explicit BinarySensor(); explicit BinarySensor();
@ -60,12 +60,6 @@ class BinarySensor : public EntityBase {
/// The current reported state of the binary sensor. /// The current reported state of the binary sensor.
bool state; 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_filter(Filter *filter);
void add_filters(const std::vector<Filter *> &filters); void add_filters(const std::vector<Filter *> &filters);
@ -82,7 +76,6 @@ class BinarySensor : public EntityBase {
protected: protected:
CallbackManager<void(bool)> state_callback_{}; CallbackManager<void(bool)> state_callback_{};
optional<std::string> device_class_{}; ///< Stores the override of the device class
Filter *filter_list_{nullptr}; Filter *filter_list_{nullptr};
bool has_state_{false}; bool has_state_{false};
bool publish_initial_state_{false}; bool publish_initial_state_{false};

View File

@ -16,6 +16,9 @@ void BinarySensorMap::loop() {
case BINARY_SENSOR_MAP_TYPE_SUM: case BINARY_SENSOR_MAP_TYPE_SUM:
this->process_sum_(); this->process_sum_();
break; 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; float total_current_value = 0.0;
uint8_t num_active_sensors = 0; uint8_t num_active_sensors = 0;
uint64_t mask = 0x00; 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++) { for (size_t i = 0; i < this->channels_.size(); i++) {
auto bs = this->channels_[i]; auto bs = this->channels_[i];
if (bs.binary_sensor->state) { if (bs.binary_sensor->state) {
num_active_sensors++; num_active_sensors++;
total_current_value += bs.sensor_value; total_current_value += bs.parameters.sensor_value;
mask |= 1ULL << i; mask |= 1ULL << i;
} }
} }
// check if the sensor map was touched
// potentially update state only if a binary_sensor is active
if (mask != 0ULL) { 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) { if (this->last_mask_ != mask) {
float publish_value = total_current_value / num_active_sensors; float publish_value = total_current_value / num_active_sensors;
this->publish_state(publish_value); this->publish_state(publish_value);
} }
} else if (this->last_mask_ != 0ULL) { } 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()); ESP_LOGV(TAG, "'%s' - No binary sensor active, publishing NAN", this->name_.c_str());
this->publish_state(NAN); this->publish_state(NAN);
} }
this->last_mask_ = mask; this->last_mask_ = mask;
} }
void BinarySensorMap::process_sum_() { void BinarySensorMap::process_sum_() {
float total_current_value = 0.0; float total_current_value = 0.0;
uint64_t mask = 0x00; uint64_t mask = 0x00;
// - check all binary_sensor states // - check all binary_sensor states
// - if active, add its value to total_current_value // - 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++) { for (size_t i = 0; i < this->channels_.size(); i++) {
auto bs = this->channels_[i]; auto bs = this->channels_[i];
if (bs.binary_sensor->state) { if (bs.binary_sensor->state) {
total_current_value += bs.sensor_value; total_current_value += bs.parameters.sensor_value;
mask |= 1ULL << i; 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())) { if ((this->last_mask_ != mask) || (!this->has_state())) {
this->publish_state(total_current_value); this->publish_state(total_current_value);
} }
@ -70,15 +78,65 @@ void BinarySensorMap::process_sum_() {
this->last_mask_ = mask; 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) { void BinarySensorMap::add_channel(binary_sensor::BinarySensor *sensor, float value) {
BinarySensorMapChannel sensor_channel{ BinarySensorMapChannel sensor_channel{
.binary_sensor = sensor, .binary_sensor = sensor,
.parameters{
.sensor_value = value, .sensor_value = value,
},
}; };
this->channels_.push_back(sensor_channel); 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 binary_sensor_map
} // namespace esphome } // namespace esphome

View File

@ -12,51 +12,88 @@ namespace binary_sensor_map {
enum BinarySensorMapType { enum BinarySensorMapType {
BINARY_SENSOR_MAP_TYPE_GROUP, BINARY_SENSOR_MAP_TYPE_GROUP,
BINARY_SENSOR_MAP_TYPE_SUM, BINARY_SENSOR_MAP_TYPE_SUM,
BINARY_SENSOR_MAP_TYPE_BAYESIAN,
}; };
struct BinarySensorMapChannel { struct BinarySensorMapChannel {
binary_sensor::BinarySensor *binary_sensor; binary_sensor::BinarySensor *binary_sensor;
union {
float sensor_value; 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 { class BinarySensorMap : public sensor::Sensor, public Component {
public: public:
void dump_config() override; void dump_config() override;
/** /**
* The loop checks all binary_sensor states * The loop calls the configured type processing method
* When the binary_sensor reports a true value for its state, then the float value it represents is added to the
* total_current_value
* *
* Only when the total_current_value changed and at least one sensor reports an active state we publish the sensors * The processing method loops through all sensors and calculates the numerical result
* average value. When the value changed and no sensors ar active we publish NAN. * The result is only published if a binary sensor state has changed or, for some types, on initial boot
* */ */
void loop() override; 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 *sensor The binary sensor.
* @param value The value this binary_sensor represents * @param value The value this binary_sensor represents
*/ */
void add_channel(binary_sensor::BinarySensor *sensor, float value); 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: protected:
std::vector<BinarySensorMapChannel> channels_{}; std::vector<BinarySensorMapChannel> channels_{};
BinarySensorMapType sensor_type_{BINARY_SENSOR_MAP_TYPE_GROUP}; 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}; 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 * Methods to process the binary_sensor_maps types
* GROUP: process_group_() just map to a value *
* GROUP: process_group_() averages all the values
* ADD: process_add_() adds all the values * ADD: process_add_() adds all the values
* BAYESIAN: process_bayesian_() computes the predicate probability
* */ * */
void process_group_(); void process_group_();
void process_sum_(); 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 } // namespace binary_sensor_map

View File

@ -20,16 +20,29 @@ BinarySensorMap = binary_sensor_map_ns.class_(
) )
SensorMapType = binary_sensor_map_ns.enum("SensorMapType") 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 = { SENSOR_MAP_TYPES = {
CONF_GROUP: SensorMapType.BINARY_SENSOR_MAP_TYPE_GROUP, CONF_GROUP: SensorMapType.BINARY_SENSOR_MAP_TYPE_GROUP,
CONF_SUM: SensorMapType.BINARY_SENSOR_MAP_TYPE_SUM, 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_BINARY_SENSOR): cv.use_id(binary_sensor.BinarySensor),
cv.Required(CONF_VALUE): cv.float_, 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( CONFIG_SCHEMA = cv.typed_schema(
{ {
CONF_GROUP: sensor.sensor_schema( CONF_GROUP: sensor.sensor_schema(
@ -39,7 +52,7 @@ CONFIG_SCHEMA = cv.typed_schema(
).extend( ).extend(
{ {
cv.Required(CONF_CHANNELS): cv.All( 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( ).extend(
{ {
cv.Required(CONF_CHANNELS): cv.All( 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]] constant = SENSOR_MAP_TYPES[config[CONF_TYPE]]
cg.add(var.set_sensor_type(constant)) cg.add(var.set_sensor_type(constant))
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]: for ch in config[CONF_CHANNELS]:
input_var = await cg.get_variable(ch[CONF_BINARY_SENSOR]) input_var = await cg.get_variable(ch[CONF_BINARY_SENSOR])
cg.add(var.add_channel(input_var, ch[CONF_VALUE])) cg.add(var.add_channel(input_var, ch[CONF_VALUE]))

View File

@ -29,8 +29,35 @@ BLEClientConnectTrigger = ble_client_ns.class_(
BLEClientDisconnectTrigger = ble_client_ns.class_( BLEClientDisconnectTrigger = ble_client_ns.class_(
"BLEClientDisconnectTrigger", automation.Trigger.template(BLEClientNodeConstRef) "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 # Actions
BLEWriteAction = ble_client_ns.class_("BLEClientWriteAction", automation.Action) 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 # Espressif platformio framework is built with MAX_BLE_CONN to 3, so
# enforce this in yaml checks. # 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) .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( @automation.register_action(
"ble_client.ble_write", BLEWriteAction, BLE_WRITE_ACTION_SCHEMA "ble_client.ble_write", BLEWriteAction, BLE_WRITE_ACTION_SCHEMA
) )
async def ble_write_to_code(config, action_id, template_arg, args): async def ble_write_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID]) parent = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren) var = cg.new_Pvariable(action_id, template_arg, parent)
value = config[CONF_VALUE] value = config[CONF_VALUE]
if cg.is_template(value): if cg.is_template(value):
@ -137,6 +208,54 @@ async def ble_write_to_code(config, action_id, template_arg, args):
return var 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): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config) await cg.register_component(var, config)
@ -148,3 +267,12 @@ async def to_code(config):
for conf in config.get(CONF_ON_DISCONNECT, []): for conf in config.get(CONF_ON_DISCONNECT, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf) 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)

View File

@ -37,6 +37,44 @@ class BLEClientDisconnectTrigger : public Trigger<>, public BLEClientNode {
} }
}; };
class BLEClientPasskeyRequestTrigger : public Trigger<>, public BLEClientNode {
public:
explicit BLEClientPasskeyRequestTrigger(BLEClient *parent) { parent->register_ble_node(this); }
void loop() override {}
void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override {
if (event == ESP_GAP_BLE_PASSKEY_REQ_EVT &&
memcmp(param->ble_security.auth_cmpl.bd_addr, this->parent_->get_remote_bda(), 6) == 0) {
this->trigger();
}
}
};
class BLEClientPasskeyNotificationTrigger : public Trigger<uint32_t>, public BLEClientNode {
public:
explicit BLEClientPasskeyNotificationTrigger(BLEClient *parent) { parent->register_ble_node(this); }
void loop() override {}
void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override {
if (event == ESP_GAP_BLE_PASSKEY_NOTIF_EVT &&
memcmp(param->ble_security.auth_cmpl.bd_addr, this->parent_->get_remote_bda(), 6) == 0) {
uint32_t passkey = param->ble_security.key_notif.passkey;
this->trigger(passkey);
}
}
};
class BLEClientNumericComparisonRequestTrigger : public Trigger<uint32_t>, public BLEClientNode {
public:
explicit BLEClientNumericComparisonRequestTrigger(BLEClient *parent) { parent->register_ble_node(this); }
void loop() override {}
void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override {
if (event == ESP_GAP_BLE_NC_REQ_EVT &&
memcmp(param->ble_security.auth_cmpl.bd_addr, this->parent_->get_remote_bda(), 6) == 0) {
uint32_t passkey = param->ble_security.key_notif.passkey;
this->trigger(passkey);
}
}
};
class BLEWriterClientNode : public BLEClientNode { class BLEWriterClientNode : public BLEClientNode {
public: public:
BLEWriterClientNode(BLEClient *ble_client) { BLEWriterClientNode(BLEClient *ble_client) {
@ -94,6 +132,86 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
std::function<std::vector<uint8_t>(Ts...)> value_template_{}; std::function<std::vector<uint8_t>(Ts...)> value_template_{};
}; };
template<typename... Ts> class BLEClientPasskeyReplyAction : public Action<Ts...> {
public:
BLEClientPasskeyReplyAction(BLEClient *ble_client) { parent_ = ble_client; }
void play(Ts... x) override {
uint32_t passkey;
if (has_simple_value_) {
passkey = this->value_simple_;
} else {
passkey = this->value_template_(x...);
}
if (passkey > 999999)
return;
esp_bd_addr_t remote_bda;
memcpy(remote_bda, parent_->get_remote_bda(), sizeof(esp_bd_addr_t));
esp_ble_passkey_reply(remote_bda, true, passkey);
}
void set_value_template(std::function<uint32_t(Ts...)> func) {
this->value_template_ = std::move(func);
has_simple_value_ = false;
}
void set_value_simple(const uint32_t &value) {
this->value_simple_ = value;
has_simple_value_ = true;
}
private:
BLEClient *parent_{nullptr};
bool has_simple_value_ = true;
uint32_t value_simple_{0};
std::function<uint32_t(Ts...)> value_template_{};
};
template<typename... Ts> class BLEClientNumericComparisonReplyAction : public Action<Ts...> {
public:
BLEClientNumericComparisonReplyAction(BLEClient *ble_client) { parent_ = ble_client; }
void play(Ts... x) override {
esp_bd_addr_t remote_bda;
memcpy(remote_bda, parent_->get_remote_bda(), sizeof(esp_bd_addr_t));
if (has_simple_value_) {
esp_ble_confirm_reply(remote_bda, this->value_simple_);
} else {
esp_ble_confirm_reply(remote_bda, this->value_template_(x...));
}
}
void set_value_template(std::function<bool(Ts...)> func) {
this->value_template_ = std::move(func);
has_simple_value_ = false;
}
void set_value_simple(const bool &value) {
this->value_simple_ = value;
has_simple_value_ = true;
}
private:
BLEClient *parent_{nullptr};
bool has_simple_value_ = true;
bool value_simple_{false};
std::function<bool(Ts...)> value_template_{};
};
template<typename... Ts> class BLEClientRemoveBondAction : public Action<Ts...> {
public:
BLEClientRemoveBondAction(BLEClient *ble_client) { parent_ = ble_client; }
void play(Ts... x) override {
esp_bd_addr_t remote_bda;
memcpy(remote_bda, parent_->get_remote_bda(), sizeof(esp_bd_addr_t));
esp_ble_remove_bond_device(remote_bda);
}
private:
BLEClient *parent_{nullptr};
};
} // namespace ble_client } // namespace ble_client
} // namespace esphome } // namespace esphome

View File

@ -27,7 +27,7 @@ class BLEClient;
class BLEClientNode { class BLEClientNode {
public: public:
virtual void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, 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 gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {}
virtual void loop() {} virtual void loop() {}
void set_address(uint64_t address) { address_ = address; } void set_address(uint64_t address) { address_ = address; }

View File

@ -13,8 +13,5 @@ void Button::press() {
} }
void Button::add_on_press_callback(std::function<void()> &&callback) { this->press_callback_.add(std::move(callback)); } void Button::add_on_press_callback(std::function<void()> &&callback) { this->press_callback_.add(std::move(callback)); }
void Button::set_device_class(const std::string &device_class) { this->device_class_ = device_class; }
std::string Button::get_device_class() { return this->device_class_; }
} // namespace button } // namespace button
} // namespace esphome } // namespace esphome

View File

@ -26,7 +26,7 @@ namespace button {
* *
* A button is just a momentary switch that does not have a state, only a trigger. * 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: public:
/** Press this button. This is called by the front-end. /** Press this button. This is called by the front-end.
* *
@ -40,19 +40,12 @@ class Button : public EntityBase {
*/ */
void add_on_press_callback(std::function<void()> &&callback); void add_on_press_callback(std::function<void()> &&callback);
/// Set the Home Assistant device class (see button::device_class).
void set_device_class(const std::string &device_class);
/// Get the device class for this button.
std::string get_device_class();
protected: protected:
/** You should implement this virtual method if you want to create your own button. /** You should implement this virtual method if you want to create your own button.
*/ */
virtual void press_action() = 0; virtual void press_action() = 0;
CallbackManager<void()> press_callback_{}; CallbackManager<void()> press_callback_{};
std::string device_class_{};
}; };
} // namespace button } // namespace button

View File

@ -343,7 +343,7 @@ CLIMATE_CONTROL_ACTION_SCHEMA = cv.Schema(
cv.Optional(CONF_TARGET_TEMPERATURE): cv.templatable(cv.temperature), cv.Optional(CONF_TARGET_TEMPERATURE): cv.templatable(cv.temperature),
cv.Optional(CONF_TARGET_TEMPERATURE_LOW): 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_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( cv.Exclusive(CONF_FAN_MODE, "fan_mode"): cv.templatable(
validate_climate_fan_mode 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 config[CONF_TARGET_TEMPERATURE_HIGH], args, float
) )
cg.add(var.set_target_temperature_high(template_)) 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: if CONF_FAN_MODE in config:
template_ = await cg.templatable(config[CONF_FAN_MODE], args, ClimateFanMode) template_ = await cg.templatable(config[CONF_FAN_MODE], args, ClimateFanMode)
cg.add(var.set_fan_mode(template_)) cg.add(var.set_fan_mode(template_))

View File

@ -264,25 +264,11 @@ const optional<ClimateMode> &ClimateCall::get_mode() const { return this->mode_;
const optional<float> &ClimateCall::get_target_temperature() const { return this->target_temperature_; } const optional<float> &ClimateCall::get_target_temperature() const { return this->target_temperature_; }
const optional<float> &ClimateCall::get_target_temperature_low() const { return this->target_temperature_low_; } const optional<float> &ClimateCall::get_target_temperature_low() const { return this->target_temperature_low_; }
const optional<float> &ClimateCall::get_target_temperature_high() const { return this->target_temperature_high_; } const optional<float> &ClimateCall::get_target_temperature_high() const { return this->target_temperature_high_; }
optional<bool> ClimateCall::get_away() const {
if (!this->preset_.has_value())
return {};
return *this->preset_ == ClimatePreset::CLIMATE_PRESET_AWAY;
}
const optional<ClimateFanMode> &ClimateCall::get_fan_mode() const { return this->fan_mode_; } const optional<ClimateFanMode> &ClimateCall::get_fan_mode() const { return this->fan_mode_; }
const optional<std::string> &ClimateCall::get_custom_fan_mode() const { return this->custom_fan_mode_; } const optional<std::string> &ClimateCall::get_custom_fan_mode() const { return this->custom_fan_mode_; }
const optional<ClimatePreset> &ClimateCall::get_preset() const { return this->preset_; } const optional<ClimatePreset> &ClimateCall::get_preset() const { return this->preset_; }
const optional<std::string> &ClimateCall::get_custom_preset() const { return this->custom_preset_; } const optional<std::string> &ClimateCall::get_custom_preset() const { return this->custom_preset_; }
const optional<ClimateSwingMode> &ClimateCall::get_swing_mode() const { return this->swing_mode_; } const optional<ClimateSwingMode> &ClimateCall::get_swing_mode() const { return this->swing_mode_; }
ClimateCall &ClimateCall::set_away(bool away) {
this->preset_ = away ? CLIMATE_PRESET_AWAY : CLIMATE_PRESET_HOME;
return *this;
}
ClimateCall &ClimateCall::set_away(optional<bool> away) {
if (away.has_value())
this->preset_ = *away ? CLIMATE_PRESET_AWAY : CLIMATE_PRESET_HOME;
return *this;
}
ClimateCall &ClimateCall::set_target_temperature_high(optional<float> target_temperature_high) { ClimateCall &ClimateCall::set_target_temperature_high(optional<float> target_temperature_high) {
this->target_temperature_high_ = target_temperature_high; this->target_temperature_high_ = target_temperature_high;
return *this; return *this;

View File

@ -64,10 +64,6 @@ class ClimateCall {
* For climate devices with two point target temperature control * For climate devices with two point target temperature control
*/ */
ClimateCall &set_target_temperature_high(optional<float> target_temperature_high); ClimateCall &set_target_temperature_high(optional<float> target_temperature_high);
ESPDEPRECATED("set_away() is deprecated, please use .set_preset(CLIMATE_PRESET_AWAY) instead", "v1.20")
ClimateCall &set_away(bool away);
ESPDEPRECATED("set_away() is deprecated, please use .set_preset(CLIMATE_PRESET_AWAY) instead", "v1.20")
ClimateCall &set_away(optional<bool> away);
/// Set the fan mode of the climate device. /// Set the fan mode of the climate device.
ClimateCall &set_fan_mode(ClimateFanMode fan_mode); ClimateCall &set_fan_mode(ClimateFanMode fan_mode);
/// Set the fan mode of the climate device. /// Set the fan mode of the climate device.
@ -97,8 +93,6 @@ class ClimateCall {
const optional<float> &get_target_temperature() const; const optional<float> &get_target_temperature() const;
const optional<float> &get_target_temperature_low() const; const optional<float> &get_target_temperature_low() const;
const optional<float> &get_target_temperature_high() const; const optional<float> &get_target_temperature_high() const;
ESPDEPRECATED("get_away() is deprecated, please use .get_preset() instead", "v1.20")
optional<bool> get_away() const;
const optional<ClimateFanMode> &get_fan_mode() const; const optional<ClimateFanMode> &get_fan_mode() const;
const optional<ClimateSwingMode> &get_swing_mode() const; const optional<ClimateSwingMode> &get_swing_mode() const;
const optional<std::string> &get_custom_fan_mode() const; const optional<std::string> &get_custom_fan_mode() const;
@ -184,14 +178,6 @@ class Climate : public EntityBase {
}; };
}; };
/** Whether the climate device is in away mode.
*
* Away allows climate devices to have two different target temperature configs:
* one for normal mode and one for away mode.
*/
ESPDEPRECATED("away is deprecated, use preset instead", "v1.20")
bool away{false};
/// The active fan mode of the climate device. /// The active fan mode of the climate device.
optional<ClimateFanMode> fan_mode; optional<ClimateFanMode> fan_mode;

View File

@ -117,15 +117,6 @@ class ClimateTraits {
bool supports_custom_preset(const std::string &custom_preset) const { bool supports_custom_preset(const std::string &custom_preset) const {
return supported_custom_presets_.count(custom_preset); return supported_custom_presets_.count(custom_preset);
} }
ESPDEPRECATED("This method is deprecated, use set_supported_presets() instead", "v1.20")
void set_supports_away(bool supports) {
if (supports) {
supported_presets_.insert(CLIMATE_PRESET_AWAY);
supported_presets_.insert(CLIMATE_PRESET_HOME);
}
}
ESPDEPRECATED("This method is deprecated, use supports_preset() instead", "v1.20")
bool get_supports_away() const { return supports_preset(CLIMATE_PRESET_AWAY); }
void set_supported_swing_modes(std::set<ClimateSwingMode> modes) { supported_swing_modes_ = std::move(modes); } void set_supported_swing_modes(std::set<ClimateSwingMode> modes) { supported_swing_modes_ = std::move(modes); }
void add_supported_swing_mode(ClimateSwingMode mode) { supported_swing_modes_.insert(mode); } void add_supported_swing_mode(ClimateSwingMode mode) { supported_swing_modes_.insert(mode); }

View File

@ -28,6 +28,7 @@ cover::CoverTraits CopyCover::get_traits() {
// copy traits manually so it doesn't break when new options are added // copy traits manually so it doesn't break when new options are added
// but the control() method hasn't implemented them yet. // but the control() method hasn't implemented them yet.
traits.set_is_assumed_state(base.get_is_assumed_state()); 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_position(base.get_supports_position());
traits.set_supports_tilt(base.get_supports_tilt()); traits.set_supports_tilt(base.get_supports_tilt());
traits.set_supports_toggle(base.get_supports_toggle()); traits.set_supports_toggle(base.get_supports_toggle());

View File

@ -145,7 +145,7 @@ CoverCall &CoverCall::set_stop(bool stop) {
return *this; return *this;
} }
bool CoverCall::get_stop() const { return this->stop_; } 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}; } CoverCall Cover::make_call() { return {this}; }
void Cover::open() { void Cover::open() {
auto call = this->make_call(); auto call = this->make_call();
@ -204,11 +204,7 @@ optional<CoverRestoreState> Cover::restore_state_() {
return {}; return {};
return recovered; 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_open() const { return this->position == COVER_OPEN; }
bool Cover::is_fully_closed() const { return this->position == COVER_CLOSED; } bool Cover::is_fully_closed() const { return this->position == COVER_CLOSED; }

View File

@ -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 * to control all values of the cover. Also implement get_traits() to return what operations
* the cover supports. * the cover supports.
*/ */
class Cover : public EntityBase { class Cover : public EntityBase, public EntityBase_DeviceClass {
public: public:
explicit Cover(); explicit Cover();
@ -156,8 +156,6 @@ class Cover : public EntityBase {
void publish_state(bool save = true); void publish_state(bool save = true);
virtual CoverTraits get_traits() = 0; 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 /// Helper method to check if the cover is fully open. Equivalent to comparing .position against 1.0
bool is_fully_open() const; bool is_fully_open() const;
@ -172,7 +170,6 @@ class Cover : public EntityBase {
optional<CoverRestoreState> restore_state_(); optional<CoverRestoreState> restore_state_();
CallbackManager<void()> state_callback_{}; CallbackManager<void()> state_callback_{};
optional<std::string> device_class_override_{};
ESPPreferenceObject rtc_; ESPPreferenceObject rtc_;
}; };

View File

@ -15,12 +15,15 @@ class CoverTraits {
void set_supports_tilt(bool supports_tilt) { this->supports_tilt_ = supports_tilt; } void set_supports_tilt(bool supports_tilt) { this->supports_tilt_ = supports_tilt; }
bool get_supports_toggle() const { return this->supports_toggle_; } bool get_supports_toggle() const { return this->supports_toggle_; }
void set_supports_toggle(bool supports_toggle) { this->supports_toggle_ = 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: protected:
bool is_assumed_state_{false}; bool is_assumed_state_{false};
bool supports_position_{false}; bool supports_position_{false};
bool supports_tilt_{false}; bool supports_tilt_{false};
bool supports_toggle_{false}; bool supports_toggle_{false};
bool supports_stop_{false};
}; };
} // namespace cover } // namespace cover

View File

@ -12,6 +12,7 @@ using namespace esphome::cover;
CoverTraits CurrentBasedCover::get_traits() { CoverTraits CurrentBasedCover::get_traits() {
auto traits = CoverTraits(); auto traits = CoverTraits();
traits.set_supports_stop(true);
traits.set_supports_position(true); traits.set_supports_position(true);
traits.set_supports_toggle(true); traits.set_supports_toggle(true);
traits.set_is_assumed_state(false); traits.set_is_assumed_state(false);

View File

@ -7,9 +7,10 @@ import requests
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
import esphome.final_validate as fv
from esphome import git from esphome import git
from esphome.components.packages import validate_source_shorthand 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.wizard import wizard_file
from esphome.yaml_util import dump from esphome.yaml_util import dump
@ -52,6 +53,17 @@ CONFIG_SCHEMA = cv.All(
validate_full_url, 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_CONFIG = """
wifi: wifi:

View File

@ -72,6 +72,7 @@ class DemoCover : public cover::Cover, public Component {
traits.set_supports_tilt(true); traits.set_supports_tilt(true);
break; break;
case DemoCoverType::TYPE_4: case DemoCoverType::TYPE_4:
traits.set_supports_stop(true);
traits.set_is_assumed_state(true); traits.set_is_assumed_state(true);
traits.set_supports_tilt(true); traits.set_supports_tilt(true);
break; break;

View File

@ -40,6 +40,7 @@ DEVICE = {
NextAction = dfplayer_ns.class_("NextAction", automation.Action) NextAction = dfplayer_ns.class_("NextAction", automation.Action)
PreviousAction = dfplayer_ns.class_("PreviousAction", automation.Action) PreviousAction = dfplayer_ns.class_("PreviousAction", automation.Action)
PlayMp3Action = dfplayer_ns.class_("PlayMp3Action", automation.Action)
PlayFileAction = dfplayer_ns.class_("PlayFileAction", automation.Action) PlayFileAction = dfplayer_ns.class_("PlayFileAction", automation.Action)
PlayFolderAction = dfplayer_ns.class_("PlayFolderAction", automation.Action) PlayFolderAction = dfplayer_ns.class_("PlayFolderAction", automation.Action)
SetVolumeAction = dfplayer_ns.class_("SetVolumeAction", 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 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( @automation.register_action(
"dfplayer.play", "dfplayer.play",
PlayFileAction, PlayFileAction,

View File

@ -7,10 +7,10 @@ namespace dfplayer {
static const char *const TAG = "dfplayer"; static const char *const TAG = "dfplayer";
void DFPlayer::play_folder(uint16_t folder, uint16_t file) { 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->ack_set_is_playing_ = true;
this->send_cmd_(0x0F, (uint8_t) folder, (uint8_t) file); 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->ack_set_is_playing_ = true;
this->send_cmd_(0x14, (((uint16_t) folder) << 12) | file); this->send_cmd_(0x14, (((uint16_t) folder) << 12) | file);
} else { } else {

View File

@ -35,6 +35,10 @@ class DFPlayer : public uart::UARTDevice, public Component {
this->ack_set_is_playing_ = true; this->ack_set_is_playing_ = true;
this->send_cmd_(0x02); 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) { void play_file(uint16_t file) {
this->ack_set_is_playing_ = true; this->ack_set_is_playing_ = true;
this->send_cmd_(0x03, file); this->send_cmd_(0x03, file);
@ -113,6 +117,16 @@ class DFPlayer : public uart::UARTDevice, public Component {
DFPLAYER_SIMPLE_ACTION(NextAction, next) DFPLAYER_SIMPLE_ACTION(NextAction, next)
DFPLAYER_SIMPLE_ACTION(PreviousAction, previous) DFPLAYER_SIMPLE_ACTION(PreviousAction, previous)
template<typename... Ts> class PlayMp3Action : public Action<Ts...>, public Parented<DFPlayer> {
public:
TEMPLATABLE_VALUE(uint16_t, file)
void play(Ts... x) override {
auto file = this->file_.value(x...);
this->parent_->play_mp3(file);
}
};
template<typename... Ts> class PlayFileAction : public Action<Ts...>, public Parented<DFPlayer> { template<typename... Ts> class PlayFileAction : public Action<Ts...>, public Parented<DFPlayer> {
public: public:
TEMPLATABLE_VALUE(uint16_t, file) TEMPLATABLE_VALUE(uint16_t, file)

View File

@ -282,13 +282,17 @@ void DisplayBuffer::print(int x, int y, Font *font, Color color, TextAlign align
int scan_x1, scan_y1, scan_width, scan_height; int scan_x1, scan_y1, scan_width, scan_height;
glyph.scan_area(&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++) { 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)) { if (glyph.get_pixel(glyph_x, glyph_y)) {
this->draw_pixel_at(glyph_x + x_at, glyph_y + y_start, color); this->draw_pixel_at(glyph_x + x_at, glyph_y + y_start, color);
} }
} }
} }
}
x_at += glyph.glyph_data_->width + glyph.glyph_data_->offset_x; x_at += glyph.glyph_data_->width + glyph.glyph_data_->offset_x;

View File

@ -11,6 +11,7 @@ using namespace esphome::cover;
CoverTraits EndstopCover::get_traits() { CoverTraits EndstopCover::get_traits() {
auto traits = CoverTraits(); auto traits = CoverTraits();
traits.set_supports_stop(true);
traits.set_supports_position(true); traits.set_supports_position(true);
traits.set_supports_toggle(true); traits.set_supports_toggle(true);
traits.set_is_assumed_state(false); traits.set_is_assumed_state(false);

View File

@ -7,6 +7,7 @@
#include <freertos/task.h> #include <freertos/task.h>
#include <esp_idf_version.h> #include <esp_idf_version.h>
#include <esp_task_wdt.h> #include <esp_task_wdt.h>
#include <esp_timer.h>
#include <soc/rtc.h> #include <soc/rtc.h>
#if ESP_IDF_VERSION_MAJOR >= 4 #if ESP_IDF_VERSION_MAJOR >= 4

View File

@ -9,10 +9,9 @@ CODEOWNERS = ["@jesserockz"]
CONFLICTS_WITH = ["esp32_ble_beacon"] CONFLICTS_WITH = ["esp32_ble_beacon"]
CONF_BLE_ID = "ble_id" CONF_BLE_ID = "ble_id"
CONF_IO_CAPABILITY = "io_capability"
NO_BLUTOOTH_VARIANTS = [const.VARIANT_ESP32S2] NO_BLUETOOTH_VARIANTS = [const.VARIANT_ESP32S2]
NO_BLUTOOTH_VARIANTS = [const.VARIANT_ESP32S2]
esp32_ble_ns = cg.esphome_ns.namespace("esp32_ble") esp32_ble_ns = cg.esphome_ns.namespace("esp32_ble")
ESP32BLE = esp32_ble_ns.class_("ESP32BLE", cg.Component) ESP32BLE = esp32_ble_ns.class_("ESP32BLE", cg.Component)
@ -21,17 +20,28 @@ GAPEventHandler = esp32_ble_ns.class_("GAPEventHandler")
GATTcEventHandler = esp32_ble_ns.class_("GATTcEventHandler") GATTcEventHandler = esp32_ble_ns.class_("GATTcEventHandler")
GATTsEventHandler = esp32_ble_ns.class_("GATTsEventHandler") 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( CONFIG_SCHEMA = cv.Schema(
{ {
cv.GenerateID(): cv.declare_id(ESP32BLE), cv.GenerateID(): cv.declare_id(ESP32BLE),
cv.Optional(CONF_IO_CAPABILITY, default="none"): cv.enum(
IO_CAPABILITY, lower=True
),
} }
).extend(cv.COMPONENT_SCHEMA) ).extend(cv.COMPONENT_SCHEMA)
def validate_variant(_): def validate_variant(_):
variant = get_esp32_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") raise cv.Invalid(f"{variant} does not support Bluetooth")
@ -41,6 +51,7 @@ FINAL_VALIDATE_SCHEMA = validate_variant
async def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config) await cg.register_component(var, config)
cg.add(var.set_io_capability(config[CONF_IO_CAPABILITY]))
if CORE.using_esp_idf: if CORE.using_esp_idf:
add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True) add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True)

View File

@ -134,8 +134,7 @@ bool ESP32BLE::ble_setup_() {
return false; return false;
} }
esp_ble_io_cap_t iocap = ESP_IO_CAP_NONE; err = esp_ble_gap_set_security_param(ESP_BLE_SM_IOCAP_MODE, &(this->io_cap_), sizeof(uint8_t));
err = esp_ble_gap_set_security_param(ESP_BLE_SM_IOCAP_MODE, &iocap, sizeof(uint8_t));
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ble_gap_set_security_param failed: %d", err); ESP_LOGE(TAG, "esp_ble_gap_set_security_param failed: %d", err);
return false; return false;
@ -215,9 +214,31 @@ float ESP32BLE::get_setup_priority() const { return setup_priority::BLUETOOTH; }
void ESP32BLE::dump_config() { void ESP32BLE::dump_config() {
const uint8_t *mac_address = esp_bt_dev_get_address(); const uint8_t *mac_address = esp_bt_dev_get_address();
if (mac_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, "ESP32 BLE:");
ESP_LOGCONFIG(TAG, " MAC address: %02X:%02X:%02X:%02X:%02X:%02X", mac_address[0], mac_address[1], mac_address[2], 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]); mac_address[3], mac_address[4], mac_address[5]);
ESP_LOGCONFIG(TAG, " IO Capability: %s", io_capability_s);
} else { } else {
ESP_LOGCONFIG(TAG, "ESP32 BLE: bluetooth stack is not enabled"); ESP_LOGCONFIG(TAG, "ESP32 BLE: bluetooth stack is not enabled");
} }

View File

@ -25,6 +25,14 @@ typedef struct {
uint16_t mtu; uint16_t mtu;
} conn_status_t; } 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 { class GAPEventHandler {
public: public:
virtual void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) = 0; 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 { class ESP32BLE : public Component {
public: public:
void set_io_capability(IoCapability io_capability) { this->io_cap_ = (esp_ble_io_cap_t) io_capability; }
void setup() override; void setup() override;
void loop() override; void loop() override;
void dump_config() override; void dump_config() override;
@ -72,6 +82,7 @@ class ESP32BLE : public Component {
Queue<BLEEvent> ble_events_; Queue<BLEEvent> ble_events_;
BLEAdvertising *advertising_; BLEAdvertising *advertising_;
esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE};
}; };
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)

View File

@ -0,0 +1,207 @@
#include "led_strip.h"
#ifdef USE_ESP32
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <esp_attr.h>
namespace esphome {
namespace esp32_rmt_led_strip {
static const char *const TAG = "esp32_rmt_led_strip";
static const uint8_t RMT_CLK_DIV = 2;
void ESP32RMTLEDStripLightOutput::setup() {
ESP_LOGCONFIG(TAG, "Setting up ESP32 LED Strip...");
size_t buffer_size = this->get_buffer_size_();
ExternalRAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
this->buf_ = allocator.allocate(buffer_size);
if (this->buf_ == nullptr) {
ESP_LOGE(TAG, "Cannot allocate LED buffer!");
this->mark_failed();
return;
}
this->effect_data_ = allocator.allocate(this->num_leds_);
if (this->effect_data_ == nullptr) {
ESP_LOGE(TAG, "Cannot allocate effect data!");
this->mark_failed();
return;
}
ExternalRAMAllocator<rmt_item32_t> rmt_allocator(ExternalRAMAllocator<rmt_item32_t>::ALLOW_FAILURE);
this->rmt_buf_ = rmt_allocator.allocate(buffer_size * 8); // 8 bits per byte, 1 rmt_item32_t per bit
rmt_config_t config;
memset(&config, 0, sizeof(config));
config.channel = this->channel_;
config.rmt_mode = RMT_MODE_TX;
config.gpio_num = gpio_num_t(this->pin_);
config.mem_block_num = 1;
config.clk_div = RMT_CLK_DIV;
config.tx_config.loop_en = false;
config.tx_config.carrier_level = RMT_CARRIER_LEVEL_LOW;
config.tx_config.carrier_en = false;
config.tx_config.idle_level = RMT_IDLE_LEVEL_LOW;
config.tx_config.idle_output_en = true;
if (rmt_config(&config) != ESP_OK) {
ESP_LOGE(TAG, "Cannot initialize RMT!");
this->mark_failed();
return;
}
if (rmt_driver_install(config.channel, 0, 0) != ESP_OK) {
ESP_LOGE(TAG, "Cannot install RMT driver!");
this->mark_failed();
return;
}
}
void ESP32RMTLEDStripLightOutput::set_led_params(uint32_t bit0_high, uint32_t bit0_low, uint32_t bit1_high,
uint32_t bit1_low) {
float ratio = (float) APB_CLK_FREQ / RMT_CLK_DIV / 1e09f;
// 0-bit
this->bit0_.duration0 = (uint32_t) (ratio * bit0_high);
this->bit0_.level0 = 1;
this->bit0_.duration1 = (uint32_t) (ratio * bit0_low);
this->bit0_.level1 = 0;
// 1-bit
this->bit1_.duration0 = (uint32_t) (ratio * bit1_high);
this->bit1_.level0 = 1;
this->bit1_.duration1 = (uint32_t) (ratio * bit1_low);
this->bit1_.level1 = 0;
}
void ESP32RMTLEDStripLightOutput::write_state(light::LightState *state) {
// protect from refreshing too often
uint32_t now = micros();
if (*this->max_refresh_rate_ != 0 && (now - this->last_refresh_) < *this->max_refresh_rate_) {
// try again next loop iteration, so that this change won't get lost
this->schedule_show();
return;
}
this->last_refresh_ = now;
this->mark_shown_();
ESP_LOGVV(TAG, "Writing RGB values to bus...");
if (rmt_wait_tx_done(this->channel_, pdMS_TO_TICKS(1000)) != ESP_OK) {
ESP_LOGE(TAG, "RMT TX timeout");
this->status_set_warning();
return;
}
delayMicroseconds(50);
size_t buffer_size = this->get_buffer_size_();
size_t size = 0;
size_t len = 0;
uint8_t *psrc = this->buf_;
rmt_item32_t *pdest = this->rmt_buf_;
while (size < buffer_size) {
uint8_t b = *psrc;
for (int i = 0; i < 8; i++) {
pdest->val = b & (1 << (7 - i)) ? this->bit1_.val : this->bit0_.val;
pdest++;
len++;
}
size++;
psrc++;
}
if (rmt_write_items(this->channel_, this->rmt_buf_, len, false) != ESP_OK) {
ESP_LOGE(TAG, "RMT TX error");
this->status_set_warning();
return;
}
this->status_clear_warning();
}
light::ESPColorView ESP32RMTLEDStripLightOutput::get_view_internal(int32_t index) const {
int32_t r = 0, g = 0, b = 0;
switch (this->rgb_order_) {
case ORDER_RGB:
r = 0;
g = 1;
b = 2;
break;
case ORDER_RBG:
r = 0;
g = 2;
b = 1;
break;
case ORDER_GRB:
r = 1;
g = 0;
b = 2;
break;
case ORDER_GBR:
r = 2;
g = 0;
b = 1;
break;
case ORDER_BGR:
r = 2;
g = 1;
b = 0;
break;
case ORDER_BRG:
r = 1;
g = 2;
b = 0;
break;
}
uint8_t multiplier = this->is_rgbw_ ? 4 : 3;
return {this->buf_ + (index * multiplier) + r,
this->buf_ + (index * multiplier) + g,
this->buf_ + (index * multiplier) + b,
this->is_rgbw_ ? this->buf_ + (index * multiplier) + 3 : nullptr,
&this->effect_data_[index],
&this->correction_};
}
void ESP32RMTLEDStripLightOutput::dump_config() {
ESP_LOGCONFIG(TAG, "ESP32 RMT LED Strip:");
ESP_LOGCONFIG(TAG, " Pin: %u", this->pin_);
ESP_LOGCONFIG(TAG, " Channel: %u", this->channel_);
const char *rgb_order;
switch (this->rgb_order_) {
case ORDER_RGB:
rgb_order = "RGB";
break;
case ORDER_RBG:
rgb_order = "RBG";
break;
case ORDER_GRB:
rgb_order = "GRB";
break;
case ORDER_GBR:
rgb_order = "GBR";
break;
case ORDER_BGR:
rgb_order = "BGR";
break;
case ORDER_BRG:
rgb_order = "BRG";
break;
default:
rgb_order = "UNKNOWN";
break;
}
ESP_LOGCONFIG(TAG, " RGB Order: %s", rgb_order);
ESP_LOGCONFIG(TAG, " Max refresh rate: %u", *this->max_refresh_rate_);
ESP_LOGCONFIG(TAG, " Number of LEDs: %u", this->num_leds_);
}
float ESP32RMTLEDStripLightOutput::get_setup_priority() const { return setup_priority::HARDWARE; }
} // namespace esp32_rmt_led_strip
} // namespace esphome
#endif // USE_ESP32

View File

@ -0,0 +1,87 @@
#pragma once
#ifdef USE_ESP32
#include "esphome/components/light/addressable_light.h"
#include "esphome/components/light/light_output.h"
#include "esphome/core/color.h"
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include <driver/gpio.h>
#include <driver/rmt.h>
#include <esp_err.h>
namespace esphome {
namespace esp32_rmt_led_strip {
enum RGBOrder : uint8_t {
ORDER_RGB,
ORDER_RBG,
ORDER_GRB,
ORDER_GBR,
ORDER_BGR,
ORDER_BRG,
};
class ESP32RMTLEDStripLightOutput : public light::AddressableLight {
public:
void setup() override;
void write_state(light::LightState *state) override;
float get_setup_priority() const override;
int32_t size() const override { return this->num_leds_; }
light::LightTraits get_traits() override {
auto traits = light::LightTraits();
if (this->is_rgbw_) {
traits.set_supported_color_modes({light::ColorMode::RGB, light::ColorMode::RGB_WHITE});
} else {
traits.set_supported_color_modes({light::ColorMode::RGB});
}
return traits;
}
void set_pin(uint8_t pin) { this->pin_ = pin; }
void set_num_leds(uint16_t num_leds) { this->num_leds_ = num_leds; }
void set_is_rgbw(bool is_rgbw) { this->is_rgbw_ = is_rgbw; }
/// Set a maximum refresh rate in µs as some lights do not like being updated too often.
void set_max_refresh_rate(uint32_t interval_us) { this->max_refresh_rate_ = interval_us; }
void set_led_params(uint32_t bit0_high, uint32_t bit0_low, uint32_t bit1_high, uint32_t bit1_low);
void set_rgb_order(RGBOrder rgb_order) { this->rgb_order_ = rgb_order; }
void set_rmt_channel(rmt_channel_t channel) { this->channel_ = channel; }
void clear_effect_data() override {
for (int i = 0; i < this->size(); i++)
this->effect_data_[i] = 0;
}
void dump_config() override;
protected:
light::ESPColorView get_view_internal(int32_t index) const override;
size_t get_buffer_size_() const { return this->num_leds_ * (3 + this->is_rgbw_); }
uint8_t *buf_{nullptr};
uint8_t *effect_data_{nullptr};
rmt_item32_t *rmt_buf_{nullptr};
uint8_t pin_;
uint16_t num_leds_;
bool is_rgbw_;
rmt_item32_t bit0_, bit1_;
RGBOrder rgb_order_;
rmt_channel_t channel_;
uint32_t last_refresh_{0};
optional<uint32_t> max_refresh_rate_{};
};
} // namespace esp32_rmt_led_strip
} // namespace esphome
#endif // USE_ESP32

View File

@ -0,0 +1,151 @@
from dataclasses import dataclass
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import pins
from esphome.components import esp32, light
from esphome.const import (
CONF_CHIPSET,
CONF_MAX_REFRESH_RATE,
CONF_NUM_LEDS,
CONF_OUTPUT_ID,
CONF_PIN,
CONF_RGB_ORDER,
)
CODEOWNERS = ["@jesserockz"]
DEPENDENCIES = ["esp32"]
esp32_rmt_led_strip_ns = cg.esphome_ns.namespace("esp32_rmt_led_strip")
ESP32RMTLEDStripLightOutput = esp32_rmt_led_strip_ns.class_(
"ESP32RMTLEDStripLightOutput", light.AddressableLight
)
rmt_channel_t = cg.global_ns.enum("rmt_channel_t")
RGBOrder = esp32_rmt_led_strip_ns.enum("RGBOrder")
RGB_ORDERS = {
"RGB": RGBOrder.ORDER_RGB,
"RBG": RGBOrder.ORDER_RBG,
"GRB": RGBOrder.ORDER_GRB,
"GBR": RGBOrder.ORDER_GBR,
"BGR": RGBOrder.ORDER_BGR,
"BRG": RGBOrder.ORDER_BRG,
}
@dataclass
class LEDStripTimings:
bit0_high: int
bit0_low: int
bit1_high: int
bit1_low: int
CHIPSETS = {
"WS2812": LEDStripTimings(400, 1000, 1000, 400),
"SK6812": LEDStripTimings(300, 900, 600, 600),
"APA106": LEDStripTimings(350, 1360, 1360, 350),
"SM16703": LEDStripTimings(300, 900, 1360, 350),
}
CONF_IS_RGBW = "is_rgbw"
CONF_BIT0_HIGH = "bit0_high"
CONF_BIT0_LOW = "bit0_low"
CONF_BIT1_HIGH = "bit1_high"
CONF_BIT1_LOW = "bit1_low"
CONF_RMT_CHANNEL = "rmt_channel"
RMT_CHANNELS = {
esp32.const.VARIANT_ESP32: [0, 1, 2, 3, 4, 5, 6, 7],
esp32.const.VARIANT_ESP32S2: [0, 1, 2, 3],
esp32.const.VARIANT_ESP32S3: [0, 1, 2, 3],
esp32.const.VARIANT_ESP32C3: [0, 1],
}
def _validate_rmt_channel(value):
variant = esp32.get_esp32_variant()
if variant not in RMT_CHANNELS:
raise cv.Invalid(f"ESP32 variant {variant} does not support RMT.")
if value not in RMT_CHANNELS[variant]:
raise cv.Invalid(
f"RMT channel {value} is not supported for ESP32 variant {variant}."
)
return value
CONFIG_SCHEMA = cv.All(
light.ADDRESSABLE_LIGHT_SCHEMA.extend(
{
cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(ESP32RMTLEDStripLightOutput),
cv.Required(CONF_PIN): pins.internal_gpio_output_pin_number,
cv.Required(CONF_NUM_LEDS): cv.positive_not_null_int,
cv.Required(CONF_RGB_ORDER): cv.enum(RGB_ORDERS, upper=True),
cv.Required(CONF_RMT_CHANNEL): _validate_rmt_channel,
cv.Optional(CONF_MAX_REFRESH_RATE): cv.positive_time_period_microseconds,
cv.Optional(CONF_CHIPSET): cv.one_of(*CHIPSETS, upper=True),
cv.Optional(CONF_IS_RGBW, default=False): cv.boolean,
cv.Inclusive(
CONF_BIT0_HIGH,
"custom",
): cv.positive_time_period_microseconds,
cv.Inclusive(
CONF_BIT0_LOW,
"custom",
): cv.positive_time_period_microseconds,
cv.Inclusive(
CONF_BIT1_HIGH,
"custom",
): cv.positive_time_period_microseconds,
cv.Inclusive(
CONF_BIT1_LOW,
"custom",
): cv.positive_time_period_microseconds,
}
),
cv.has_exactly_one_key(CONF_CHIPSET, CONF_BIT0_HIGH),
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_OUTPUT_ID])
await light.register_light(var, config)
await cg.register_component(var, config)
cg.add(var.set_num_leds(config[CONF_NUM_LEDS]))
cg.add(var.set_pin(config[CONF_PIN]))
if CONF_MAX_REFRESH_RATE in config:
cg.add(var.set_max_refresh_rate(config[CONF_MAX_REFRESH_RATE]))
if CONF_CHIPSET in config:
chipset = CHIPSETS[config[CONF_CHIPSET]]
cg.add(
var.set_led_params(
chipset.bit0_high,
chipset.bit0_low,
chipset.bit1_high,
chipset.bit1_low,
)
)
else:
cg.add(
var.set_led_params(
config[CONF_BIT0_HIGH],
config[CONF_BIT0_LOW],
config[CONF_BIT1_HIGH],
config[CONF_BIT1_LOW],
)
)
cg.add(var.set_rgb_order(config[CONF_RGB_ORDER]))
cg.add(var.set_is_rgbw(config[CONF_IS_RGBW]))
cg.add(
var.set_rmt_channel(
getattr(rmt_channel_t, f"RMT_CHANNEL_{config[CONF_RMT_CHANNEL]}")
)
)

View File

@ -106,20 +106,18 @@ void EZOSensor::loop() {
break; 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 if (buf[0] == 1) {
// 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;
}
}
std::string payload = reinterpret_cast<char *>(&buf[1]); std::string payload = reinterpret_cast<char *>(&buf[1]);
if (!payload.empty()) { if (!payload.empty()) {
switch (to_run->command_type) { switch (to_run->command_type) {
case EzoCommandType::EZO_READ: { case EzoCommandType::EZO_READ: {
// some sensors return multiple comma-separated values, terminate string after first one
int start_location = 0;
if ((start_location = payload.find(',')) != std::string::npos) {
payload.erase(start_location);
}
auto val = parse_number<float>(payload); auto val = parse_number<float>(payload);
if (!val.has_value()) { if (!val.has_value()) {
ESP_LOGW(TAG, "Can't convert '%s' to number!", payload.c_str()); ESP_LOGW(TAG, "Can't convert '%s' to number!", payload.c_str());
@ -154,7 +152,10 @@ void EZOSensor::loop() {
break; break;
} }
case EzoCommandType::EZO_T: { 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; break;
} }
case EzoCommandType::EZO_CUSTOM: { case EzoCommandType::EZO_CUSTOM: {

View File

@ -41,6 +41,7 @@ void FeedbackCover::setup() {
CoverTraits FeedbackCover::get_traits() { CoverTraits FeedbackCover::get_traits() {
auto traits = CoverTraits(); auto traits = CoverTraits();
traits.set_supports_stop(true);
traits.set_supports_position(true); traits.set_supports_position(true);
traits.set_supports_toggle(true); traits.set_supports_toggle(true);
traits.set_is_assumed_state(this->assumed_state_); traits.set_is_assumed_state(this->assumed_state_);

View File

@ -0,0 +1,40 @@
import esphome.config_validation as cv
import esphome.codegen as cg
from esphome.components import i2c
from esphome.const import CONF_ID, CONF_VOLTAGE
CODEOWNERS = ["@jesserockz"]
DEPENDENCIES = ["i2c"]
MULTI_CONF = True
gp8403_ns = cg.esphome_ns.namespace("gp8403")
GP8403 = gp8403_ns.class_("GP8403", cg.Component, i2c.I2CDevice)
GP8403Voltage = gp8403_ns.enum("GP8403Voltage")
CONF_GP8403_ID = "gp8403_id"
VOLTAGES = {
"5V": GP8403Voltage.GP8403_VOLTAGE_5V,
"10V": GP8403Voltage.GP8403_VOLTAGE_10V,
}
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(GP8403),
cv.Required(CONF_VOLTAGE): cv.enum(VOLTAGES, upper=True),
}
)
.extend(cv.COMPONENT_SCHEMA)
.extend(i2c.i2c_device_schema(0x58))
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
cg.add(var.set_voltage(config[CONF_VOLTAGE]))

View File

@ -0,0 +1,21 @@
#include "gp8403.h"
#include "esphome/core/log.h"
namespace esphome {
namespace gp8403 {
static const char *const TAG = "gp8403";
static const uint8_t RANGE_REGISTER = 0x01;
void GP8403::setup() { this->write_register(RANGE_REGISTER, (uint8_t *) (&this->voltage_), 1); }
void GP8403::dump_config() {
ESP_LOGCONFIG(TAG, "GP8403:");
ESP_LOGCONFIG(TAG, " Voltage: %dV", this->voltage_ == GP8403_VOLTAGE_5V ? 5 : 10);
LOG_I2C_DEVICE(this);
}
} // namespace gp8403
} // namespace esphome

View File

@ -0,0 +1,27 @@
#pragma once
#include "esphome/components/i2c/i2c.h"
#include "esphome/core/component.h"
namespace esphome {
namespace gp8403 {
enum GP8403Voltage {
GP8403_VOLTAGE_5V = 0x00,
GP8403_VOLTAGE_10V = 0x11,
};
class GP8403 : public Component, public i2c::I2CDevice {
public:
void setup() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
void set_voltage(gp8403::GP8403Voltage voltage) { this->voltage_ = voltage; }
protected:
GP8403Voltage voltage_;
};
} // namespace gp8403
} // namespace esphome

View File

@ -0,0 +1,31 @@
import esphome.config_validation as cv
import esphome.codegen as cg
from esphome.components import i2c, output
from esphome.const import CONF_ID, CONF_CHANNEL
from .. import gp8403_ns, GP8403, CONF_GP8403_ID
DEPENDENCIES = ["gp8403"]
GP8403Output = gp8403_ns.class_(
"GP8403Output", cg.Component, i2c.I2CDevice, output.FloatOutput
)
CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(GP8403Output),
cv.GenerateID(CONF_GP8403_ID): cv.use_id(GP8403),
cv.Required(CONF_CHANNEL): cv.one_of(0, 1),
}
).extend(cv.COMPONENT_SCHEMA)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await output.register_output(var, config)
await cg.register_parented(var, config[CONF_GP8403_ID])
cg.add(var.set_channel(config[CONF_CHANNEL]))

View File

@ -0,0 +1,26 @@
#include "gp8403_output.h"
#include "esphome/core/log.h"
namespace esphome {
namespace gp8403 {
static const char *const TAG = "gp8403.output";
static const uint8_t OUTPUT_REGISTER = 0x02;
void GP8403Output::dump_config() {
ESP_LOGCONFIG(TAG, "GP8403 Output:");
ESP_LOGCONFIG(TAG, " Channel: %u", this->channel_);
}
void GP8403Output::write_state(float state) {
uint16_t value = ((uint16_t) (state * 4095)) << 4;
i2c::ErrorCode err = this->parent_->write_register(OUTPUT_REGISTER + (2 * this->channel_), (uint8_t *) &value, 2);
if (err != i2c::ERROR_OK) {
ESP_LOGE(TAG, "Error writing to GP8403, code %d", err);
}
}
} // namespace gp8403
} // namespace esphome

View File

@ -0,0 +1,25 @@
#pragma once
#include "esphome/components/output/float_output.h"
#include "esphome/core/component.h"
#include "esphome/components/gp8403/gp8403.h"
namespace esphome {
namespace gp8403 {
class GP8403Output : public Component, public output::FloatOutput, public Parented<GP8403> {
public:
void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA - 1; }
void set_channel(uint8_t channel) { this->channel_ = channel; }
void write_state(float state) override;
protected:
uint8_t channel_;
};
} // namespace gp8403
} // namespace esphome

View File

@ -0,0 +1,38 @@
from esphome.const import (
KEY_CORE,
KEY_FRAMEWORK_VERSION,
KEY_TARGET_FRAMEWORK,
KEY_TARGET_PLATFORM,
)
from esphome.core import CORE
import esphome.config_validation as cv
import esphome.codegen as cg
from .const import KEY_HOST
# force import gpio to register pin schema
from .gpio import host_pin_to_code # noqa
CODEOWNERS = ["@esphome/core"]
AUTO_LOAD = ["network"]
def set_core_data(config):
CORE.data[KEY_HOST] = {}
CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = "host"
CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = "host"
CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version(1, 0, 0)
return config
CONFIG_SCHEMA = cv.All(
cv.Schema({}),
set_core_data,
)
async def to_code(config):
cg.add_build_flag("-DUSE_HOST")
cg.add_define("ESPHOME_BOARD", "host")
cg.add_platformio_option("platform", "platformio/native")

View File

@ -0,0 +1,5 @@
import esphome.codegen as cg
KEY_HOST = "host"
host_ns = cg.esphome_ns.namespace("host")

View File

@ -0,0 +1,77 @@
#ifdef USE_HOST
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "preferences.h"
#include <sched.h>
#include <time.h>
#include <cmath>
#include <cstdlib>
namespace esphome {
void IRAM_ATTR HOT yield() { ::sched_yield(); }
uint32_t IRAM_ATTR HOT millis() {
struct timespec spec;
clock_gettime(CLOCK_MONOTONIC, &spec);
time_t seconds = spec.tv_sec;
uint32_t ms = round(spec.tv_nsec / 1e6);
return ((uint32_t) seconds) * 1000U + ms;
}
void IRAM_ATTR HOT delay(uint32_t ms) {
struct timespec ts;
ts.tv_sec = ms / 1000;
ts.tv_nsec = (ms % 1000) * 1000000;
int res;
do {
res = nanosleep(&ts, &ts);
} while (res != 0 && errno == EINTR);
}
uint32_t IRAM_ATTR HOT micros() {
struct timespec spec;
clock_gettime(CLOCK_MONOTONIC, &spec);
time_t seconds = spec.tv_sec;
uint32_t us = round(spec.tv_nsec / 1e3);
return ((uint32_t) seconds) * 1000000U + us;
}
void IRAM_ATTR HOT delayMicroseconds(uint32_t us) {
struct timespec ts;
ts.tv_sec = us / 1000000U;
ts.tv_nsec = (us % 1000000U) * 1000U;
int res;
do {
res = nanosleep(&ts, &ts);
} while (res != 0 && errno == EINTR);
}
void arch_restart() { exit(0); }
void arch_init() {
// pass
}
void IRAM_ATTR HOT arch_feed_wdt() {
// pass
}
uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; }
uint32_t arch_get_cpu_cycle_count() {
struct timespec spec;
clock_gettime(CLOCK_MONOTONIC, &spec);
time_t seconds = spec.tv_sec;
uint32_t us = spec.tv_nsec;
return ((uint32_t) seconds) * 1000000000U + us;
}
uint32_t arch_get_cpu_freq_hz() { return 1000000000U; }
} // namespace esphome
void setup();
void loop();
int main() {
esphome::host::setup_preferences();
setup();
while (true) {
loop();
}
}
#endif // USE_HOST

View File

@ -0,0 +1,59 @@
#ifdef USE_HOST
#include "gpio.h"
#include "esphome/core/log.h"
namespace esphome {
namespace host {
static const char *const TAG = "host";
struct ISRPinArg {
uint8_t pin;
bool inverted;
};
ISRInternalGPIOPin HostGPIOPin::to_isr() const {
auto *arg = new ISRPinArg{}; // NOLINT(cppcoreguidelines-owning-memory)
arg->pin = pin_;
arg->inverted = inverted_;
return ISRInternalGPIOPin((void *) arg);
}
void HostGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const {
ESP_LOGD(TAG, "Attaching interrupt %p to pin %d and mode %d", func, pin_, (uint32_t) type);
}
void HostGPIOPin::pin_mode(gpio::Flags flags) { ESP_LOGD(TAG, "Setting pin %d mode to %02X", pin_, (uint32_t) flags); }
std::string HostGPIOPin::dump_summary() const {
char buffer[32];
snprintf(buffer, sizeof(buffer), "GPIO%u", pin_);
return buffer;
}
bool HostGPIOPin::digital_read() { return inverted_; }
void HostGPIOPin::digital_write(bool value) {
// pass
ESP_LOGD(TAG, "Setting pin %d to %s", pin_, value != inverted_ ? "HIGH" : "LOW");
}
void HostGPIOPin::detach_interrupt() const {}
} // namespace host
using namespace host;
bool IRAM_ATTR ISRInternalGPIOPin::digital_read() {
auto *arg = reinterpret_cast<ISRPinArg *>(arg_);
return arg->inverted;
}
void IRAM_ATTR ISRInternalGPIOPin::digital_write(bool value) {
// pass
}
void IRAM_ATTR ISRInternalGPIOPin::clear_interrupt() {
auto *arg = reinterpret_cast<ISRPinArg *>(arg_);
ESP_LOGD(TAG, "Clearing interrupt for pin %d", arg->pin);
}
} // namespace esphome
#endif // USE_HOST

View File

@ -0,0 +1,37 @@
#pragma once
#ifdef USE_HOST
#include "esphome/core/hal.h"
namespace esphome {
namespace host {
class HostGPIOPin : public InternalGPIOPin {
public:
void set_pin(uint8_t pin) { pin_ = pin; }
void set_inverted(bool inverted) { inverted_ = inverted; }
void set_flags(gpio::Flags flags) { flags_ = flags; }
void setup() override { pin_mode(flags_); }
void pin_mode(gpio::Flags flags) override;
bool digital_read() override;
void digital_write(bool value) override;
std::string dump_summary() const override;
void detach_interrupt() const override;
ISRInternalGPIOPin to_isr() const override;
uint8_t get_pin() const override { return pin_; }
bool is_inverted() const override { return inverted_; }
protected:
void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override;
uint8_t pin_;
bool inverted_;
gpio::Flags flags_;
};
} // namespace host
} // namespace esphome
#endif // USE_HOST

View File

@ -0,0 +1,73 @@
import logging
from esphome.const import (
CONF_ID,
CONF_INPUT,
CONF_INVERTED,
CONF_MODE,
CONF_NUMBER,
CONF_OPEN_DRAIN,
CONF_OUTPUT,
CONF_PULLDOWN,
CONF_PULLUP,
)
from esphome import pins
import esphome.config_validation as cv
import esphome.codegen as cg
from .const import host_ns
_LOGGER = logging.getLogger(__name__)
HostGPIOPin = host_ns.class_("HostGPIOPin", cg.InternalGPIOPin)
def _translate_pin(value):
if isinstance(value, dict) or value is None:
raise cv.Invalid(
"This variable only supports pin numbers, not full pin schemas "
"(with inverted and mode)."
)
if isinstance(value, int):
return value
try:
return int(value)
except ValueError:
pass
if value.startswith("GPIO"):
return cv.int_(value[len("GPIO") :].strip())
return value
def validate_gpio_pin(value):
return _translate_pin(value)
HOST_PIN_SCHEMA = cv.All(
{
cv.GenerateID(): cv.declare_id(HostGPIOPin),
cv.Required(CONF_NUMBER): validate_gpio_pin,
cv.Optional(CONF_MODE, default={}): cv.Schema(
{
cv.Optional(CONF_INPUT, default=False): cv.boolean,
cv.Optional(CONF_OUTPUT, default=False): cv.boolean,
cv.Optional(CONF_OPEN_DRAIN, default=False): cv.boolean,
cv.Optional(CONF_PULLUP, default=False): cv.boolean,
cv.Optional(CONF_PULLDOWN, default=False): cv.boolean,
}
),
cv.Optional(CONF_INVERTED, default=False): cv.boolean,
},
)
@pins.PIN_SCHEMA_REGISTRY.register("host", HOST_PIN_SCHEMA)
async def host_pin_to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
num = config[CONF_NUMBER]
cg.add(var.set_pin(num))
cg.add(var.set_inverted(config[CONF_INVERTED]))
cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE])))
return var

View File

@ -0,0 +1,36 @@
#ifdef USE_HOST
#include "preferences.h"
#include <cstring>
#include "esphome/core/preferences.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/defines.h"
namespace esphome {
namespace host {
static const char *const TAG = "host.preferences";
class HostPreferences : public ESPPreferences {
public:
ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash) override { return {}; }
ESPPreferenceObject make_preference(size_t length, uint32_t type) override { return {}; }
bool sync() override { return true; }
bool reset() override { return true; }
};
void setup_preferences() {
auto *pref = new HostPreferences(); // NOLINT(cppcoreguidelines-owning-memory)
global_preferences = pref;
}
} // namespace host
ESPPreferences *global_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
} // namespace esphome
#endif // USE_HOST

View File

@ -0,0 +1,13 @@
#pragma once
#ifdef USE_HOST
namespace esphome {
namespace host {
void setup_preferences();
} // namespace host
} // namespace esphome
#endif // USE_HOST

View File

@ -0,0 +1 @@
CODEOWNERS = ["@Philippe12"]

View File

@ -0,0 +1,52 @@
#include "hyt271.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
namespace esphome {
namespace hyt271 {
static const char *const TAG = "hyt271";
static const uint8_t HYT271_ADDRESS = 0x28;
void HYT271Component::dump_config() {
ESP_LOGCONFIG(TAG, "HYT271:");
LOG_I2C_DEVICE(this);
LOG_UPDATE_INTERVAL(this);
LOG_SENSOR(" ", "Temperature", this->temperature_);
LOG_SENSOR(" ", "Humidity", this->humidity_);
}
void HYT271Component::update() {
uint8_t raw_data[4];
if (this->write(&raw_data[0], 0) != i2c::ERROR_OK) {
this->status_set_warning();
ESP_LOGE(TAG, "Communication with HYT271 failed! => Ask new values");
return;
}
this->set_timeout("wait_convert", 50, [this]() {
uint8_t raw_data[4];
if (this->read(raw_data, 4) != i2c::ERROR_OK) {
this->status_set_warning();
ESP_LOGE(TAG, "Communication with HYT271 failed! => Read values");
return;
}
uint16_t raw_temperature = ((raw_data[2] << 8) | raw_data[3]) >> 2;
uint16_t raw_humidity = ((raw_data[0] & 0x3F) << 8) | raw_data[1];
float temperature = ((float(raw_temperature)) * (165.0f / 16383.0f)) - 40.0f;
float humidity = (float(raw_humidity)) * (100.0f / 16383.0f);
ESP_LOGD(TAG, "Got Temperature=%.1f°C Humidity=%.1f%%", temperature, humidity);
if (this->temperature_ != nullptr)
this->temperature_->publish_state(temperature);
if (this->humidity_ != nullptr)
this->humidity_->publish_state(humidity);
this->status_clear_warning();
});
}
float HYT271Component::get_setup_priority() const { return setup_priority::DATA; }
} // namespace hyt271
} // namespace esphome

View File

@ -0,0 +1,27 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/i2c/i2c.h"
namespace esphome {
namespace hyt271 {
class HYT271Component : public PollingComponent, public i2c::I2CDevice {
public:
void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; }
void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; }
void dump_config() override;
/// Update the sensor values (temperature+humidity).
void update() override;
float get_setup_priority() const override;
protected:
sensor::Sensor *temperature_{nullptr};
sensor::Sensor *humidity_{nullptr};
};
} // namespace hyt271
} // namespace esphome

View File

@ -0,0 +1,56 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import i2c, sensor
from esphome.const import (
CONF_HUMIDITY,
CONF_ID,
CONF_TEMPERATURE,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_TEMPERATURE,
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
UNIT_PERCENT,
)
DEPENDENCIES = ["i2c"]
hyt271_ns = cg.esphome_ns.namespace("hyt271")
HYT271Component = hyt271_ns.class_(
"HYT271Component", cg.PollingComponent, i2c.I2CDevice
)
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(HYT271Component),
cv.Required(CONF_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=1,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Required(CONF_HUMIDITY): sensor.sensor_schema(
unit_of_measurement=UNIT_PERCENT,
accuracy_decimals=1,
device_class=DEVICE_CLASS_HUMIDITY,
state_class=STATE_CLASS_MEASUREMENT,
),
}
)
.extend(cv.polling_component_schema("60s"))
.extend(i2c.i2c_device_schema(0x28))
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
if CONF_TEMPERATURE in config:
sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
cg.add(var.set_temperature(sens))
if CONF_HUMIDITY in config:
sens = await sensor.new_sensor(config[CONF_HUMIDITY])
cg.add(var.set_humidity(sens))

View File

@ -42,8 +42,8 @@ I2S_PORTS = {
CONFIG_SCHEMA = cv.Schema( CONFIG_SCHEMA = cv.Schema(
{ {
cv.GenerateID(): cv.declare_id(I2SAudioComponent), 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.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]) var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config) 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])) 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]))

View File

@ -19,15 +19,6 @@ class I2SAudioComponent : public Component {
public: public:
void setup() override; 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 { i2s_pin_config_t get_pin_config() const {
return { return {
.mck_io_num = I2S_PIN_NO_CHANGE, .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_bclk_pin(int pin) { this->bclk_pin_ = pin; }
void set_lrclk_pin(uint8_t pin) { this->lrclk_pin_ = pin; } void set_lrclk_pin(int pin) { this->lrclk_pin_ = pin; }
void lock() { this->lock_.lock(); } void lock() { this->lock_.lock(); }
bool try_lock() { return this->lock_.try_lock(); } bool try_lock() { return this->lock_.try_lock(); }
@ -53,8 +44,8 @@ class I2SAudioComponent : public Component {
I2SAudioIn *audio_in_{nullptr}; I2SAudioIn *audio_in_{nullptr};
I2SAudioOut *audio_out_{nullptr}; I2SAudioOut *audio_out_{nullptr};
uint8_t bclk_pin_; int bclk_pin_{I2S_PIN_NO_CHANGE};
uint8_t lrclk_pin_; int lrclk_pin_;
i2s_port_t port_{}; i2s_port_t port_{};
}; };

View File

@ -84,8 +84,7 @@ async def to_code(config):
await cg.register_component(var, config) await cg.register_component(var, config)
await media_player.register_media_player(var, config) await media_player.register_media_player(var, config)
parent = await cg.get_variable(config[CONF_I2S_AUDIO_ID]) await cg.register_parented(var, config[CONF_I2S_AUDIO_ID])
cg.add(parent.register_audio_out(var))
if config[CONF_DAC_TYPE] == "internal": if config[CONF_DAC_TYPE] == "internal":
cg.add(var.set_internal_dac_mode(config[CONF_MODE])) 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("WiFiClientSecure", None)
cg.add_library("HTTPClient", 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") cg.add_build_flag("-DAUDIO_NO_SD_FS")

View File

@ -2,8 +2,9 @@ import esphome.config_validation as cv
import esphome.codegen as cg import esphome.codegen as cg
from esphome import pins from esphome import pins
from esphome.const import CONF_ID from esphome.const import CONF_ID, CONF_NUMBER
from esphome.components import microphone from esphome.components import microphone, esp32
from esphome.components.adc import ESP32_VARIANT_ADC1_PIN_TO_CHANNEL, validate_adc_pin
from .. import ( from .. import (
i2s_audio_ns, i2s_audio_ns,
@ -16,26 +17,73 @@ from .. import (
CODEOWNERS = ["@jesserockz"] CODEOWNERS = ["@jesserockz"]
DEPENDENCIES = ["i2s_audio"] DEPENDENCIES = ["i2s_audio"]
CONF_ADC_PIN = "adc_pin"
CONF_ADC_TYPE = "adc_type"
CONF_PDM = "pdm"
I2SAudioMicrophone = i2s_audio_ns.class_( I2SAudioMicrophone = i2s_audio_ns.class_(
"I2SAudioMicrophone", I2SAudioIn, microphone.Microphone, cg.Component "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(): cv.declare_id(I2SAudioMicrophone),
cv.GenerateID(CONF_I2S_AUDIO_ID): cv.use_id(I2SAudioComponent), 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) ).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): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config) await cg.register_component(var, config)
parent = await cg.get_variable(config[CONF_I2S_AUDIO_ID]) await cg.register_parented(var, config[CONF_I2S_AUDIO_ID])
cg.add(parent.register_audio_in(var))
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_din_pin(config[CONF_I2S_DIN_PIN]))
cg.add(var.set_pdm(config[CONF_PDM]))
await microphone.register_microphone(var, config) await microphone.register_microphone(var, config)

View File

@ -17,15 +17,36 @@ static const char *const TAG = "i2s_audio.microphone";
void I2SAudioMicrophone::setup() { void I2SAudioMicrophone::setup() {
ESP_LOGCONFIG(TAG, "Setting up I2S Audio Microphone..."); ESP_LOGCONFIG(TAG, "Setting up I2S Audio Microphone...");
this->buffer_.resize(BUFFER_SIZE); 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_() { void I2SAudioMicrophone::start_() {
if (!this->parent_->try_lock()) { if (!this->parent_->try_lock()) {
return; // Waiting for another i2s to return lock return; // Waiting for another i2s to return lock
} }
i2s_driver_config_t config = { 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, .sample_rate = 16000,
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
.channel_format = I2S_CHANNEL_FMT_ONLY_RIGHT, .channel_format = I2S_CHANNEL_FMT_ONLY_RIGHT,
@ -40,18 +61,33 @@ void I2SAudioMicrophone::start_() {
.bits_per_chan = I2S_BITS_PER_CHAN_DEFAULT, .bits_per_chan = I2S_BITS_PER_CHAN_DEFAULT,
}; };
#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_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_driver_install(this->parent_->get_port(), &config, 0, nullptr); i2s_driver_install(this->parent_->get_port(), &config, 0, nullptr);
i2s_pin_config_t pin_config = this->parent_->get_pin_config(); i2s_pin_config_t pin_config = this->parent_->get_pin_config();
pin_config.data_in_num = this->din_pin_; pin_config.data_in_num = this->din_pin_;
i2s_set_pin(this->parent_->get_port(), &pin_config); i2s_set_pin(this->parent_->get_port(), &pin_config);
#if SOC_I2S_SUPPORTS_ADC
}
#endif
this->state_ = microphone::STATE_RUNNING; this->state_ = microphone::STATE_RUNNING;
this->high_freq_.start(); this->high_freq_.start();
} }
void I2SAudioMicrophone::stop() { void I2SAudioMicrophone::stop() {
if (this->state_ == microphone::STATE_STOPPED) if (this->state_ == microphone::STATE_STOPPED || this->is_failed())
return; return;
this->state_ = microphone::STATE_STOPPING; this->state_ = microphone::STATE_STOPPING;
} }

View File

@ -18,14 +18,27 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub
void loop() override; 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: protected:
void start_(); void start_();
void stop_(); void stop_();
void read_(); void read_();
uint8_t din_pin_{0}; int8_t din_pin_{I2S_PIN_NO_CHANGE};
#if SOC_I2S_SUPPORTS_ADC
adc1_channel_t adc_channel_{ADC1_CHANNEL_MAX};
bool adc_{false};
#endif
bool pdm_{false};
std::vector<uint8_t> buffer_; std::vector<uint8_t> buffer_;
HighFrequencyLoopRequester high_freq_; HighFrequencyLoopRequester high_freq_;

View File

@ -0,0 +1,87 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import pins
from esphome.const import CONF_ID, CONF_MODE
from esphome.components import esp32, speaker
from .. import (
CONF_I2S_AUDIO_ID,
CONF_I2S_DOUT_PIN,
I2SAudioComponent,
I2SAudioOut,
i2s_audio_ns,
)
CODEOWNERS = ["@jesserockz"]
DEPENDENCIES = ["i2s_audio"]
I2SAudioSpeaker = i2s_audio_ns.class_(
"I2SAudioSpeaker", cg.Component, speaker.Speaker, I2SAudioOut
)
i2s_dac_mode_t = cg.global_ns.enum("i2s_dac_mode_t")
CONF_MUTE_PIN = "mute_pin"
CONF_DAC_TYPE = "dac_type"
INTERNAL_DAC_OPTIONS = {
"left": i2s_dac_mode_t.I2S_DAC_CHANNEL_LEFT_EN,
"right": i2s_dac_mode_t.I2S_DAC_CHANNEL_RIGHT_EN,
"stereo": i2s_dac_mode_t.I2S_DAC_CHANNEL_BOTH_EN,
}
EXTERNAL_DAC_OPTIONS = ["mono", "stereo"]
NO_INTERNAL_DAC_VARIANTS = [esp32.const.VARIANT_ESP32S2]
def validate_esp32_variant(config):
if config[CONF_DAC_TYPE] != "internal":
return config
variant = esp32.get_esp32_variant()
if variant in NO_INTERNAL_DAC_VARIANTS:
raise cv.Invalid(f"{variant} does not have an internal DAC")
return config
CONFIG_SCHEMA = cv.All(
cv.typed_schema(
{
"internal": speaker.SPEAKER_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(I2SAudioSpeaker),
cv.GenerateID(CONF_I2S_AUDIO_ID): cv.use_id(I2SAudioComponent),
cv.Required(CONF_MODE): cv.enum(INTERNAL_DAC_OPTIONS, lower=True),
}
).extend(cv.COMPONENT_SCHEMA),
"external": speaker.SPEAKER_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(I2SAudioSpeaker),
cv.GenerateID(CONF_I2S_AUDIO_ID): cv.use_id(I2SAudioComponent),
cv.Required(
CONF_I2S_DOUT_PIN
): pins.internal_gpio_output_pin_number,
cv.Optional(CONF_MODE, default="mono"): cv.one_of(
*EXTERNAL_DAC_OPTIONS, lower=True
),
}
).extend(cv.COMPONENT_SCHEMA),
},
key=CONF_DAC_TYPE,
),
validate_esp32_variant,
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await speaker.register_speaker(var, config)
await cg.register_parented(var, config[CONF_I2S_AUDIO_ID])
if config[CONF_DAC_TYPE] == "internal":
cg.add(var.set_internal_dac_mode(config[CONF_MODE]))
else:
cg.add(var.set_dout_pin(config[CONF_I2S_DOUT_PIN]))
cg.add(var.set_external_dac_channels(2 if config[CONF_MODE] == "stereo" else 1))

View File

@ -0,0 +1,208 @@
#include "i2s_audio_speaker.h"
#ifdef USE_ESP32
#include <driver/i2s.h>
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
namespace esphome {
namespace i2s_audio {
static const size_t BUFFER_COUNT = 10;
static const char *const TAG = "i2s_audio.speaker";
void I2SAudioSpeaker::setup() {
ESP_LOGCONFIG(TAG, "Setting up I2S Audio Speaker...");
this->buffer_queue_ = xQueueCreate(BUFFER_COUNT, sizeof(DataEvent));
this->event_queue_ = xQueueCreate(20, sizeof(TaskEvent));
}
void I2SAudioSpeaker::start() { this->state_ = speaker::STATE_STARTING; }
void I2SAudioSpeaker::start_() {
if (!this->parent_->try_lock()) {
return; // Waiting for another i2s component to return lock
}
this->state_ = speaker::STATE_RUNNING;
xTaskCreate(I2SAudioSpeaker::player_task, "speaker_task", 8192, (void *) this, 0, &this->player_task_handle_);
}
void I2SAudioSpeaker::player_task(void *params) {
I2SAudioSpeaker *this_speaker = (I2SAudioSpeaker *) params;
TaskEvent event;
event.type = TaskEventType::STARTING;
xQueueSend(this_speaker->event_queue_, &event, portMAX_DELAY);
i2s_driver_config_t config = {
.mode = (i2s_mode_t) (I2S_MODE_MASTER | I2S_MODE_TX),
.sample_rate = 16000,
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
.channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT,
.communication_format = I2S_COMM_FORMAT_STAND_I2S,
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
.dma_buf_count = 8,
.dma_buf_len = 1024,
.use_apll = false,
.tx_desc_auto_clear = true,
.fixed_mclk = I2S_PIN_NO_CHANGE,
.mclk_multiple = I2S_MCLK_MULTIPLE_DEFAULT,
.bits_per_chan = I2S_BITS_PER_CHAN_DEFAULT,
};
#if SOC_I2S_SUPPORTS_DAC
if (this_speaker->internal_dac_mode_ != I2S_DAC_CHANNEL_DISABLE) {
config.mode = (i2s_mode_t) (config.mode | I2S_MODE_DAC_BUILT_IN);
}
#endif
i2s_driver_install(this_speaker->parent_->get_port(), &config, 0, nullptr);
#if SOC_I2S_SUPPORTS_DAC
if (this_speaker->internal_dac_mode_ == I2S_DAC_CHANNEL_DISABLE) {
#endif
i2s_pin_config_t pin_config = this_speaker->parent_->get_pin_config();
pin_config.data_out_num = this_speaker->dout_pin_;
i2s_set_pin(this_speaker->parent_->get_port(), &pin_config);
#if SOC_I2S_SUPPORTS_DAC
} else {
i2s_set_dac_mode(this_speaker->internal_dac_mode_);
}
#endif
DataEvent data_event;
event.type = TaskEventType::STARTED;
xQueueSend(this_speaker->event_queue_, &event, portMAX_DELAY);
int16_t buffer[BUFFER_SIZE / 2];
while (true) {
if (xQueueReceive(this_speaker->buffer_queue_, &data_event, 100 / portTICK_PERIOD_MS) != pdTRUE) {
break; // End of audio from main thread
}
if (data_event.stop) {
// Stop signal from main thread
while (xQueueReceive(this_speaker->buffer_queue_, &data_event, 0) == pdTRUE) {
// Flush queue
}
break;
}
size_t bytes_written;
memmove(buffer, data_event.data, data_event.len);
size_t remaining = data_event.len / 2;
size_t current = 0;
while (remaining > 0) {
uint32_t sample = (buffer[current] << 16) | (buffer[current] & 0xFFFF);
esp_err_t err = i2s_write(this_speaker->parent_->get_port(), &sample, sizeof(sample), &bytes_written,
(100 / portTICK_PERIOD_MS));
if (err != ESP_OK) {
event = {.type = TaskEventType::WARNING, .err = err};
xQueueSend(this_speaker->event_queue_, &event, portMAX_DELAY);
continue;
}
remaining--;
current++;
}
event.type = TaskEventType::PLAYING;
xQueueSend(this_speaker->event_queue_, &event, portMAX_DELAY);
}
i2s_zero_dma_buffer(this_speaker->parent_->get_port());
event.type = TaskEventType::STOPPING;
xQueueSend(this_speaker->event_queue_, &event, portMAX_DELAY);
i2s_stop(this_speaker->parent_->get_port());
i2s_driver_uninstall(this_speaker->parent_->get_port());
event.type = TaskEventType::STOPPED;
xQueueSend(this_speaker->event_queue_, &event, portMAX_DELAY);
while (true) {
delay(10);
}
}
void I2SAudioSpeaker::stop() {
if (this->state_ == speaker::STATE_STOPPED)
return;
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

View File

@ -0,0 +1,81 @@
#pragma once
#ifdef USE_ESP32
#include "../i2s_audio.h"
#include <driver/i2s.h>
#include <freertos/FreeRTOS.h>
#include <freertos/queue.h>
#include "esphome/components/speaker/speaker.h"
#include "esphome/core/component.h"
#include "esphome/core/gpio.h"
#include "esphome/core/helpers.h"
namespace esphome {
namespace i2s_audio {
static const size_t BUFFER_SIZE = 1024;
enum class TaskEventType : uint8_t {
STARTING = 0,
STARTED,
PLAYING,
STOPPING,
STOPPED,
WARNING = 255,
};
struct TaskEvent {
TaskEventType type;
esp_err_t err;
};
struct DataEvent {
bool stop;
size_t len;
uint8_t data[BUFFER_SIZE];
};
class I2SAudioSpeaker : public Component, public speaker::Speaker, public I2SAudioOut {
public:
float get_setup_priority() const override { return esphome::setup_priority::LATE; }
void setup() override;
void loop() override;
void set_dout_pin(uint8_t pin) { this->dout_pin_ = pin; }
#if SOC_I2S_SUPPORTS_DAC
void set_internal_dac_mode(i2s_dac_mode_t mode) { this->internal_dac_mode_ = mode; }
#endif
void set_external_dac_channels(uint8_t channels) { this->external_dac_channels_ = channels; }
void start();
void stop() override;
bool play(const uint8_t *data, size_t length) override;
protected:
void start_();
// void stop_();
void watch_();
static void player_task(void *params);
TaskHandle_t player_task_handle_{nullptr};
QueueHandle_t buffer_queue_;
QueueHandle_t event_queue_;
uint8_t dout_pin_{0};
#if SOC_I2S_SUPPORTS_DAC
i2s_dac_mode_t internal_dac_mode_{I2S_DAC_CHANNEL_DISABLE};
#endif
uint8_t external_dac_channels_;
};
} // namespace i2s_audio
} // namespace esphome
#endif // USE_ESP32

View File

@ -84,10 +84,19 @@ void ILI9XXXDisplay::fill(Color color) {
break; break;
case BITS_16: case BITS_16:
new_color = display::ColorUtil::color_to_565(color); new_color = display::ColorUtil::color_to_565(color);
for (uint32_t i = 0; i < this->get_buffer_length_() * 2; i = i + 2) { {
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] = (uint8_t) (new_color >> 8);
this->buffer_[i + 1] = (uint8_t) new_color; this->buffer_[i + 1] = (uint8_t) new_color;
} }
}
}
return; return;
break; break;
default: default:

View File

@ -17,6 +17,9 @@ from esphome.const import (
CONF_TAG, CONF_TAG,
CONF_TRIGGER_ID, CONF_TRIGGER_ID,
CONF_TX_BUFFER_SIZE, CONF_TX_BUFFER_SIZE,
PLATFORM_ESP32,
PLATFORM_ESP8266,
PLATFORM_RP2040,
) )
from esphome.core import CORE, EsphomeError, Lambda, coroutine_with_priority from esphome.core import CORE, EsphomeError, Lambda, coroutine_with_priority
from esphome.components.esp32 import add_idf_sdkconfig_option, get_esp32_variant from esphome.components.esp32 import add_idf_sdkconfig_option, get_esp32_variant
@ -141,7 +144,10 @@ CONFIG_SCHEMA = cv.All(
esp8266=UART0, esp8266=UART0,
esp32=UART0, esp32=UART0,
rp2040=USB_CDC, 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_LEVEL, default="DEBUG"): is_log_level,
cv.Optional(CONF_LOGS, default={}): cv.Schema( cv.Optional(CONF_LOGS, default={}): cv.Schema(
{ {

View File

@ -145,6 +145,9 @@ void HOT Logger::log_message_(int level, const char *tag, int offset) {
if (xPortGetFreeHeapSize() < 2048) if (xPortGetFreeHeapSize() < 2048)
return; return;
#endif #endif
#ifdef USE_HOST
puts(msg);
#endif
this->log_callback_.call(level, tag, msg); 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) { void Logger::set_log_level(const std::string &tag, int log_level) {
this->log_levels_.push_back(LogLevelOverride{tag, 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_; } UARTSelection Logger::get_uart() const { return this->uart_; }
#endif
void Logger::add_on_log_callback(std::function<void(int, const char *, const char *)> &&callback) { void Logger::add_on_log_callback(std::function<void(int, const char *, const char *)> &&callback) {
this->log_callback_.add(std::move(callback)); this->log_callback_.add(std::move(callback));
} }
@ -294,7 +301,10 @@ void Logger::dump_config() {
ESP_LOGCONFIG(TAG, "Logger:"); ESP_LOGCONFIG(TAG, "Logger:");
ESP_LOGCONFIG(TAG, " Level: %s", LOG_LEVELS[ESPHOME_LOG_LEVEL]); ESP_LOGCONFIG(TAG, " Level: %s", LOG_LEVELS[ESPHOME_LOG_LEVEL]);
ESP_LOGCONFIG(TAG, " Log Baud Rate: %" PRIu32, this->baud_rate_); 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_]); ESP_LOGCONFIG(TAG, " Hardware UART: %s", UART_SELECTIONS[this->uart_]);
#endif
for (auto &it : this->log_levels_) { for (auto &it : this->log_levels_) {
ESP_LOGCONFIG(TAG, " Level for '%s': %s", it.tag.c_str(), LOG_LEVELS[it.level]); ESP_LOGCONFIG(TAG, " Level for '%s': %s", it.tag.c_str(), LOG_LEVELS[it.level]);
} }

View File

@ -25,6 +25,7 @@ namespace esphome {
namespace logger { namespace logger {
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040)
/** Enum for logging UART selection /** Enum for logging UART selection
* *
* Advanced configuration (pin selection, etc) is not supported. * Advanced configuration (pin selection, etc) is not supported.
@ -52,6 +53,7 @@ enum UARTSelection {
UART_SELECTION_USB_CDC, UART_SELECTION_USB_CDC,
#endif // USE_RP2040 #endif // USE_RP2040
}; };
#endif // USE_ESP32 || USE_ESP8266
class Logger : public Component { class Logger : public Component {
public: public:
@ -66,10 +68,11 @@ class Logger : public Component {
#ifdef USE_ESP_IDF #ifdef USE_ESP_IDF
uart_port_t get_uart_num() const { return uart_num_; } uart_port_t get_uart_num() const { return uart_num_; }
#endif #endif
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040)
void set_uart_selection(UARTSelection uart_selection) { uart_ = uart_selection; } void set_uart_selection(UARTSelection uart_selection) { uart_ = uart_selection; }
/// Get the UART used by the logger. /// Get the UART used by the logger.
UARTSelection get_uart() const; UARTSelection get_uart() const;
#endif
/// Set the log level of the specified tag. /// Set the log level of the specified tag.
void set_log_level(const std::string &tag, int log_level); void set_log_level(const std::string &tag, int log_level);
@ -139,7 +142,9 @@ class Logger : public Component {
char *tx_buffer_{nullptr}; char *tx_buffer_{nullptr};
int tx_buffer_at_{0}; int tx_buffer_at_{0};
int tx_buffer_size_{0}; int tx_buffer_size_{0};
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040)
UARTSelection uart_{UART_SELECTION_UART0}; UARTSelection uart_{UART_SELECTION_UART0};
#endif
#ifdef USE_ARDUINO #ifdef USE_ARDUINO
Stream *hw_serial_{nullptr}; Stream *hw_serial_{nullptr};
#endif #endif

View File

@ -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

View File

@ -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<typename... Ts> class SetCurrentGlobalAction : public Action<Ts...> {
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<typename... Ts> class SetCurrentModeAction : public Action<Ts...> {
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

View File

@ -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

View File

@ -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 411 (data bits D0D7)
};
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

View File

@ -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))

View File

@ -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

View File

@ -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

View File

@ -4,10 +4,13 @@ from esphome.const import (
CONF_PROTOCOL, CONF_PROTOCOL,
CONF_SERVICES, CONF_SERVICES,
CONF_SERVICE, CONF_SERVICE,
KEY_CORE,
KEY_FRAMEWORK_VERSION,
) )
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.core import CORE, coroutine_with_priority from esphome.core import CORE, coroutine_with_priority
from esphome.components.esp32 import add_idf_component
CODEOWNERS = ["@esphome/core"] CODEOWNERS = ["@esphome/core"]
DEPENDENCIES = ["network"] DEPENDENCIES = ["network"]
@ -79,6 +82,16 @@ async def to_code(config):
elif CORE.is_rp2040: elif CORE.is_rp2040:
cg.add_library("LEAmDNS", None) 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]: if config[CONF_DISABLED]:
return return

View File

@ -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

View File

View File

@ -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

View File

@ -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

Some files were not shown because too many files have changed in this diff Show More