1
0
mirror of https://github.com/esphome/esphome.git synced 2025-03-13 22:28:14 +00:00

Merge branch 'dev' into feature_dallas_pio

This commit is contained in:
Thierry Duvernoy 2025-02-20 20:59:59 +01:00 committed by GitHub
commit dd996c8e98
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
789 changed files with 11765 additions and 11462 deletions

View File

@ -22,7 +22,7 @@ runs:
python-version: ${{ inputs.python-version }}
- name: Restore Python virtual environment
id: cache-venv
uses: actions/cache/restore@v4.2.0
uses: actions/cache/restore@v4.2.1
with:
path: venv
# yamllint disable-line rule:line-length

View File

@ -33,11 +33,11 @@ concurrency:
jobs:
check-docker:
name: Build docker containers
runs-on: ubuntu-latest
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
arch: [amd64, aarch64]
os: ["ubuntu-latest", "ubuntu-24.04-arm"]
build_type: ["ha-addon", "docker", "lint"]
steps:
- uses: actions/checkout@v4.1.7
@ -47,8 +47,6 @@ jobs:
python-version: "3.9"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.9.0
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.4.0
- name: Set TAG
run: |
@ -58,6 +56,6 @@ jobs:
run: |
docker/build.py \
--tag "${TAG}" \
--arch "${{ matrix.arch }}" \
--arch "${{ matrix.os == 'ubuntu-24.04-arm' && 'aarch64' || 'amd64' }}" \
--build-type "${{ matrix.build_type }}" \
build

View File

@ -47,7 +47,7 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore Python virtual environment
id: cache-venv
uses: actions/cache@v4.2.0
uses: actions/cache@v4.2.1
with:
path: venv
# yamllint disable-line rule:line-length
@ -61,8 +61,8 @@ jobs:
pip install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt
pip install -e .
black:
name: Check black
ruff:
name: Check ruff
runs-on: ubuntu-24.04
needs:
- common
@ -74,10 +74,10 @@ jobs:
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Run black
- name: Run Ruff
run: |
. venv/bin/activate
black --verbose esphome tests
ruff format esphome tests
- name: Suggested changes
run: script/ci-suggest-changes
if: always()
@ -255,7 +255,7 @@ jobs:
runs-on: ubuntu-24.04
needs:
- common
- black
- ruff
- ci-custom
- clang-format
- flake8
@ -303,14 +303,14 @@ jobs:
- name: Cache platformio
if: github.ref == 'refs/heads/dev'
uses: actions/cache@v4.2.0
uses: actions/cache@v4.2.1
with:
path: ~/.platformio
key: platformio-${{ matrix.pio_cache_key }}
- name: Cache platformio
if: github.ref != 'refs/heads/dev'
uses: actions/cache/restore@v4.2.0
uses: actions/cache/restore@v4.2.1
with:
path: ~/.platformio
key: platformio-${{ matrix.pio_cache_key }}
@ -482,7 +482,7 @@ jobs:
runs-on: ubuntu-24.04
needs:
- common
- black
- ruff
- ci-custom
- clang-format
- flake8

View File

@ -4,7 +4,7 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.5.4
rev: v0.9.2
hooks:
# Run the linter.
- id: ruff
@ -45,6 +45,6 @@ repos:
hooks:
- id: pylint
name: pylint
entry: python script/run-in-env pylint
entry: python3 script/run-in-env.py pylint
language: system
types: [python]

View File

@ -235,6 +235,7 @@ esphome/components/kuntze/* @ssieb
esphome/components/lcd_menu/* @numo68
esphome/components/ld2410/* @regevbr @sebcaps
esphome/components/ld2420/* @descipher
esphome/components/ld2450/* @hareeshmu
esphome/components/ledc/* @OttoWinter
esphome/components/libretiny/* @kuba2k2
esphome/components/libretiny_pwm/* @kuba2k2
@ -243,6 +244,7 @@ esphome/components/lightwaverf/* @max246
esphome/components/lilygo_t5_47/touchscreen/* @jesserockz
esphome/components/lock/* @esphome/core
esphome/components/logger/* @esphome/core
esphome/components/logger/select/* @clydebarrow
esphome/components/ltr390/* @latonita @sjtrny
esphome/components/ltr501/* @latonita
esphome/components/ltr_als_ps/* @latonita
@ -390,6 +392,7 @@ esphome/components/sn74hc165/* @jesserockz
esphome/components/socket/* @esphome/core
esphome/components/sonoff_d1/* @anatoly-savchenkov
esphome/components/speaker/* @jesserockz @kahrendt
esphome/components/speaker/media_player/* @kahrendt @synesthesiam
esphome/components/spi/* @clydebarrow @esphome/core
esphome/components/spi_device/* @clydebarrow
esphome/components/spi_led_strip/* @clydebarrow

View File

@ -1,12 +1,14 @@
# Contributing to ESPHome
# Contributing to ESPHome [![Discord Chat](https://img.shields.io/discord/429907082951524364.svg)](https://discord.gg/KhAMKrd) [![GitHub release](https://img.shields.io/github/release/esphome/esphome.svg)](https://GitHub.com/esphome/esphome/releases/)
For a detailed guide, please see https://esphome.io/guides/contributing.html#contributing-to-esphome
We welcome contributions to the ESPHome suite of code and documentation!
Things to note when contributing:
Please read our [contributing guide](https://esphome.io/guides/contributing.html) if you wish to contribute to the
project and be sure to join us on [Discord](https://discord.gg/KhAMKrd).
- Please test your changes :)
- If a new feature is added or an existing user-facing feature is changed, you should also
update the [docs](https://github.com/esphome/esphome-docs). See [contributing to esphome-docs](https://esphome.io/guides/contributing.html#contributing-to-esphomedocs)
for more information.
- Please also update the tests in the `tests/` folder. You can do so by just adding a line in one of the YAML files
which checks if your new feature compiles correctly.
**See also:**
[Documentation](https://esphome.io) -- [Issues](https://github.com/esphome/issues/issues) -- [Feature requests](https://github.com/esphome/feature-requests/issues)
---
[![ESPHome - A project from the Open Home Foundation](https://www.openhomefoundation.org/badges/esphome.png)](https://www.openhomefoundation.org/)

View File

@ -7,10 +7,10 @@
</picture>
</a>
**Documentation:** https://esphome.io/
---
For issues, please go to [the issue tracker](https://github.com/esphome/issues/issues).
[Documentation](https://esphome.io) -- [Issues](https://github.com/esphome/issues/issues) -- [Feature requests](https://github.com/esphome/feature-requests/issues)
For feature requests, please see [feature requests](https://github.com/esphome/feature-requests/issues).
---
[![ESPHome - A project from the Open Home Foundation](https://www.openhomefoundation.org/badges/esphome.png)](https://www.openhomefoundation.org/)

View File

@ -35,7 +35,7 @@ RUN \
iputils-ping=3:20221126-1+deb12u1 \
git=1:2.39.5-0+deb12u1 \
curl=7.88.1-10+deb12u8 \
openssh-client=1:9.2p1-2+deb12u3 \
openssh-client=1:9.2p1-2+deb12u4 \
python3-cffi=1.15.1-5 \
libcairo2=1.16.0-7 \
libmagic1=1:5.44-3 \

View File

@ -66,7 +66,7 @@ def choose_prompt(options, purpose: str = None):
return options[0][1]
safe_print(
f'Found multiple options{f" for {purpose}" if purpose else ""}, please choose one:'
f"Found multiple options{f' for {purpose}' if purpose else ''}, please choose one:"
)
for i, (desc, _) in enumerate(options):
safe_print(f" [{i + 1}] {desc}")

View File

@ -36,6 +36,14 @@ ATTENUATION_MODES = {
"auto": "auto",
}
sampling_mode = adc_ns.enum("SamplingMode", is_class=True)
SAMPLING_MODES = {
"avg": sampling_mode.AVG,
"min": sampling_mode.MIN,
"max": sampling_mode.MAX,
}
adc1_channel_t = cg.global_ns.enum("adc1_channel_t")
adc2_channel_t = cg.global_ns.enum("adc2_channel_t")

View File

@ -28,6 +28,21 @@ static const adc_atten_t ADC_ATTEN_DB_12_COMPAT = ADC_ATTEN_DB_11;
#endif
#endif // USE_ESP32
enum class SamplingMode : uint8_t { AVG = 0, MIN = 1, MAX = 2 };
const LogString *sampling_mode_to_str(SamplingMode mode);
class Aggregator {
public:
void add_sample(uint32_t value);
uint32_t aggregate();
Aggregator(SamplingMode mode);
protected:
SamplingMode mode_{SamplingMode::AVG};
uint32_t aggr_{0};
uint32_t samples_{0};
};
class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage_sampler::VoltageSampler {
public:
#ifdef USE_ESP32
@ -54,6 +69,7 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage
void set_pin(InternalGPIOPin *pin) { this->pin_ = pin; }
void set_output_raw(bool output_raw) { this->output_raw_ = output_raw; }
void set_sample_count(uint8_t sample_count);
void set_sampling_mode(SamplingMode sampling_mode);
float sample() override;
#ifdef USE_ESP8266
@ -68,6 +84,7 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage
InternalGPIOPin *pin_;
bool output_raw_{false};
uint8_t sample_count_{1};
SamplingMode sampling_mode_{SamplingMode::AVG};
#ifdef USE_RP2040
bool is_temperature_{false};

View File

@ -6,6 +6,59 @@ namespace adc {
static const char *const TAG = "adc.common";
const LogString *sampling_mode_to_str(SamplingMode mode) {
switch (mode) {
case SamplingMode::AVG:
return LOG_STR("average");
case SamplingMode::MIN:
return LOG_STR("minimum");
case SamplingMode::MAX:
return LOG_STR("maximum");
}
return LOG_STR("unknown");
}
Aggregator::Aggregator(SamplingMode mode) {
this->mode_ = mode;
// set to max uint if mode is "min"
if (mode == SamplingMode::MIN) {
this->aggr_ = UINT32_MAX;
}
}
void Aggregator::add_sample(uint32_t value) {
this->samples_ += 1;
switch (this->mode_) {
case SamplingMode::AVG:
this->aggr_ += value;
break;
case SamplingMode::MIN:
if (value < this->aggr_) {
this->aggr_ = value;
}
break;
case SamplingMode::MAX:
if (value > this->aggr_) {
this->aggr_ = value;
}
}
}
uint32_t Aggregator::aggregate() {
if (this->mode_ == SamplingMode::AVG) {
if (this->samples_ == 0) {
return this->aggr_;
}
return (this->aggr_ + (this->samples_ >> 1)) / this->samples_; // NOLINT(clang-analyzer-core.DivideZero)
}
return this->aggr_;
}
void ADCSensor::update() {
float value_v = this->sample();
ESP_LOGV(TAG, "'%s': Got voltage=%.4fV", this->get_name().c_str(), value_v);
@ -18,6 +71,8 @@ void ADCSensor::set_sample_count(uint8_t sample_count) {
}
}
void ADCSensor::set_sampling_mode(SamplingMode sampling_mode) { this->sampling_mode_ = sampling_mode; }
float ADCSensor::get_setup_priority() const { return setup_priority::DATA; }
} // namespace adc

View File

@ -78,12 +78,14 @@ void ADCSensor::dump_config() {
}
}
ESP_LOGCONFIG(TAG, " Samples: %i", this->sample_count_);
ESP_LOGCONFIG(TAG, " Sampling mode: %s", LOG_STR_ARG(sampling_mode_to_str(this->sampling_mode_)));
LOG_UPDATE_INTERVAL(this);
}
float ADCSensor::sample() {
if (!this->autorange_) {
uint32_t sum = 0;
auto aggr = Aggregator(this->sampling_mode_);
for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
int raw = -1;
if (this->channel1_ != ADC1_CHANNEL_MAX) {
@ -94,13 +96,14 @@ float ADCSensor::sample() {
if (raw == -1) {
return NAN;
}
sum += raw;
aggr.add_sample(raw);
}
sum = (sum + (this->sample_count_ >> 1)) / this->sample_count_; // NOLINT(clang-analyzer-core.DivideZero)
if (this->output_raw_) {
return sum;
return aggr.aggregate();
}
uint32_t mv = esp_adc_cal_raw_to_voltage(sum, &this->cal_characteristics_[(int32_t) this->attenuation_]);
uint32_t mv =
esp_adc_cal_raw_to_voltage(aggr.aggregate(), &this->cal_characteristics_[(int32_t) this->attenuation_]);
return mv / 1000.0f;
}

View File

@ -31,23 +31,27 @@ void ADCSensor::dump_config() {
LOG_PIN(" Pin: ", this->pin_);
#endif // USE_ADC_SENSOR_VCC
ESP_LOGCONFIG(TAG, " Samples: %i", this->sample_count_);
ESP_LOGCONFIG(TAG, " Sampling mode: %s", LOG_STR_ARG(sampling_mode_to_str(this->sampling_mode_)));
LOG_UPDATE_INTERVAL(this);
}
float ADCSensor::sample() {
uint32_t raw = 0;
auto aggr = Aggregator(this->sampling_mode_);
for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
uint32_t raw = 0;
#ifdef USE_ADC_SENSOR_VCC
raw += ESP.getVcc(); // NOLINT(readability-static-accessed-through-instance)
raw = ESP.getVcc(); // NOLINT(readability-static-accessed-through-instance)
#else
raw += analogRead(this->pin_->get_pin()); // NOLINT
raw = analogRead(this->pin_->get_pin()); // NOLINT
#endif // USE_ADC_SENSOR_VCC
aggr.add_sample(raw);
}
raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_; // NOLINT(clang-analyzer-core.DivideZero)
if (this->output_raw_) {
return raw;
return aggr.aggregate();
}
return raw / 1024.0f;
return aggr.aggregate() / 1024.0f;
}
std::string ADCSensor::unique_id() { return get_mac_address() + "-adc"; }

View File

@ -23,23 +23,28 @@ void ADCSensor::dump_config() {
LOG_PIN(" Pin: ", this->pin_);
#endif // USE_ADC_SENSOR_VCC
ESP_LOGCONFIG(TAG, " Samples: %i", this->sample_count_);
ESP_LOGCONFIG(TAG, " Sampling mode: %s", LOG_STR_ARG(sampling_mode_to_str(this->sampling_mode_)));
LOG_UPDATE_INTERVAL(this);
}
float ADCSensor::sample() {
uint32_t raw = 0;
auto aggr = Aggregator(this->sampling_mode_);
if (this->output_raw_) {
for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
raw += analogRead(this->pin_->get_pin()); // NOLINT
raw = analogRead(this->pin_->get_pin()); // NOLINT
aggr.add_sample(raw);
}
raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_; // NOLINT(clang-analyzer-core.DivideZero)
return raw;
return aggr.aggregate();
}
for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
raw += analogReadVoltage(this->pin_->get_pin()); // NOLINT
raw = analogReadVoltage(this->pin_->get_pin()); // NOLINT
aggr.add_sample(raw);
}
raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_; // NOLINT(clang-analyzer-core.DivideZero)
return raw / 1000.0f;
return aggr.aggregate() / 1000.0f;
}
} // namespace adc

View File

@ -34,24 +34,28 @@ void ADCSensor::dump_config() {
#endif // USE_ADC_SENSOR_VCC
}
ESP_LOGCONFIG(TAG, " Samples: %i", this->sample_count_);
ESP_LOGCONFIG(TAG, " Sampling mode: %s", LOG_STR_ARG(sampling_mode_to_str(this->sampling_mode_)));
LOG_UPDATE_INTERVAL(this);
}
float ADCSensor::sample() {
uint32_t raw = 0;
auto aggr = Aggregator(this->sampling_mode_);
if (this->is_temperature_) {
adc_set_temp_sensor_enabled(true);
delay(1);
adc_select_input(4);
uint32_t raw = 0;
for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
raw += adc_read();
raw = adc_read();
aggr.add_sample(raw);
}
raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_; // NOLINT(clang-analyzer-core.DivideZero)
adc_set_temp_sensor_enabled(false);
if (this->output_raw_) {
return raw;
return aggr.aggregate();
}
return raw * 3.3f / 4096.0f;
return aggr.aggregate() * 3.3f / 4096.0f;
}
uint8_t pin = this->pin_->get_pin();
@ -68,11 +72,10 @@ float ADCSensor::sample() {
adc_gpio_init(pin);
adc_select_input(pin - 26);
uint32_t raw = 0;
for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
raw += adc_read();
raw = adc_read();
aggr.add_sample(raw);
}
raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_; // NOLINT(clang-analyzer-core.DivideZero)
#ifdef CYW43_USES_VSYS_PIN
if (pin == PICO_VSYS_PIN) {
@ -81,10 +84,10 @@ float ADCSensor::sample() {
#endif // CYW43_USES_VSYS_PIN
if (this->output_raw_) {
return raw;
return aggr.aggregate();
}
float coeff = pin == PICO_VSYS_PIN ? 3.0f : 1.0f;
return raw * 3.3f / 4096.0f * coeff;
return aggr.aggregate() * 3.3f / 4096.0f * coeff;
}
} // namespace adc

View File

@ -1,11 +1,9 @@
import logging
import esphome.codegen as cg
import esphome.config_validation as cv
import esphome.final_validate as fv
from esphome.core import CORE
from esphome.components import sensor, voltage_sampler
from esphome.components.esp32 import get_esp32_variant
import esphome.config_validation as cv
from esphome.const import (
CONF_ATTENUATION,
CONF_ID,
@ -17,10 +15,14 @@ from esphome.const import (
STATE_CLASS_MEASUREMENT,
UNIT_VOLT,
)
from esphome.core import CORE
import esphome.final_validate as fv
from . import (
ATTENUATION_MODES,
ESP32_VARIANT_ADC1_PIN_TO_CHANNEL,
ESP32_VARIANT_ADC2_PIN_TO_CHANNEL,
SAMPLING_MODES,
adc_ns,
validate_adc_pin,
)
@ -30,9 +32,11 @@ _LOGGER = logging.getLogger(__name__)
AUTO_LOAD = ["voltage_sampler"]
CONF_SAMPLES = "samples"
CONF_SAMPLING_MODE = "sampling_mode"
_attenuation = cv.enum(ATTENUATION_MODES, lower=True)
_sampling_mode = cv.enum(SAMPLING_MODES, lower=True)
def validate_config(config):
@ -88,6 +92,7 @@ CONFIG_SCHEMA = cv.All(
cv.only_on_esp32, _attenuation
),
cv.Optional(CONF_SAMPLES, default=1): cv.int_range(min=1, max=255),
cv.Optional(CONF_SAMPLING_MODE, default="avg"): _sampling_mode,
}
)
.extend(cv.polling_component_schema("60s")),
@ -112,6 +117,7 @@ async def to_code(config):
cg.add(var.set_output_raw(config[CONF_RAW]))
cg.add(var.set_sample_count(config[CONF_SAMPLES]))
cg.add(var.set_sampling_mode(config[CONF_SAMPLING_MODE]))
if attenuation := config.get(CONF_ATTENUATION):
if attenuation == "auto":

View File

@ -1,12 +1,11 @@
from __future__ import annotations
import asyncio
import logging
from datetime import datetime
from typing import Any
import logging
from typing import TYPE_CHECKING, Any
from aioesphomeapi import APIClient
from aioesphomeapi.api_pb2 import SubscribeLogsResponse
from aioesphomeapi.log_runner import async_run
from esphome.const import CONF_KEY, CONF_PASSWORD, CONF_PORT, __version__
@ -14,6 +13,12 @@ from esphome.core import CORE
from . import CONF_ENCRYPTION
if TYPE_CHECKING:
from aioesphomeapi.api_pb2 import (
SubscribeLogsResponse, # pylint: disable=no-name-in-module
)
_LOGGER = logging.getLogger(__name__)

View File

@ -128,7 +128,6 @@ VISUAL_TEMPERATURE_STEP_SCHEMA = cv.Schema(
def visual_temperature_step(value):
# Allow defining target/current temperature steps separately
if isinstance(value, dict):
return VISUAL_TEMPERATURE_STEP_SCHEMA(value)

View File

@ -1,8 +1,5 @@
#include "cse7766.h"
#include "esphome/core/log.h"
#include <cinttypes>
#include <iomanip>
#include <sstream>
namespace esphome {
namespace cse7766 {
@ -72,12 +69,8 @@ bool CSE7766Component::check_byte_() {
void CSE7766Component::parse_data_() {
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
{
std::stringstream ss;
ss << "Raw data:" << std::hex << std::uppercase << std::setfill('0');
for (uint8_t i = 0; i < 23; i++) {
ss << ' ' << std::setw(2) << static_cast<unsigned>(this->raw_data_[i]);
}
ESP_LOGVV(TAG, "%s", ss.str().c_str());
std::string s = format_hex_pretty(this->raw_data_, sizeof(this->raw_data_));
ESP_LOGVV(TAG, "Raw data: %s", s.c_str());
}
#endif
@ -211,21 +204,20 @@ void CSE7766Component::parse_data_() {
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
{
std::stringstream ss;
ss << "Parsed:";
std::string buf = "Parsed:";
if (have_voltage) {
ss << " V=" << voltage << "V";
buf += str_sprintf(" V=%fV", voltage);
}
if (have_current) {
ss << " I=" << current * 1000.0f << "mA (~" << calculated_current * 1000.0f << "mA)";
buf += str_sprintf(" I=%fmA (~%fmA)", current * 1000.0f, calculated_current * 1000.0f);
}
if (have_power) {
ss << " P=" << power << "W";
buf += str_sprintf(" P=%fW", power);
}
if (energy != 0.0f) {
ss << " E=" << energy << "kWh (" << cf_pulses << ")";
buf += str_sprintf(" E=%fkWh (%u)", energy, cf_pulses);
}
ESP_LOGVV(TAG, "%s", ss.str().c_str());
ESP_LOGVV(TAG, "%s", buf.c_str());
}
#endif
}

View File

@ -23,6 +23,7 @@ void DHT::dump_config() {
} else {
ESP_LOGCONFIG(TAG, " Model: DHT22 (or equivalent)");
}
ESP_LOGCONFIG(TAG, " Internal Pull-up: %s", ONOFF(this->pin_->get_flags() & gpio::FLAG_PULLUP));
LOG_UPDATE_INTERVAL(this);
@ -101,7 +102,7 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r
} else {
delayMicroseconds(800);
}
this->pin_->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP);
this->pin_->pin_mode(this->pin_->get_flags());
{
InterruptLock lock;

View File

@ -34,7 +34,7 @@ DHT = dht_ns.class_("DHT", cg.PollingComponent)
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(DHT),
cv.Required(CONF_PIN): pins.internal_gpio_input_pin_schema,
cv.Required(CONF_PIN): pins.internal_gpio_input_pullup_pin_schema,
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=1,

View File

@ -815,8 +815,20 @@ void Display::test_card() {
DisplayPage::DisplayPage(display_writer_t writer) : writer_(std::move(writer)) {}
void DisplayPage::show() { this->parent_->show_page(this); }
void DisplayPage::show_next() { this->next_->show(); }
void DisplayPage::show_prev() { this->prev_->show(); }
void DisplayPage::show_next() {
if (this->next_ == nullptr) {
ESP_LOGE(TAG, "no next page");
return;
}
this->next_->show();
}
void DisplayPage::show_prev() {
if (this->prev_ == nullptr) {
ESP_LOGE(TAG, "no previous page");
return;
}
this->prev_->show();
}
void DisplayPage::set_parent(Display *parent) { this->parent_ = parent; }
void DisplayPage::set_prev(DisplayPage *prev) { this->prev_ = prev; }
void DisplayPage::set_next(DisplayPage *next) { this->next_ = next; }

View File

@ -66,7 +66,9 @@ FINAL_VALIDATE_SCHEMA = esp32_ble.validate_variant
async def to_code(config):
uuid = config[CONF_UUID].hex
uuid_arr = [cg.RawExpression(f"0x{uuid[i:i + 2]}") for i in range(0, len(uuid), 2)]
uuid_arr = [
cg.RawExpression(f"0x{uuid[i : i + 2]}") for i in range(0, len(uuid), 2)
]
var = cg.new_Pvariable(config[CONF_ID], uuid_arr)
parent = await cg.get_variable(config[esp32_ble.CONF_BLE_ID])

View File

@ -7,13 +7,16 @@
#ifdef USE_ARDUINO
#include <esp32-hal-dac.h>
#endif
#ifdef USE_ESP_IDF
#include <driver/dac.h>
#endif
namespace esphome {
namespace esp32_dac {
#ifdef USE_ESP32_VARIANT_ESP32S2
static constexpr uint8_t DAC0_PIN = 17;
#else
static constexpr uint8_t DAC0_PIN = 25;
#endif
static const char *const TAG = "esp32_dac";
void ESP32DAC::setup() {
@ -22,8 +25,15 @@ void ESP32DAC::setup() {
this->turn_off();
#ifdef USE_ESP_IDF
auto channel = pin_->get_pin() == 25 ? DAC_CHANNEL_1 : DAC_CHANNEL_2;
dac_output_enable(channel);
const dac_channel_t channel = this->pin_->get_pin() == DAC0_PIN ? DAC_CHAN_0 : DAC_CHAN_1;
const dac_oneshot_config_t oneshot_cfg{channel};
dac_oneshot_new_channel(&oneshot_cfg, &this->dac_handle_);
#endif
}
void ESP32DAC::on_safe_shutdown() {
#ifdef USE_ESP_IDF
dac_oneshot_del_channel(this->dac_handle_);
#endif
}
@ -40,8 +50,7 @@ void ESP32DAC::write_state(float state) {
state = state * 255;
#ifdef USE_ESP_IDF
auto channel = pin_->get_pin() == 25 ? DAC_CHANNEL_1 : DAC_CHANNEL_2;
dac_output_voltage(channel, (uint8_t) state);
dac_oneshot_output_voltage(this->dac_handle_, state);
#endif
#ifdef USE_ARDUINO
dacWrite(this->pin_->get_pin(), state);

View File

@ -7,6 +7,10 @@
#ifdef USE_ESP32
#ifdef USE_ESP_IDF
#include <driver/dac_oneshot.h>
#endif
namespace esphome {
namespace esp32_dac {
@ -16,6 +20,7 @@ class ESP32DAC : public output::FloatOutput, public Component {
/// Initialize pin
void setup() override;
void on_safe_shutdown() override;
void dump_config() override;
/// HARDWARE setup_priority
float get_setup_priority() const override { return setup_priority::HARDWARE; }
@ -24,6 +29,9 @@ class ESP32DAC : public output::FloatOutput, public Component {
void write_state(float state) override;
InternalGPIOPin *pin_;
#ifdef USE_ESP_IDF
dac_oneshot_handle_t dac_handle_;
#endif
};
} // namespace esp32_dac

View File

@ -1,15 +1,27 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import pins
from esphome.components import output
import esphome.config_validation as cv
import esphome.codegen as cg
from esphome.components.esp32 import get_esp32_variant
from esphome.components.esp32.const import VARIANT_ESP32, VARIANT_ESP32S2
from esphome.const import CONF_ID, CONF_NUMBER, CONF_PIN
DEPENDENCIES = ["esp32"]
DAC_PINS = {
VARIANT_ESP32: (25, 26),
VARIANT_ESP32S2: (17, 18),
}
def valid_dac_pin(value):
num = value[CONF_NUMBER]
cv.one_of(25, 26)(num)
variant = get_esp32_variant()
try:
valid_pins = DAC_PINS[variant]
except KeyError as ex:
raise cv.Invalid(f"DAC is not supported on {variant}") from ex
given_pin = value[CONF_NUMBER]
cv.one_of(*valid_pins)(given_pin)
return value

View File

@ -112,8 +112,7 @@ def validate_supports(value):
)
if is_pullup and num == 16:
raise cv.Invalid(
"GPIO Pin 16 does not support pullup pin mode. "
"Please choose another pin.",
"GPIO Pin 16 does not support pullup pin mode. Please choose another pin.",
[CONF_MODE, CONF_PULLUP],
)
if is_pulldown and num != 16:

View File

@ -5,8 +5,8 @@ import os
from pathlib import Path
import re
import esphome_glyphsets as glyphsets
import freetype
import glyphsets
import requests
from esphome import core, external_files

View File

@ -4,9 +4,6 @@
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
#include <algorithm>
#include <sstream>
#include <iostream> // std::cout, std::fixed
#include <iomanip>
namespace esphome {
namespace graph {
@ -231,9 +228,8 @@ void GraphLegend::init(Graph *g) {
ESP_LOGI(TAGL, " %s %d %d", txtstr.c_str(), fw, fh);
if (this->values_ != VALUE_POSITION_TYPE_NONE) {
std::stringstream ss;
ss << std::fixed << std::setprecision(trace->sensor_->get_accuracy_decimals()) << trace->sensor_->get_state();
std::string valstr = ss.str();
std::string valstr =
value_accuracy_to_string(trace->sensor_->get_state(), trace->sensor_->get_accuracy_decimals());
if (this->units_) {
valstr += trace->sensor_->get_unit_of_measurement();
}
@ -368,9 +364,8 @@ void Graph::draw_legend(display::Display *buff, uint16_t x_offset, uint16_t y_of
if (legend_->values_ != VALUE_POSITION_TYPE_NONE) {
int xv = x + legend_->xv_;
int yv = y + legend_->yv_;
std::stringstream ss;
ss << std::fixed << std::setprecision(trace->sensor_->get_accuracy_decimals()) << trace->sensor_->get_state();
std::string valstr = ss.str();
std::string valstr =
value_accuracy_to_string(trace->sensor_->get_state(), trace->sensor_->get_accuracy_decimals());
if (legend_->units_) {
valstr += trace->sensor_->get_unit_of_measurement();
}

View File

@ -1,9 +1,15 @@
import logging
import esphome.codegen as cg
import esphome.config_validation as cv
import esphome.final_validate as fv
from esphome.components import uart, climate, logger
import logging
from esphome import automation
import esphome.codegen as cg
from esphome.components import climate, logger, uart
from esphome.components.climate import (
CONF_CURRENT_TEMPERATURE,
ClimateMode,
ClimatePreset,
ClimateSwingMode,
)
import esphome.config_validation as cv
from esphome.const import (
CONF_BEEPER,
CONF_DISPLAY,
@ -24,12 +30,7 @@ from esphome.const import (
CONF_VISUAL,
CONF_WIFI,
)
from esphome.components.climate import (
ClimateMode,
ClimatePreset,
ClimateSwingMode,
CONF_CURRENT_TEMPERATURE,
)
import esphome.final_validate as fv
_LOGGER = logging.getLogger(__name__)

View File

@ -18,8 +18,8 @@ namespace esphome {
namespace http_request {
struct Header {
const char *name;
const char *value;
std::string name;
std::string value;
};
// Some common HTTP status codes

View File

@ -96,7 +96,7 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::start(std::string url, std::s
container->client_.setUserAgent(this->useragent_);
}
for (const auto &header : headers) {
container->client_.addHeader(header.name, header.value, false, true);
container->client_.addHeader(header.name.c_str(), header.value.c_str(), false, true);
}
// returned needed headers must be collected before the requests

View File

@ -84,7 +84,7 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::start(std::string url, std::strin
container->set_secure(secure);
for (const auto &header : headers) {
esp_http_client_set_header(client, header.name, header.value);
esp_http_client_set_header(client, header.name.c_str(), header.value.c_str());
}
const int body_len = body.length();

View File

@ -0,0 +1,51 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import uart
from esphome.const import (
CONF_ID,
CONF_THROTTLE,
)
DEPENDENCIES = ["uart"]
CODEOWNERS = ["@hareeshmu"]
MULTI_CONF = True
ld2450_ns = cg.esphome_ns.namespace("ld2450")
LD2450Component = ld2450_ns.class_("LD2450Component", cg.Component, uart.UARTDevice)
CONF_LD2450_ID = "ld2450_id"
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(LD2450Component),
cv.Optional(CONF_THROTTLE, default="1000ms"): cv.All(
cv.positive_time_period_milliseconds,
cv.Range(min=cv.TimePeriod(milliseconds=1)),
),
}
)
.extend(uart.UART_DEVICE_SCHEMA)
.extend(cv.COMPONENT_SCHEMA)
)
LD2450BaseSchema = cv.Schema(
{
cv.GenerateID(CONF_LD2450_ID): cv.use_id(LD2450Component),
},
)
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
"ld2450",
require_tx=True,
require_rx=True,
parity="NONE",
stop_bits=1,
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
cg.add(var.set_throttle(config[CONF_THROTTLE]))

View File

@ -0,0 +1,47 @@
import esphome.codegen as cg
from esphome.components import binary_sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_HAS_MOVING_TARGET,
CONF_HAS_STILL_TARGET,
CONF_HAS_TARGET,
DEVICE_CLASS_MOTION,
DEVICE_CLASS_OCCUPANCY,
)
from . import CONF_LD2450_ID, LD2450Component
DEPENDENCIES = ["ld2450"]
ICON_MEDITATION = "mdi:meditation"
ICON_SHIELD_ACCOUNT = "mdi:shield-account"
ICON_TARGET_ACCOUNT = "mdi:target-account"
CONFIG_SCHEMA = {
cv.GenerateID(CONF_LD2450_ID): cv.use_id(LD2450Component),
cv.Optional(CONF_HAS_TARGET): binary_sensor.binary_sensor_schema(
device_class=DEVICE_CLASS_OCCUPANCY,
icon=ICON_SHIELD_ACCOUNT,
),
cv.Optional(CONF_HAS_MOVING_TARGET): binary_sensor.binary_sensor_schema(
device_class=DEVICE_CLASS_MOTION,
icon=ICON_TARGET_ACCOUNT,
),
cv.Optional(CONF_HAS_STILL_TARGET): binary_sensor.binary_sensor_schema(
device_class=DEVICE_CLASS_OCCUPANCY,
icon=ICON_MEDITATION,
),
}
async def to_code(config):
ld2450_component = await cg.get_variable(config[CONF_LD2450_ID])
if has_target_config := config.get(CONF_HAS_TARGET):
sens = await binary_sensor.new_binary_sensor(has_target_config)
cg.add(ld2450_component.set_target_binary_sensor(sens))
if has_moving_target_config := config.get(CONF_HAS_MOVING_TARGET):
sens = await binary_sensor.new_binary_sensor(has_moving_target_config)
cg.add(ld2450_component.set_moving_target_binary_sensor(sens))
if has_still_target_config := config.get(CONF_HAS_STILL_TARGET):
sens = await binary_sensor.new_binary_sensor(has_still_target_config)
cg.add(ld2450_component.set_still_target_binary_sensor(sens))

View File

@ -0,0 +1,45 @@
import esphome.codegen as cg
from esphome.components import button
import esphome.config_validation as cv
from esphome.const import (
CONF_FACTORY_RESET,
CONF_RESTART,
DEVICE_CLASS_RESTART,
ENTITY_CATEGORY_CONFIG,
ENTITY_CATEGORY_DIAGNOSTIC,
ICON_RESTART,
ICON_RESTART_ALERT,
)
from .. import CONF_LD2450_ID, LD2450Component, ld2450_ns
ResetButton = ld2450_ns.class_("ResetButton", button.Button)
RestartButton = ld2450_ns.class_("RestartButton", button.Button)
CONFIG_SCHEMA = {
cv.GenerateID(CONF_LD2450_ID): cv.use_id(LD2450Component),
cv.Optional(CONF_FACTORY_RESET): button.button_schema(
ResetButton,
device_class=DEVICE_CLASS_RESTART,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_RESTART_ALERT,
),
cv.Optional(CONF_RESTART): button.button_schema(
RestartButton,
device_class=DEVICE_CLASS_RESTART,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
icon=ICON_RESTART,
),
}
async def to_code(config):
ld2450_component = await cg.get_variable(config[CONF_LD2450_ID])
if factory_reset_config := config.get(CONF_FACTORY_RESET):
b = await button.new_button(factory_reset_config)
await cg.register_parented(b, config[CONF_LD2450_ID])
cg.add(ld2450_component.set_reset_button(b))
if restart_config := config.get(CONF_RESTART):
b = await button.new_button(restart_config)
await cg.register_parented(b, config[CONF_LD2450_ID])
cg.add(ld2450_component.set_restart_button(b))

View File

@ -0,0 +1,9 @@
#include "reset_button.h"
namespace esphome {
namespace ld2450 {
void ResetButton::press_action() { this->parent_->factory_reset(); }
} // namespace ld2450
} // namespace esphome

View File

@ -0,0 +1,18 @@
#pragma once
#include "esphome/components/button/button.h"
#include "../ld2450.h"
namespace esphome {
namespace ld2450 {
class ResetButton : public button::Button, public Parented<LD2450Component> {
public:
ResetButton() = default;
protected:
void press_action() override;
};
} // namespace ld2450
} // namespace esphome

View File

@ -0,0 +1,9 @@
#include "restart_button.h"
namespace esphome {
namespace ld2450 {
void RestartButton::press_action() { this->parent_->restart_and_read_all_info(); }
} // namespace ld2450
} // namespace esphome

View File

@ -0,0 +1,18 @@
#pragma once
#include "esphome/components/button/button.h"
#include "../ld2450.h"
namespace esphome {
namespace ld2450 {
class RestartButton : public button::Button, public Parented<LD2450Component> {
public:
RestartButton() = default;
protected:
void press_action() override;
};
} // namespace ld2450
} // namespace esphome

View File

@ -0,0 +1,867 @@
#include "ld2450.h"
#include <utility>
#ifdef USE_NUMBER
#include "esphome/components/number/number.h"
#endif
#ifdef USE_SENSOR
#include "esphome/components/sensor/sensor.h"
#endif
#include "esphome/core/component.h"
#define highbyte(val) (uint8_t)((val) >> 8)
#define lowbyte(val) (uint8_t)((val) &0xff)
namespace esphome {
namespace ld2450 {
static const char *const TAG = "ld2450";
static const char *const UNKNOWN_MAC("unknown");
// LD2450 UART Serial Commands
static const uint8_t CMD_ENABLE_CONF = 0x00FF;
static const uint8_t CMD_DISABLE_CONF = 0x00FE;
static const uint8_t CMD_VERSION = 0x00A0;
static const uint8_t CMD_MAC = 0x00A5;
static const uint8_t CMD_RESET = 0x00A2;
static const uint8_t CMD_RESTART = 0x00A3;
static const uint8_t CMD_BLUETOOTH = 0x00A4;
static const uint8_t CMD_SINGLE_TARGET_MODE = 0x0080;
static const uint8_t CMD_MULTI_TARGET_MODE = 0x0090;
static const uint8_t CMD_QUERY_TARGET_MODE = 0x0091;
static const uint8_t CMD_SET_BAUD_RATE = 0x00A1;
static const uint8_t CMD_QUERY_ZONE = 0x00C1;
static const uint8_t CMD_SET_ZONE = 0x00C2;
static inline uint16_t convert_seconds_to_ms(uint16_t value) { return value * 1000; };
static inline std::string convert_signed_int_to_hex(int value) {
auto value_as_str = str_snprintf("%04x", 4, value & 0xFFFF);
return value_as_str;
}
static inline void convert_int_values_to_hex(const int *values, uint8_t *bytes) {
for (int i = 0; i < 4; i++) {
std::string temp_hex = convert_signed_int_to_hex(values[i]);
bytes[i * 2] = std::stoi(temp_hex.substr(2, 2), nullptr, 16); // Store high byte
bytes[i * 2 + 1] = std::stoi(temp_hex.substr(0, 2), nullptr, 16); // Store low byte
}
}
static inline int16_t decode_coordinate(uint8_t low_byte, uint8_t high_byte) {
int16_t coordinate = (high_byte & 0x7F) << 8 | low_byte;
if ((high_byte & 0x80) == 0) {
coordinate = -coordinate;
}
return coordinate; // mm
}
static inline int16_t decode_speed(uint8_t low_byte, uint8_t high_byte) {
int16_t speed = (high_byte & 0x7F) << 8 | low_byte;
if ((high_byte & 0x80) == 0) {
speed = -speed;
}
return speed * 10; // mm/s
}
static inline int16_t hex_to_signed_int(const uint8_t *buffer, uint8_t offset) {
uint16_t hex_val = (buffer[offset + 1] << 8) | buffer[offset];
int16_t dec_val = static_cast<int16_t>(hex_val);
if (dec_val & 0x8000) {
dec_val -= 65536;
}
return dec_val;
}
static inline float calculate_angle(float base, float hypotenuse) {
if (base < 0.0 || hypotenuse <= 0.0) {
return 0.0;
}
float angle_radians = std::acos(base / hypotenuse);
float angle_degrees = angle_radians * (180.0 / M_PI);
return angle_degrees;
}
static inline std::string get_direction(int16_t speed) {
static const char *const APPROACHING = "Approaching";
static const char *const MOVING_AWAY = "Moving away";
static const char *const STATIONARY = "Stationary";
if (speed > 0) {
return MOVING_AWAY;
}
if (speed < 0) {
return APPROACHING;
}
return STATIONARY;
}
static inline std::string format_mac(uint8_t *buffer) {
return str_snprintf("%02X:%02X:%02X:%02X:%02X:%02X", 17, buffer[10], buffer[11], buffer[12], buffer[13], buffer[14],
buffer[15]);
}
static inline std::string format_version(uint8_t *buffer) {
return str_sprintf("%u.%02X.%02X%02X%02X%02X", buffer[13], buffer[12], buffer[17], buffer[16], buffer[15],
buffer[14]);
}
LD2450Component::LD2450Component() {}
void LD2450Component::setup() {
ESP_LOGCONFIG(TAG, "Setting up HLK-LD2450...");
#ifdef USE_NUMBER
this->pref_ = global_preferences->make_preference<float>(this->presence_timeout_number_->get_object_id_hash());
this->set_presence_timeout();
#endif
this->read_all_info();
}
void LD2450Component::dump_config() {
ESP_LOGCONFIG(TAG, "HLK-LD2450 Human motion tracking radar module:");
#ifdef USE_BINARY_SENSOR
LOG_BINARY_SENSOR(" ", "TargetBinarySensor", this->target_binary_sensor_);
LOG_BINARY_SENSOR(" ", "MovingTargetBinarySensor", this->moving_target_binary_sensor_);
LOG_BINARY_SENSOR(" ", "StillTargetBinarySensor", this->still_target_binary_sensor_);
#endif
#ifdef USE_SWITCH
LOG_SWITCH(" ", "BluetoothSwitch", this->bluetooth_switch_);
LOG_SWITCH(" ", "MultiTargetSwitch", this->multi_target_switch_);
#endif
#ifdef USE_BUTTON
LOG_BUTTON(" ", "ResetButton", this->reset_button_);
LOG_BUTTON(" ", "RestartButton", this->restart_button_);
#endif
#ifdef USE_SENSOR
LOG_SENSOR(" ", "TargetCountSensor", this->target_count_sensor_);
LOG_SENSOR(" ", "StillTargetCountSensor", this->still_target_count_sensor_);
LOG_SENSOR(" ", "MovingTargetCountSensor", this->moving_target_count_sensor_);
for (sensor::Sensor *s : this->move_x_sensors_) {
LOG_SENSOR(" ", "NthTargetXSensor", s);
}
for (sensor::Sensor *s : this->move_y_sensors_) {
LOG_SENSOR(" ", "NthTargetYSensor", s);
}
for (sensor::Sensor *s : this->move_speed_sensors_) {
LOG_SENSOR(" ", "NthTargetSpeedSensor", s);
}
for (sensor::Sensor *s : this->move_angle_sensors_) {
LOG_SENSOR(" ", "NthTargetAngleSensor", s);
}
for (sensor::Sensor *s : this->move_distance_sensors_) {
LOG_SENSOR(" ", "NthTargetDistanceSensor", s);
}
for (sensor::Sensor *s : this->move_resolution_sensors_) {
LOG_SENSOR(" ", "NthTargetResolutionSensor", s);
}
for (sensor::Sensor *s : this->zone_target_count_sensors_) {
LOG_SENSOR(" ", "NthZoneTargetCountSensor", s);
}
for (sensor::Sensor *s : this->zone_still_target_count_sensors_) {
LOG_SENSOR(" ", "NthZoneStillTargetCountSensor", s);
}
for (sensor::Sensor *s : this->zone_moving_target_count_sensors_) {
LOG_SENSOR(" ", "NthZoneMovingTargetCountSensor", s);
}
#endif
#ifdef USE_TEXT_SENSOR
LOG_TEXT_SENSOR(" ", "VersionTextSensor", this->version_text_sensor_);
LOG_TEXT_SENSOR(" ", "MacTextSensor", this->mac_text_sensor_);
for (text_sensor::TextSensor *s : this->direction_text_sensors_) {
LOG_TEXT_SENSOR(" ", "NthDirectionTextSensor", s);
}
#endif
#ifdef USE_NUMBER
for (number::Number *n : this->zone_x1_numbers_) {
LOG_NUMBER(" ", "ZoneX1Number", n);
}
for (number::Number *n : this->zone_y1_numbers_) {
LOG_NUMBER(" ", "ZoneY1Number", n);
}
for (number::Number *n : this->zone_x2_numbers_) {
LOG_NUMBER(" ", "ZoneX2Number", n);
}
for (number::Number *n : this->zone_y2_numbers_) {
LOG_NUMBER(" ", "ZoneY2Number", n);
}
#endif
#ifdef USE_SELECT
LOG_SELECT(" ", "BaudRateSelect", this->baud_rate_select_);
LOG_SELECT(" ", "ZoneTypeSelect", this->zone_type_select_);
#endif
#ifdef USE_NUMBER
LOG_NUMBER(" ", "PresenceTimeoutNumber", this->presence_timeout_number_);
#endif
ESP_LOGCONFIG(TAG, " Throttle : %ums", this->throttle_);
ESP_LOGCONFIG(TAG, " MAC Address : %s", const_cast<char *>(this->mac_.c_str()));
ESP_LOGCONFIG(TAG, " Firmware version : %s", const_cast<char *>(this->version_.c_str()));
}
void LD2450Component::loop() {
while (this->available()) {
this->readline_(read(), this->buffer_data_, MAX_LINE_LENGTH);
}
}
// Count targets in zone
uint8_t LD2450Component::count_targets_in_zone_(const Zone &zone, bool is_moving) {
uint8_t count = 0;
for (auto &index : this->target_info_) {
if (index.x > zone.x1 && index.x < zone.x2 && index.y > zone.y1 && index.y < zone.y2 &&
index.is_moving == is_moving) {
count++;
}
}
return count;
}
// Service reset_radar_zone
void LD2450Component::reset_radar_zone() {
this->zone_type_ = 0;
for (auto &i : this->zone_config_) {
i.x1 = 0;
i.y1 = 0;
i.x2 = 0;
i.y2 = 0;
}
this->send_set_zone_command_();
}
void LD2450Component::set_radar_zone(int32_t zone_type, int32_t zone1_x1, int32_t zone1_y1, int32_t zone1_x2,
int32_t zone1_y2, int32_t zone2_x1, int32_t zone2_y1, int32_t zone2_x2,
int32_t zone2_y2, int32_t zone3_x1, int32_t zone3_y1, int32_t zone3_x2,
int32_t zone3_y2) {
this->zone_type_ = zone_type;
int zone_parameters[12] = {zone1_x1, zone1_y1, zone1_x2, zone1_y2, zone2_x1, zone2_y1,
zone2_x2, zone2_y2, zone3_x1, zone3_y1, zone3_x2, zone3_y2};
for (int i = 0; i < MAX_ZONES; i++) {
this->zone_config_[i].x1 = zone_parameters[i * 4];
this->zone_config_[i].y1 = zone_parameters[i * 4 + 1];
this->zone_config_[i].x2 = zone_parameters[i * 4 + 2];
this->zone_config_[i].y2 = zone_parameters[i * 4 + 3];
}
this->send_set_zone_command_();
}
// Set Zone on LD2450 Sensor
void LD2450Component::send_set_zone_command_() {
uint8_t cmd_value[26] = {};
uint8_t zone_type_bytes[2] = {static_cast<uint8_t>(this->zone_type_), 0x00};
uint8_t area_config[24] = {};
for (int i = 0; i < MAX_ZONES; i++) {
int values[4] = {this->zone_config_[i].x1, this->zone_config_[i].y1, this->zone_config_[i].x2,
this->zone_config_[i].y2};
ld2450::convert_int_values_to_hex(values, area_config + (i * 8));
}
std::memcpy(cmd_value, zone_type_bytes, 2);
std::memcpy(cmd_value + 2, area_config, 24);
this->set_config_mode_(true);
this->send_command_(CMD_SET_ZONE, cmd_value, 26);
this->set_config_mode_(false);
}
// Check presense timeout to reset presence status
bool LD2450Component::get_timeout_status_(uint32_t check_millis) {
if (check_millis == 0) {
return true;
}
if (this->timeout_ == 0) {
this->timeout_ = ld2450::convert_seconds_to_ms(DEFAULT_PRESENCE_TIMEOUT);
}
auto current_millis = millis();
return current_millis - check_millis >= this->timeout_;
}
// Extract, store and publish zone details LD2450 buffer
void LD2450Component::process_zone_(uint8_t *buffer) {
uint8_t index, start;
for (index = 0; index < MAX_ZONES; index++) {
start = 12 + index * 8;
this->zone_config_[index].x1 = ld2450::hex_to_signed_int(buffer, start);
this->zone_config_[index].y1 = ld2450::hex_to_signed_int(buffer, start + 2);
this->zone_config_[index].x2 = ld2450::hex_to_signed_int(buffer, start + 4);
this->zone_config_[index].y2 = ld2450::hex_to_signed_int(buffer, start + 6);
#ifdef USE_NUMBER
this->zone_x1_numbers_[index]->publish_state(this->zone_config_[index].x1);
this->zone_y1_numbers_[index]->publish_state(this->zone_config_[index].y1);
this->zone_x2_numbers_[index]->publish_state(this->zone_config_[index].x2);
this->zone_y2_numbers_[index]->publish_state(this->zone_config_[index].y2);
#endif
}
}
// Read all info from LD2450 buffer
void LD2450Component::read_all_info() {
this->set_config_mode_(true);
this->get_version_();
this->get_mac_();
this->query_target_tracking_mode_();
this->query_zone_();
this->set_config_mode_(false);
#ifdef USE_SELECT
const auto baud_rate = std::to_string(this->parent_->get_baud_rate());
if (this->baud_rate_select_ != nullptr && this->baud_rate_select_->state != baud_rate) {
this->baud_rate_select_->publish_state(baud_rate);
}
this->publish_zone_type();
#endif
}
// Read zone info from LD2450 buffer
void LD2450Component::query_zone_info() {
this->set_config_mode_(true);
this->query_zone_();
this->set_config_mode_(false);
}
// Restart LD2450 and read all info from buffer
void LD2450Component::restart_and_read_all_info() {
this->set_config_mode_(true);
this->restart_();
this->set_timeout(1000, [this]() { this->read_all_info(); });
}
// Send command with values to LD2450
void LD2450Component::send_command_(uint8_t command, const uint8_t *command_value, uint8_t command_value_len) {
ESP_LOGV(TAG, "Sending command %02X", command);
// frame header
this->write_array(CMD_FRAME_HEADER, 4);
// length bytes
int len = 2;
if (command_value != nullptr) {
len += command_value_len;
}
this->write_byte(lowbyte(len));
this->write_byte(highbyte(len));
// command
this->write_byte(lowbyte(command));
this->write_byte(highbyte(command));
// command value bytes
if (command_value != nullptr) {
for (int i = 0; i < command_value_len; i++) {
this->write_byte(command_value[i]);
}
}
// footer
this->write_array(CMD_FRAME_END, 4);
// FIXME to remove
delay(50); // NOLINT
}
// LD2450 Radar data message:
// [AA FF 03 00] [0E 03 B1 86 10 00 40 01] [00 00 00 00 00 00 00 00] [00 00 00 00 00 00 00 00] [55 CC]
// Header Target 1 Target 2 Target 3 End
void LD2450Component::handle_periodic_data_(uint8_t *buffer, uint8_t len) {
if (len < 29) { // header (4 bytes) + 8 x 3 target data + footer (2 bytes)
ESP_LOGE(TAG, "Periodic data: invalid message length");
return;
}
if (buffer[0] != 0xAA || buffer[1] != 0xFF || buffer[2] != 0x03 || buffer[3] != 0x00) { // header
ESP_LOGE(TAG, "Periodic data: invalid message header");
return;
}
if (buffer[len - 2] != 0x55 || buffer[len - 1] != 0xCC) { // footer
ESP_LOGE(TAG, "Periodic data: invalid message footer");
return;
}
auto current_millis = millis();
if (current_millis - this->last_periodic_millis_ < this->throttle_) {
ESP_LOGV(TAG, "Throttling: %d", this->throttle_);
return;
}
this->last_periodic_millis_ = current_millis;
int16_t target_count = 0;
int16_t still_target_count = 0;
int16_t moving_target_count = 0;
int16_t start = 0;
int16_t val = 0;
uint8_t index = 0;
int16_t tx = 0;
int16_t ty = 0;
int16_t td = 0;
int16_t ts = 0;
int16_t angle = 0;
std::string direction{};
bool is_moving = false;
#ifdef USE_SENSOR
// Loop thru targets
// X
for (index = 0; index < MAX_TARGETS; index++) {
start = TARGET_X + index * 8;
is_moving = false;
sensor::Sensor *sx = this->move_x_sensors_[index];
if (sx != nullptr) {
val = ld2450::decode_coordinate(buffer[start], buffer[start + 1]);
tx = val;
sx->publish_state(val);
}
// Y
start = TARGET_Y + index * 8;
sensor::Sensor *sy = this->move_y_sensors_[index];
if (sy != nullptr) {
val = ld2450::decode_coordinate(buffer[start], buffer[start + 1]);
ty = val;
sy->publish_state(val);
}
// SPEED
start = TARGET_SPEED + index * 8;
sensor::Sensor *ss = this->move_speed_sensors_[index];
if (ss != nullptr) {
val = ld2450::decode_speed(buffer[start], buffer[start + 1]);
ts = val;
if (val) {
is_moving = true;
moving_target_count++;
}
ss->publish_state(val);
}
// RESOLUTION
start = TARGET_RESOLUTION + index * 8;
sensor::Sensor *sr = this->move_resolution_sensors_[index];
if (sr != nullptr) {
val = (buffer[start + 1] << 8) | buffer[start];
sr->publish_state(val);
}
// DISTANCE
sensor::Sensor *sd = this->move_distance_sensors_[index];
if (sd != nullptr) {
val = (uint16_t) sqrt(
pow(ld2450::decode_coordinate(buffer[TARGET_X + index * 8], buffer[(TARGET_X + index * 8) + 1]), 2) +
pow(ld2450::decode_coordinate(buffer[TARGET_Y + index * 8], buffer[(TARGET_Y + index * 8) + 1]), 2));
td = val;
if (val > 0) {
target_count++;
}
sd->publish_state(val);
}
// ANGLE
angle = calculate_angle(static_cast<float>(ty), static_cast<float>(td));
if (tx > 0) {
angle = angle * -1;
}
sensor::Sensor *sa = this->move_angle_sensors_[index];
if (sa != nullptr) {
sa->publish_state(angle);
}
#endif
// DIRECTION
#ifdef USE_TEXT_SENSOR
direction = get_direction(ts);
if (td == 0) {
direction = "NA";
}
text_sensor::TextSensor *tsd = this->direction_text_sensors_[index];
if (tsd != nullptr) {
tsd->publish_state(direction);
}
#endif
// Store target info for zone target count
this->target_info_[index].x = tx;
this->target_info_[index].y = ty;
this->target_info_[index].is_moving = is_moving;
} // End loop thru targets
#ifdef USE_SENSOR
// Loop thru zones
uint8_t zone_still_targets = 0;
uint8_t zone_moving_targets = 0;
uint8_t zone_all_targets = 0;
for (index = 0; index < MAX_ZONES; index++) {
// Publish Still Target Count in Zones
sensor::Sensor *szstc = this->zone_still_target_count_sensors_[index];
if (szstc != nullptr) {
zone_still_targets = this->count_targets_in_zone_(this->zone_config_[index], false);
szstc->publish_state(zone_still_targets);
}
// Publish Moving Target Count in Zones
sensor::Sensor *szmtc = this->zone_moving_target_count_sensors_[index];
if (szmtc != nullptr) {
zone_moving_targets = this->count_targets_in_zone_(this->zone_config_[index], true);
szmtc->publish_state(zone_moving_targets);
}
zone_all_targets = zone_still_targets + zone_moving_targets;
// Publish All Target Count in Zones
sensor::Sensor *sztc = this->zone_target_count_sensors_[index];
if (sztc != nullptr) {
sztc->publish_state(zone_all_targets);
}
} // End loop thru zones
still_target_count = target_count - moving_target_count;
// Target Count
if (this->target_count_sensor_ != nullptr) {
this->target_count_sensor_->publish_state(target_count);
}
// Still Target Count
if (this->still_target_count_sensor_ != nullptr) {
this->still_target_count_sensor_->publish_state(still_target_count);
}
// Moving Target Count
if (this->moving_target_count_sensor_ != nullptr) {
this->moving_target_count_sensor_->publish_state(moving_target_count);
}
#endif
#ifdef USE_BINARY_SENSOR
// Target Presence
if (this->target_binary_sensor_ != nullptr) {
if (target_count > 0) {
this->target_binary_sensor_->publish_state(true);
} else {
if (this->get_timeout_status_(this->presence_millis_)) {
this->target_binary_sensor_->publish_state(false);
} else {
ESP_LOGV(TAG, "Clear presence waiting timeout: %d", this->timeout_);
}
}
}
// Moving Target Presence
if (this->moving_target_binary_sensor_ != nullptr) {
if (moving_target_count > 0) {
this->moving_target_binary_sensor_->publish_state(true);
} else {
if (this->get_timeout_status_(this->moving_presence_millis_)) {
this->moving_target_binary_sensor_->publish_state(false);
}
}
}
// Still Target Presence
if (this->still_target_binary_sensor_ != nullptr) {
if (still_target_count > 0) {
this->still_target_binary_sensor_->publish_state(true);
} else {
if (this->get_timeout_status_(this->still_presence_millis_)) {
this->still_target_binary_sensor_->publish_state(false);
}
}
}
#endif
#ifdef USE_SENSOR
// For presence timeout check
if (target_count > 0) {
this->presence_millis_ = millis();
}
if (moving_target_count > 0) {
this->moving_presence_millis_ = millis();
}
if (still_target_count > 0) {
this->still_presence_millis_ = millis();
}
#endif
}
bool LD2450Component::handle_ack_data_(uint8_t *buffer, uint8_t len) {
ESP_LOGV(TAG, "Handling ack data for command %02X", buffer[COMMAND]);
if (len < 10) {
ESP_LOGE(TAG, "Ack data: invalid length");
return true;
}
if (buffer[0] != 0xFD || buffer[1] != 0xFC || buffer[2] != 0xFB || buffer[3] != 0xFA) { // frame header
ESP_LOGE(TAG, "Ack data: invalid header (command %02X)", buffer[COMMAND]);
return true;
}
if (buffer[COMMAND_STATUS] != 0x01) {
ESP_LOGE(TAG, "Ack data: invalid status");
return true;
}
if (buffer[8] || buffer[9]) {
ESP_LOGE(TAG, "Ack data: last buffer was %u, %u", buffer[8], buffer[9]);
return true;
}
switch (buffer[COMMAND]) {
case lowbyte(CMD_ENABLE_CONF):
ESP_LOGV(TAG, "Got enable conf command");
break;
case lowbyte(CMD_DISABLE_CONF):
ESP_LOGV(TAG, "Got disable conf command");
break;
case lowbyte(CMD_SET_BAUD_RATE):
ESP_LOGV(TAG, "Got baud rate change command");
#ifdef USE_SELECT
if (this->baud_rate_select_ != nullptr) {
ESP_LOGV(TAG, "Change baud rate to %s", this->baud_rate_select_->state.c_str());
}
#endif
break;
case lowbyte(CMD_VERSION):
this->version_ = ld2450::format_version(buffer);
ESP_LOGV(TAG, "Firmware version: %s", this->version_.c_str());
#ifdef USE_TEXT_SENSOR
if (this->version_text_sensor_ != nullptr) {
this->version_text_sensor_->publish_state(this->version_);
}
#endif
break;
case lowbyte(CMD_MAC):
if (len < 20) {
return false;
}
this->mac_ = ld2450::format_mac(buffer);
ESP_LOGV(TAG, "MAC address: %s", this->mac_.c_str());
#ifdef USE_TEXT_SENSOR
if (this->mac_text_sensor_ != nullptr) {
this->mac_text_sensor_->publish_state(this->mac_);
}
#endif
#ifdef USE_SWITCH
if (this->bluetooth_switch_ != nullptr) {
this->bluetooth_switch_->publish_state(this->mac_ != UNKNOWN_MAC);
}
#endif
break;
case lowbyte(CMD_BLUETOOTH):
ESP_LOGV(TAG, "Got Bluetooth command");
break;
case lowbyte(CMD_SINGLE_TARGET_MODE):
ESP_LOGV(TAG, "Got single target conf command");
#ifdef USE_SWITCH
if (this->multi_target_switch_ != nullptr) {
this->multi_target_switch_->publish_state(false);
}
#endif
break;
case lowbyte(CMD_MULTI_TARGET_MODE):
ESP_LOGV(TAG, "Got multi target conf command");
#ifdef USE_SWITCH
if (this->multi_target_switch_ != nullptr) {
this->multi_target_switch_->publish_state(true);
}
#endif
break;
case lowbyte(CMD_QUERY_TARGET_MODE):
ESP_LOGV(TAG, "Got query target tracking mode command");
#ifdef USE_SWITCH
if (this->multi_target_switch_ != nullptr) {
this->multi_target_switch_->publish_state(buffer[10] == 0x02);
}
#endif
break;
case lowbyte(CMD_QUERY_ZONE):
ESP_LOGV(TAG, "Got query zone conf command");
this->zone_type_ = std::stoi(std::to_string(buffer[10]), nullptr, 16);
this->publish_zone_type();
#ifdef USE_SELECT
if (this->zone_type_select_ != nullptr) {
ESP_LOGV(TAG, "Change zone type to: %s", this->zone_type_select_->state.c_str());
}
#endif
if (buffer[10] == 0x00) {
ESP_LOGV(TAG, "Zone: Disabled");
}
if (buffer[10] == 0x01) {
ESP_LOGV(TAG, "Zone: Area detection");
}
if (buffer[10] == 0x02) {
ESP_LOGV(TAG, "Zone: Area filter");
}
this->process_zone_(buffer);
break;
case lowbyte(CMD_SET_ZONE):
ESP_LOGV(TAG, "Got set zone conf command");
this->query_zone_info();
break;
default:
break;
}
return true;
}
// Read LD2450 buffer data
void LD2450Component::readline_(int readch, uint8_t *buffer, uint8_t len) {
if (readch < 0) {
return;
}
if (this->buffer_pos_ < len - 1) {
buffer[this->buffer_pos_++] = readch;
buffer[this->buffer_pos_] = 0;
} else {
this->buffer_pos_ = 0;
}
if (this->buffer_pos_ < 4) {
return;
}
if (buffer[this->buffer_pos_ - 2] == 0x55 && buffer[this->buffer_pos_ - 1] == 0xCC) {
ESP_LOGV(TAG, "Handle periodic radar data");
this->handle_periodic_data_(buffer, this->buffer_pos_);
this->buffer_pos_ = 0; // Reset position index for next frame
} else if (buffer[this->buffer_pos_ - 4] == 0x04 && buffer[this->buffer_pos_ - 3] == 0x03 &&
buffer[this->buffer_pos_ - 2] == 0x02 && buffer[this->buffer_pos_ - 1] == 0x01) {
ESP_LOGV(TAG, "Handle command ack data");
if (this->handle_ack_data_(buffer, this->buffer_pos_)) {
this->buffer_pos_ = 0; // Reset position index for next frame
} else {
ESP_LOGV(TAG, "Command ack data invalid");
}
}
}
// Set Config Mode - Pre-requisite sending commands
void LD2450Component::set_config_mode_(bool enable) {
uint8_t cmd = enable ? CMD_ENABLE_CONF : CMD_DISABLE_CONF;
uint8_t cmd_value[2] = {0x01, 0x00};
this->send_command_(cmd, enable ? cmd_value : nullptr, 2);
}
// Set Bluetooth Enable/Disable
void LD2450Component::set_bluetooth(bool enable) {
this->set_config_mode_(true);
uint8_t enable_cmd_value[2] = {0x01, 0x00};
uint8_t disable_cmd_value[2] = {0x00, 0x00};
this->send_command_(CMD_BLUETOOTH, enable ? enable_cmd_value : disable_cmd_value, 2);
this->set_timeout(200, [this]() { this->restart_and_read_all_info(); });
}
// Set Baud rate
void LD2450Component::set_baud_rate(const std::string &state) {
this->set_config_mode_(true);
uint8_t cmd_value[2] = {BAUD_RATE_ENUM_TO_INT.at(state), 0x00};
this->send_command_(CMD_SET_BAUD_RATE, cmd_value, 2);
this->set_timeout(200, [this]() { this->restart_(); });
}
// Set Zone Type - one of: Disabled, Detection, Filter
void LD2450Component::set_zone_type(const std::string &state) {
ESP_LOGV(TAG, "Set zone type: %s", state.c_str());
uint8_t zone_type = ZONE_TYPE_ENUM_TO_INT.at(state);
this->zone_type_ = zone_type;
this->send_set_zone_command_();
}
// Publish Zone Type to Select component
void LD2450Component::publish_zone_type() {
#ifdef USE_SELECT
std::string zone_type = ZONE_TYPE_INT_TO_ENUM.at(static_cast<ZoneTypeStructure>(this->zone_type_));
if (this->zone_type_select_ != nullptr) {
this->zone_type_select_->publish_state(zone_type);
}
#endif
}
// Set Single/Multiplayer target detection
void LD2450Component::set_multi_target(bool enable) {
this->set_config_mode_(true);
uint8_t cmd = enable ? CMD_MULTI_TARGET_MODE : CMD_SINGLE_TARGET_MODE;
this->send_command_(cmd, nullptr, 0);
this->set_config_mode_(false);
}
// LD2450 factory reset
void LD2450Component::factory_reset() {
this->set_config_mode_(true);
this->send_command_(CMD_RESET, nullptr, 0);
this->set_timeout(200, [this]() { this->restart_and_read_all_info(); });
}
// Restart LD2450 module
void LD2450Component::restart_() { this->send_command_(CMD_RESTART, nullptr, 0); }
// Get LD2450 firmware version
void LD2450Component::get_version_() { this->send_command_(CMD_VERSION, nullptr, 0); }
// Get LD2450 mac address
void LD2450Component::get_mac_() {
uint8_t cmd_value[2] = {0x01, 0x00};
this->send_command_(CMD_MAC, cmd_value, 2);
}
// Query for target tracking mode
void LD2450Component::query_target_tracking_mode_() { this->send_command_(CMD_QUERY_TARGET_MODE, nullptr, 0); }
// Query for zone info
void LD2450Component::query_zone_() { this->send_command_(CMD_QUERY_ZONE, nullptr, 0); }
#ifdef USE_SENSOR
void LD2450Component::set_move_x_sensor(uint8_t target, sensor::Sensor *s) { this->move_x_sensors_[target] = s; }
void LD2450Component::set_move_y_sensor(uint8_t target, sensor::Sensor *s) { this->move_y_sensors_[target] = s; }
void LD2450Component::set_move_speed_sensor(uint8_t target, sensor::Sensor *s) {
this->move_speed_sensors_[target] = s;
}
void LD2450Component::set_move_angle_sensor(uint8_t target, sensor::Sensor *s) {
this->move_angle_sensors_[target] = s;
}
void LD2450Component::set_move_distance_sensor(uint8_t target, sensor::Sensor *s) {
this->move_distance_sensors_[target] = s;
}
void LD2450Component::set_move_resolution_sensor(uint8_t target, sensor::Sensor *s) {
this->move_resolution_sensors_[target] = s;
}
void LD2450Component::set_zone_target_count_sensor(uint8_t zone, sensor::Sensor *s) {
this->zone_target_count_sensors_[zone] = s;
}
void LD2450Component::set_zone_still_target_count_sensor(uint8_t zone, sensor::Sensor *s) {
this->zone_still_target_count_sensors_[zone] = s;
}
void LD2450Component::set_zone_moving_target_count_sensor(uint8_t zone, sensor::Sensor *s) {
this->zone_moving_target_count_sensors_[zone] = s;
}
#endif
#ifdef USE_TEXT_SENSOR
void LD2450Component::set_direction_text_sensor(uint8_t target, text_sensor::TextSensor *s) {
this->direction_text_sensors_[target] = s;
}
#endif
// Send Zone coordinates data to LD2450
#ifdef USE_NUMBER
void LD2450Component::set_zone_coordinate(uint8_t zone) {
number::Number *x1sens = this->zone_x1_numbers_[zone];
number::Number *y1sens = this->zone_y1_numbers_[zone];
number::Number *x2sens = this->zone_x2_numbers_[zone];
number::Number *y2sens = this->zone_y2_numbers_[zone];
if (!x1sens->has_state() || !y1sens->has_state() || !x2sens->has_state() || !y2sens->has_state()) {
return;
}
this->zone_config_[zone].x1 = static_cast<int>(x1sens->state);
this->zone_config_[zone].y1 = static_cast<int>(y1sens->state);
this->zone_config_[zone].x2 = static_cast<int>(x2sens->state);
this->zone_config_[zone].y2 = static_cast<int>(y2sens->state);
this->send_set_zone_command_();
}
void LD2450Component::set_zone_x1_number(uint8_t zone, number::Number *n) { this->zone_x1_numbers_[zone] = n; }
void LD2450Component::set_zone_y1_number(uint8_t zone, number::Number *n) { this->zone_y1_numbers_[zone] = n; }
void LD2450Component::set_zone_x2_number(uint8_t zone, number::Number *n) { this->zone_x2_numbers_[zone] = n; }
void LD2450Component::set_zone_y2_number(uint8_t zone, number::Number *n) { this->zone_y2_numbers_[zone] = n; }
#endif
// Set Presence Timeout load and save from flash
#ifdef USE_NUMBER
void LD2450Component::set_presence_timeout() {
if (this->presence_timeout_number_ != nullptr) {
if (this->presence_timeout_number_->state == 0) {
float timeout = this->restore_from_flash_();
this->presence_timeout_number_->publish_state(timeout);
this->timeout_ = ld2450::convert_seconds_to_ms(timeout);
}
if (this->presence_timeout_number_->has_state()) {
this->save_to_flash_(this->presence_timeout_number_->state);
this->timeout_ = ld2450::convert_seconds_to_ms(this->presence_timeout_number_->state);
}
}
}
// Save Presence Timeout to flash
void LD2450Component::save_to_flash_(float value) { this->pref_.save(&value); }
// Load Presence Timeout from flash
float LD2450Component::restore_from_flash_() {
float value;
if (!this->pref_.load(&value)) {
value = DEFAULT_PRESENCE_TIMEOUT;
}
return value;
}
#endif
} // namespace ld2450
} // namespace esphome

View File

@ -0,0 +1,231 @@
#pragma once
#include <iomanip>
#include <map>
#include "esphome/components/uart/uart.h"
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#include "esphome/core/preferences.h"
#ifdef USE_SENSOR
#include "esphome/components/sensor/sensor.h"
#endif
#ifdef USE_NUMBER
#include "esphome/components/number/number.h"
#endif
#ifdef USE_SWITCH
#include "esphome/components/switch/switch.h"
#endif
#ifdef USE_BUTTON
#include "esphome/components/button/button.h"
#endif
#ifdef USE_SELECT
#include "esphome/components/select/select.h"
#endif
#ifdef USE_TEXT_SENSOR
#include "esphome/components/text_sensor/text_sensor.h"
#endif
#ifdef USE_BINARY_SENSOR
#include "esphome/components/binary_sensor/binary_sensor.h"
#endif
#ifndef M_PI
#define M_PI 3.14
#endif
namespace esphome {
namespace ld2450 {
// Constants
static const uint8_t DEFAULT_PRESENCE_TIMEOUT = 5; // Timeout to reset presense status 5 sec.
static const uint8_t MAX_LINE_LENGTH = 60; // Max characters for serial buffer
static const uint8_t MAX_TARGETS = 3; // Max 3 Targets in LD2450
static const uint8_t MAX_ZONES = 3; // Max 3 Zones in LD2450
// Target coordinate struct
struct Target {
int16_t x;
int16_t y;
bool is_moving;
};
// Zone coordinate struct
struct Zone {
int16_t x1 = 0;
int16_t y1 = 0;
int16_t x2 = 0;
int16_t y2 = 0;
};
enum BaudRateStructure : uint8_t {
BAUD_RATE_9600 = 1,
BAUD_RATE_19200 = 2,
BAUD_RATE_38400 = 3,
BAUD_RATE_57600 = 4,
BAUD_RATE_115200 = 5,
BAUD_RATE_230400 = 6,
BAUD_RATE_256000 = 7,
BAUD_RATE_460800 = 8
};
// Convert baud rate enum to int
static const std::map<std::string, uint8_t> BAUD_RATE_ENUM_TO_INT{
{"9600", BAUD_RATE_9600}, {"19200", BAUD_RATE_19200}, {"38400", BAUD_RATE_38400},
{"57600", BAUD_RATE_57600}, {"115200", BAUD_RATE_115200}, {"230400", BAUD_RATE_230400},
{"256000", BAUD_RATE_256000}, {"460800", BAUD_RATE_460800}};
// Zone type struct
enum ZoneTypeStructure : uint8_t { ZONE_DISABLED = 0, ZONE_DETECTION = 1, ZONE_FILTER = 2 };
// Convert zone type int to enum
static const std::map<ZoneTypeStructure, std::string> ZONE_TYPE_INT_TO_ENUM{
{ZONE_DISABLED, "Disabled"}, {ZONE_DETECTION, "Detection"}, {ZONE_FILTER, "Filter"}};
// Convert zone type enum to int
static const std::map<std::string, uint8_t> ZONE_TYPE_ENUM_TO_INT{
{"Disabled", ZONE_DISABLED}, {"Detection", ZONE_DETECTION}, {"Filter", ZONE_FILTER}};
// LD2450 serial command header & footer
static const uint8_t CMD_FRAME_HEADER[4] = {0xFD, 0xFC, 0xFB, 0xFA};
static const uint8_t CMD_FRAME_END[4] = {0x04, 0x03, 0x02, 0x01};
enum PeriodicDataStructure : uint8_t {
TARGET_X = 4,
TARGET_Y = 6,
TARGET_SPEED = 8,
TARGET_RESOLUTION = 10,
};
enum PeriodicDataValue : uint8_t { HEAD = 0XAA, END = 0x55, CHECK = 0x00 };
enum AckDataStructure : uint8_t { COMMAND = 6, COMMAND_STATUS = 7 };
class LD2450Component : public Component, public uart::UARTDevice {
#ifdef USE_SENSOR
SUB_SENSOR(target_count)
SUB_SENSOR(still_target_count)
SUB_SENSOR(moving_target_count)
#endif
#ifdef USE_BINARY_SENSOR
SUB_BINARY_SENSOR(target)
SUB_BINARY_SENSOR(moving_target)
SUB_BINARY_SENSOR(still_target)
#endif
#ifdef USE_TEXT_SENSOR
SUB_TEXT_SENSOR(version)
SUB_TEXT_SENSOR(mac)
#endif
#ifdef USE_SELECT
SUB_SELECT(baud_rate)
SUB_SELECT(zone_type)
#endif
#ifdef USE_SWITCH
SUB_SWITCH(bluetooth)
SUB_SWITCH(multi_target)
#endif
#ifdef USE_BUTTON
SUB_BUTTON(reset)
SUB_BUTTON(restart)
#endif
#ifdef USE_NUMBER
SUB_NUMBER(presence_timeout)
#endif
public:
LD2450Component();
void setup() override;
void dump_config() override;
void loop() override;
void set_presence_timeout();
void set_throttle(uint16_t value) { this->throttle_ = value; };
void read_all_info();
void query_zone_info();
void restart_and_read_all_info();
void set_bluetooth(bool enable);
void set_multi_target(bool enable);
void set_baud_rate(const std::string &state);
void set_zone_type(const std::string &state);
void publish_zone_type();
void factory_reset();
#ifdef USE_TEXT_SENSOR
void set_direction_text_sensor(uint8_t target, text_sensor::TextSensor *s);
#endif
#ifdef USE_NUMBER
void set_zone_coordinate(uint8_t zone);
void set_zone_x1_number(uint8_t zone, number::Number *n);
void set_zone_y1_number(uint8_t zone, number::Number *n);
void set_zone_x2_number(uint8_t zone, number::Number *n);
void set_zone_y2_number(uint8_t zone, number::Number *n);
#endif
#ifdef USE_SENSOR
void set_move_x_sensor(uint8_t target, sensor::Sensor *s);
void set_move_y_sensor(uint8_t target, sensor::Sensor *s);
void set_move_speed_sensor(uint8_t target, sensor::Sensor *s);
void set_move_angle_sensor(uint8_t target, sensor::Sensor *s);
void set_move_distance_sensor(uint8_t target, sensor::Sensor *s);
void set_move_resolution_sensor(uint8_t target, sensor::Sensor *s);
void set_zone_target_count_sensor(uint8_t zone, sensor::Sensor *s);
void set_zone_still_target_count_sensor(uint8_t zone, sensor::Sensor *s);
void set_zone_moving_target_count_sensor(uint8_t zone, sensor::Sensor *s);
#endif
void reset_radar_zone();
void set_radar_zone(int32_t zone_type, int32_t zone1_x1, int32_t zone1_y1, int32_t zone1_x2, int32_t zone1_y2,
int32_t zone2_x1, int32_t zone2_y1, int32_t zone2_x2, int32_t zone2_y2, int32_t zone3_x1,
int32_t zone3_y1, int32_t zone3_x2, int32_t zone3_y2);
protected:
void send_command_(uint8_t command_str, const uint8_t *command_value, uint8_t command_value_len);
void set_config_mode_(bool enable);
void handle_periodic_data_(uint8_t *buffer, uint8_t len);
bool handle_ack_data_(uint8_t *buffer, uint8_t len);
void process_zone_(uint8_t *buffer);
void readline_(int readch, uint8_t *buffer, uint8_t len);
void get_version_();
void get_mac_();
void query_target_tracking_mode_();
void query_zone_();
void restart_();
void send_set_zone_command_();
void save_to_flash_(float value);
float restore_from_flash_();
bool get_timeout_status_(uint32_t check_millis);
uint8_t count_targets_in_zone_(const Zone &zone, bool is_moving);
Target target_info_[MAX_TARGETS];
Zone zone_config_[MAX_ZONES];
uint8_t buffer_pos_ = 0; // where to resume processing/populating buffer
uint8_t buffer_data_[MAX_LINE_LENGTH];
uint32_t last_periodic_millis_ = 0;
uint32_t presence_millis_ = 0;
uint32_t still_presence_millis_ = 0;
uint32_t moving_presence_millis_ = 0;
uint16_t throttle_ = 0;
uint16_t timeout_ = 5;
uint8_t zone_type_ = 0;
std::string version_{};
std::string mac_{};
#ifdef USE_NUMBER
ESPPreferenceObject pref_; // only used when numbers are in use
std::vector<number::Number *> zone_x1_numbers_ = std::vector<number::Number *>(MAX_ZONES);
std::vector<number::Number *> zone_y1_numbers_ = std::vector<number::Number *>(MAX_ZONES);
std::vector<number::Number *> zone_x2_numbers_ = std::vector<number::Number *>(MAX_ZONES);
std::vector<number::Number *> zone_y2_numbers_ = std::vector<number::Number *>(MAX_ZONES);
#endif
#ifdef USE_SENSOR
std::vector<sensor::Sensor *> move_x_sensors_ = std::vector<sensor::Sensor *>(MAX_TARGETS);
std::vector<sensor::Sensor *> move_y_sensors_ = std::vector<sensor::Sensor *>(MAX_TARGETS);
std::vector<sensor::Sensor *> move_speed_sensors_ = std::vector<sensor::Sensor *>(MAX_TARGETS);
std::vector<sensor::Sensor *> move_angle_sensors_ = std::vector<sensor::Sensor *>(MAX_TARGETS);
std::vector<sensor::Sensor *> move_distance_sensors_ = std::vector<sensor::Sensor *>(MAX_TARGETS);
std::vector<sensor::Sensor *> move_resolution_sensors_ = std::vector<sensor::Sensor *>(MAX_TARGETS);
std::vector<sensor::Sensor *> zone_target_count_sensors_ = std::vector<sensor::Sensor *>(MAX_ZONES);
std::vector<sensor::Sensor *> zone_still_target_count_sensors_ = std::vector<sensor::Sensor *>(MAX_ZONES);
std::vector<sensor::Sensor *> zone_moving_target_count_sensors_ = std::vector<sensor::Sensor *>(MAX_ZONES);
#endif
#ifdef USE_TEXT_SENSOR
std::vector<text_sensor::TextSensor *> direction_text_sensors_ = std::vector<text_sensor::TextSensor *>(3);
#endif
};
} // namespace ld2450
} // namespace esphome

View File

@ -0,0 +1,120 @@
import esphome.codegen as cg
from esphome.components import number
import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
DEVICE_CLASS_DISTANCE,
ENTITY_CATEGORY_CONFIG,
ICON_TIMELAPSE,
UNIT_MILLIMETER,
UNIT_SECOND,
)
from .. import CONF_LD2450_ID, LD2450Component, ld2450_ns
CONF_PRESENCE_TIMEOUT = "presence_timeout"
CONF_X1 = "x1"
CONF_X2 = "x2"
CONF_Y1 = "y1"
CONF_Y2 = "y2"
ICON_ARROW_BOTTOM_RIGHT = "mdi:arrow-bottom-right"
ICON_ARROW_BOTTOM_RIGHT_BOLD_BOX_OUTLINE = "mdi:arrow-bottom-right-bold-box-outline"
ICON_ARROW_TOP_LEFT = "mdi:arrow-top-left"
ICON_ARROW_TOP_LEFT_BOLD_BOX_OUTLINE = "mdi:arrow-top-left-bold-box-outline"
MAX_ZONES = 3
PresenceTimeoutNumber = ld2450_ns.class_("PresenceTimeoutNumber", number.Number)
ZoneCoordinateNumber = ld2450_ns.class_("ZoneCoordinateNumber", number.Number)
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_LD2450_ID): cv.use_id(LD2450Component),
cv.Required(CONF_PRESENCE_TIMEOUT): number.number_schema(
PresenceTimeoutNumber,
unit_of_measurement=UNIT_SECOND,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_TIMELAPSE,
),
}
)
CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
{
cv.Optional(f"zone_{n + 1}"): cv.Schema(
{
cv.Required(CONF_X1): number.number_schema(
ZoneCoordinateNumber,
device_class=DEVICE_CLASS_DISTANCE,
unit_of_measurement=UNIT_MILLIMETER,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_ARROW_TOP_LEFT_BOLD_BOX_OUTLINE,
),
cv.Required(CONF_Y1): number.number_schema(
ZoneCoordinateNumber,
device_class=DEVICE_CLASS_DISTANCE,
unit_of_measurement=UNIT_MILLIMETER,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_ARROW_TOP_LEFT,
),
cv.Required(CONF_X2): number.number_schema(
ZoneCoordinateNumber,
device_class=DEVICE_CLASS_DISTANCE,
unit_of_measurement=UNIT_MILLIMETER,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_ARROW_BOTTOM_RIGHT_BOLD_BOX_OUTLINE,
),
cv.Required(CONF_Y2): number.number_schema(
ZoneCoordinateNumber,
device_class=DEVICE_CLASS_DISTANCE,
unit_of_measurement=UNIT_MILLIMETER,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_ARROW_BOTTOM_RIGHT,
),
}
)
for n in range(MAX_ZONES)
}
)
async def to_code(config):
ld2450_component = await cg.get_variable(config[CONF_LD2450_ID])
if presence_timeout_config := config.get(CONF_PRESENCE_TIMEOUT):
n = await number.new_number(
presence_timeout_config,
min_value=0,
max_value=3600,
step=1,
)
await cg.register_parented(n, config[CONF_LD2450_ID])
cg.add(ld2450_component.set_presence_timeout_number(n))
for x in range(MAX_ZONES):
if zone_conf := config.get(f"zone_{x + 1}"):
if zone_x1_config := zone_conf.get(CONF_X1):
n = cg.new_Pvariable(zone_x1_config[CONF_ID], x)
await number.register_number(
n, zone_x1_config, min_value=-4860, max_value=4860, step=1
)
await cg.register_parented(n, config[CONF_LD2450_ID])
cg.add(ld2450_component.set_zone_x1_number(x, n))
if zone_y1_config := zone_conf.get(CONF_Y1):
n = cg.new_Pvariable(zone_y1_config[CONF_ID], x)
await number.register_number(
n, zone_y1_config, min_value=0, max_value=7560, step=1
)
await cg.register_parented(n, config[CONF_LD2450_ID])
cg.add(ld2450_component.set_zone_y1_number(x, n))
if zone_x2_config := zone_conf.get(CONF_X2):
n = cg.new_Pvariable(zone_x2_config[CONF_ID], x)
await number.register_number(
n, zone_x2_config, min_value=-4860, max_value=4860, step=1
)
await cg.register_parented(n, config[CONF_LD2450_ID])
cg.add(ld2450_component.set_zone_x2_number(x, n))
if zone_y2_config := zone_conf.get(CONF_Y2):
n = cg.new_Pvariable(zone_y2_config[CONF_ID], x)
await number.register_number(
n, zone_y2_config, min_value=0, max_value=7560, step=1
)
await cg.register_parented(n, config[CONF_LD2450_ID])
cg.add(ld2450_component.set_zone_y2_number(x, n))

View File

@ -0,0 +1,12 @@
#include "presence_timeout_number.h"
namespace esphome {
namespace ld2450 {
void PresenceTimeoutNumber::control(float value) {
this->publish_state(value);
this->parent_->set_presence_timeout();
}
} // namespace ld2450
} // namespace esphome

View File

@ -0,0 +1,18 @@
#pragma once
#include "esphome/components/number/number.h"
#include "../ld2450.h"
namespace esphome {
namespace ld2450 {
class PresenceTimeoutNumber : public number::Number, public Parented<LD2450Component> {
public:
PresenceTimeoutNumber() = default;
protected:
void control(float value) override;
};
} // namespace ld2450
} // namespace esphome

View File

@ -0,0 +1,14 @@
#include "zone_coordinate_number.h"
namespace esphome {
namespace ld2450 {
ZoneCoordinateNumber::ZoneCoordinateNumber(uint8_t zone) : zone_(zone) {}
void ZoneCoordinateNumber::control(float value) {
this->publish_state(value);
this->parent_->set_zone_coordinate(this->zone_);
}
} // namespace ld2450
} // namespace esphome

View File

@ -0,0 +1,19 @@
#pragma once
#include "esphome/components/number/number.h"
#include "../ld2450.h"
namespace esphome {
namespace ld2450 {
class ZoneCoordinateNumber : public number::Number, public Parented<LD2450Component> {
public:
ZoneCoordinateNumber(uint8_t zone);
protected:
uint8_t zone_;
void control(float value) override;
};
} // namespace ld2450
} // namespace esphome

View File

@ -0,0 +1,56 @@
import esphome.codegen as cg
from esphome.components import select
import esphome.config_validation as cv
from esphome.const import CONF_BAUD_RATE, ENTITY_CATEGORY_CONFIG, ICON_THERMOMETER
from .. import CONF_LD2450_ID, LD2450Component, ld2450_ns
CONF_ZONE_TYPE = "zone_type"
BaudRateSelect = ld2450_ns.class_("BaudRateSelect", select.Select)
ZoneTypeSelect = ld2450_ns.class_("ZoneTypeSelect", select.Select)
CONFIG_SCHEMA = {
cv.GenerateID(CONF_LD2450_ID): cv.use_id(LD2450Component),
cv.Optional(CONF_BAUD_RATE): select.select_schema(
BaudRateSelect,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_THERMOMETER,
),
cv.Optional(CONF_ZONE_TYPE): select.select_schema(
ZoneTypeSelect,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_THERMOMETER,
),
}
async def to_code(config):
ld2450_component = await cg.get_variable(config[CONF_LD2450_ID])
if baud_rate_config := config.get(CONF_BAUD_RATE):
s = await select.new_select(
baud_rate_config,
options=[
"9600",
"19200",
"38400",
"57600",
"115200",
"230400",
"256000",
"460800",
],
)
await cg.register_parented(s, config[CONF_LD2450_ID])
cg.add(ld2450_component.set_baud_rate_select(s))
if zone_type_config := config.get(CONF_ZONE_TYPE):
s = await select.new_select(
zone_type_config,
options=[
"Disabled",
"Detection",
"Filter",
],
)
await cg.register_parented(s, config[CONF_LD2450_ID])
cg.add(ld2450_component.set_zone_type_select(s))

View File

@ -0,0 +1,12 @@
#include "baud_rate_select.h"
namespace esphome {
namespace ld2450 {
void BaudRateSelect::control(const std::string &value) {
this->publish_state(value);
this->parent_->set_baud_rate(state);
}
} // namespace ld2450
} // namespace esphome

View File

@ -0,0 +1,18 @@
#pragma once
#include "esphome/components/select/select.h"
#include "../ld2450.h"
namespace esphome {
namespace ld2450 {
class BaudRateSelect : public select::Select, public Parented<LD2450Component> {
public:
BaudRateSelect() = default;
protected:
void control(const std::string &value) override;
};
} // namespace ld2450
} // namespace esphome

View File

@ -0,0 +1,12 @@
#include "zone_type_select.h"
namespace esphome {
namespace ld2450 {
void ZoneTypeSelect::control(const std::string &value) {
this->publish_state(value);
this->parent_->set_zone_type(state);
}
} // namespace ld2450
} // namespace esphome

View File

@ -0,0 +1,18 @@
#pragma once
#include "esphome/components/select/select.h"
#include "../ld2450.h"
namespace esphome {
namespace ld2450 {
class ZoneTypeSelect : public select::Select, public Parented<LD2450Component> {
public:
ZoneTypeSelect() = default;
protected:
void control(const std::string &value) override;
};
} // namespace ld2450
} // namespace esphome

View File

@ -0,0 +1,156 @@
import esphome.codegen as cg
from esphome.components import sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_ANGLE,
CONF_DISTANCE,
CONF_RESOLUTION,
CONF_SPEED,
DEVICE_CLASS_DISTANCE,
DEVICE_CLASS_SPEED,
UNIT_DEGREES,
UNIT_MILLIMETER,
)
from . import CONF_LD2450_ID, LD2450Component
DEPENDENCIES = ["ld2450"]
CONF_MOVING_TARGET_COUNT = "moving_target_count"
CONF_STILL_TARGET_COUNT = "still_target_count"
CONF_TARGET_COUNT = "target_count"
CONF_X = "x"
CONF_Y = "y"
ICON_ACCOUNT_GROUP = "mdi:account-group"
ICON_ACCOUNT_SWITCH = "mdi:account-switch"
ICON_ALPHA_X_BOX_OUTLINE = "mdi:alpha-x-box-outline"
ICON_ALPHA_Y_BOX_OUTLINE = "mdi:alpha-y-box-outline"
ICON_FORMAT_TEXT_ROTATION_ANGLE_UP = "mdi:format-text-rotation-angle-up"
ICON_HUMAN_GREETING_PROXIMITY = "mdi:human-greeting-proximity"
ICON_MAP_MARKER_ACCOUNT = "mdi:map-marker-account"
ICON_MAP_MARKER_DISTANCE = "mdi:map-marker-distance"
ICON_RELATION_ZERO_OR_ONE_TO_ZERO_OR_ONE = "mdi:relation-zero-or-one-to-zero-or-one"
ICON_SPEEDOMETER_SLOW = "mdi:speedometer-slow"
MAX_TARGETS = 3
MAX_ZONES = 3
UNIT_MILLIMETER_PER_SECOND = "mm/s"
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_LD2450_ID): cv.use_id(LD2450Component),
cv.Optional(CONF_TARGET_COUNT): sensor.sensor_schema(
icon=ICON_ACCOUNT_GROUP,
),
cv.Optional(CONF_STILL_TARGET_COUNT): sensor.sensor_schema(
icon=ICON_HUMAN_GREETING_PROXIMITY,
),
cv.Optional(CONF_MOVING_TARGET_COUNT): sensor.sensor_schema(
icon=ICON_ACCOUNT_SWITCH,
),
}
)
CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
{
cv.Optional(f"target_{n + 1}"): cv.Schema(
{
cv.Optional(CONF_X): sensor.sensor_schema(
device_class=DEVICE_CLASS_DISTANCE,
unit_of_measurement=UNIT_MILLIMETER,
icon=ICON_ALPHA_X_BOX_OUTLINE,
),
cv.Optional(CONF_Y): sensor.sensor_schema(
device_class=DEVICE_CLASS_DISTANCE,
unit_of_measurement=UNIT_MILLIMETER,
icon=ICON_ALPHA_Y_BOX_OUTLINE,
),
cv.Optional(CONF_SPEED): sensor.sensor_schema(
device_class=DEVICE_CLASS_SPEED,
unit_of_measurement=UNIT_MILLIMETER_PER_SECOND,
icon=ICON_SPEEDOMETER_SLOW,
),
cv.Optional(CONF_ANGLE): sensor.sensor_schema(
unit_of_measurement=UNIT_DEGREES,
icon=ICON_FORMAT_TEXT_ROTATION_ANGLE_UP,
),
cv.Optional(CONF_DISTANCE): sensor.sensor_schema(
device_class=DEVICE_CLASS_DISTANCE,
unit_of_measurement=UNIT_MILLIMETER,
icon=ICON_MAP_MARKER_DISTANCE,
),
cv.Optional(CONF_RESOLUTION): sensor.sensor_schema(
device_class=DEVICE_CLASS_DISTANCE,
unit_of_measurement=UNIT_MILLIMETER,
icon=ICON_RELATION_ZERO_OR_ONE_TO_ZERO_OR_ONE,
),
}
)
for n in range(MAX_TARGETS)
},
{
cv.Optional(f"zone_{n + 1}"): cv.Schema(
{
cv.Optional(CONF_TARGET_COUNT): sensor.sensor_schema(
icon=ICON_MAP_MARKER_ACCOUNT,
),
cv.Optional(CONF_STILL_TARGET_COUNT): sensor.sensor_schema(
icon=ICON_MAP_MARKER_ACCOUNT,
),
cv.Optional(CONF_MOVING_TARGET_COUNT): sensor.sensor_schema(
icon=ICON_MAP_MARKER_ACCOUNT,
),
}
)
for n in range(MAX_ZONES)
},
)
async def to_code(config):
ld2450_component = await cg.get_variable(config[CONF_LD2450_ID])
if target_count_config := config.get(CONF_TARGET_COUNT):
sens = await sensor.new_sensor(target_count_config)
cg.add(ld2450_component.set_target_count_sensor(sens))
if still_target_count_config := config.get(CONF_STILL_TARGET_COUNT):
sens = await sensor.new_sensor(still_target_count_config)
cg.add(ld2450_component.set_still_target_count_sensor(sens))
if moving_target_count_config := config.get(CONF_MOVING_TARGET_COUNT):
sens = await sensor.new_sensor(moving_target_count_config)
cg.add(ld2450_component.set_moving_target_count_sensor(sens))
for n in range(MAX_TARGETS):
if target_conf := config.get(f"target_{n + 1}"):
if x_config := target_conf.get(CONF_X):
sens = await sensor.new_sensor(x_config)
cg.add(ld2450_component.set_move_x_sensor(n, sens))
if y_config := target_conf.get(CONF_Y):
sens = await sensor.new_sensor(y_config)
cg.add(ld2450_component.set_move_y_sensor(n, sens))
if speed_config := target_conf.get(CONF_SPEED):
sens = await sensor.new_sensor(speed_config)
cg.add(ld2450_component.set_move_speed_sensor(n, sens))
if angle_config := target_conf.get(CONF_ANGLE):
sens = await sensor.new_sensor(angle_config)
cg.add(ld2450_component.set_move_angle_sensor(n, sens))
if distance_config := target_conf.get(CONF_DISTANCE):
sens = await sensor.new_sensor(distance_config)
cg.add(ld2450_component.set_move_distance_sensor(n, sens))
if resolution_config := target_conf.get(CONF_RESOLUTION):
sens = await sensor.new_sensor(resolution_config)
cg.add(ld2450_component.set_move_resolution_sensor(n, sens))
for n in range(MAX_ZONES):
if zone_config := config.get(f"zone_{n + 1}"):
if target_count_config := zone_config.get(CONF_TARGET_COUNT):
sens = await sensor.new_sensor(target_count_config)
cg.add(ld2450_component.set_zone_target_count_sensor(n, sens))
if still_target_count_config := zone_config.get(CONF_STILL_TARGET_COUNT):
sens = await sensor.new_sensor(still_target_count_config)
cg.add(ld2450_component.set_zone_still_target_count_sensor(n, sens))
if moving_target_count_config := zone_config.get(CONF_MOVING_TARGET_COUNT):
sens = await sensor.new_sensor(moving_target_count_config)
cg.add(ld2450_component.set_zone_moving_target_count_sensor(n, sens))

View File

@ -0,0 +1,45 @@
import esphome.codegen as cg
from esphome.components import switch
import esphome.config_validation as cv
from esphome.const import (
DEVICE_CLASS_SWITCH,
ENTITY_CATEGORY_CONFIG,
ICON_BLUETOOTH,
ICON_PULSE,
)
from .. import CONF_LD2450_ID, LD2450Component, ld2450_ns
BluetoothSwitch = ld2450_ns.class_("BluetoothSwitch", switch.Switch)
MultiTargetSwitch = ld2450_ns.class_("MultiTargetSwitch", switch.Switch)
CONF_BLUETOOTH = "bluetooth"
CONF_MULTI_TARGET = "multi_target"
CONFIG_SCHEMA = {
cv.GenerateID(CONF_LD2450_ID): cv.use_id(LD2450Component),
cv.Optional(CONF_BLUETOOTH): switch.switch_schema(
BluetoothSwitch,
device_class=DEVICE_CLASS_SWITCH,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_BLUETOOTH,
),
cv.Optional(CONF_MULTI_TARGET): switch.switch_schema(
MultiTargetSwitch,
device_class=DEVICE_CLASS_SWITCH,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_PULSE,
),
}
async def to_code(config):
ld2450_component = await cg.get_variable(config[CONF_LD2450_ID])
if bluetooth_config := config.get(CONF_BLUETOOTH):
s = await switch.new_switch(bluetooth_config)
await cg.register_parented(s, config[CONF_LD2450_ID])
cg.add(ld2450_component.set_bluetooth_switch(s))
if multi_target_config := config.get(CONF_MULTI_TARGET):
s = await switch.new_switch(multi_target_config)
await cg.register_parented(s, config[CONF_LD2450_ID])
cg.add(ld2450_component.set_multi_target_switch(s))

View File

@ -0,0 +1,12 @@
#include "bluetooth_switch.h"
namespace esphome {
namespace ld2450 {
void BluetoothSwitch::write_state(bool state) {
this->publish_state(state);
this->parent_->set_bluetooth(state);
}
} // namespace ld2450
} // namespace esphome

View File

@ -0,0 +1,18 @@
#pragma once
#include "esphome/components/switch/switch.h"
#include "../ld2450.h"
namespace esphome {
namespace ld2450 {
class BluetoothSwitch : public switch_::Switch, public Parented<LD2450Component> {
public:
BluetoothSwitch() = default;
protected:
void write_state(bool state) override;
};
} // namespace ld2450
} // namespace esphome

View File

@ -0,0 +1,12 @@
#include "multi_target_switch.h"
namespace esphome {
namespace ld2450 {
void MultiTargetSwitch::write_state(bool state) {
this->publish_state(state);
this->parent_->set_multi_target(state);
}
} // namespace ld2450
} // namespace esphome

View File

@ -0,0 +1,18 @@
#pragma once
#include "esphome/components/switch/switch.h"
#include "../ld2450.h"
namespace esphome {
namespace ld2450 {
class MultiTargetSwitch : public switch_::Switch, public Parented<LD2450Component> {
public:
MultiTargetSwitch() = default;
protected:
void write_state(bool state) override;
};
} // namespace ld2450
} // namespace esphome

View File

@ -0,0 +1,62 @@
import esphome.codegen as cg
from esphome.components import text_sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_DIRECTION,
CONF_MAC_ADDRESS,
CONF_VERSION,
ENTITY_CATEGORY_DIAGNOSTIC,
ENTITY_CATEGORY_NONE,
ICON_BLUETOOTH,
ICON_CHIP,
ICON_SIGN_DIRECTION,
)
from . import CONF_LD2450_ID, LD2450Component
DEPENDENCIES = ["ld2450"]
MAX_TARGETS = 3
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_LD2450_ID): cv.use_id(LD2450Component),
cv.Optional(CONF_VERSION): text_sensor.text_sensor_schema(
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
icon=ICON_CHIP,
),
cv.Optional(CONF_MAC_ADDRESS): text_sensor.text_sensor_schema(
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
icon=ICON_BLUETOOTH,
),
}
)
CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
{
cv.Optional(f"target_{n + 1}"): cv.Schema(
{
cv.Optional(CONF_DIRECTION): text_sensor.text_sensor_schema(
entity_category=ENTITY_CATEGORY_NONE,
icon=ICON_SIGN_DIRECTION,
),
}
)
for n in range(MAX_TARGETS)
}
)
async def to_code(config):
ld2450_component = await cg.get_variable(config[CONF_LD2450_ID])
if version_config := config.get(CONF_VERSION):
sens = await text_sensor.new_text_sensor(version_config)
cg.add(ld2450_component.set_version_text_sensor(sens))
if mac_address_config := config.get(CONF_MAC_ADDRESS):
sens = await text_sensor.new_text_sensor(mac_address_config)
cg.add(ld2450_component.set_mac_text_sensor(sens))
for n in range(MAX_TARGETS):
if direction_conf := config.get(f"target_{n + 1}"):
if direction_config := direction_conf.get(CONF_DIRECTION):
sens = await text_sensor.new_text_sensor(direction_config)
cg.add(ld2450_component.set_direction_text_sensor(n, sens))

View File

@ -35,7 +35,7 @@ from esphome.const import (
PLATFORM_RP2040,
PLATFORM_RTL87XX,
)
from esphome.core import CORE, EsphomeError, Lambda, coroutine_with_priority
from esphome.core import CORE, Lambda, coroutine_with_priority
CODEOWNERS = ["@esphome/core"]
logger_ns = cg.esphome_ns.namespace("logger")
@ -77,6 +77,9 @@ USB_SERIAL_JTAG = "USB_SERIAL_JTAG"
USB_CDC = "USB_CDC"
DEFAULT = "DEFAULT"
CONF_INITIAL_LEVEL = "initial_level"
CONF_LOGGER_ID = "logger_id"
UART_SELECTION_ESP32 = {
VARIANT_ESP32: [UART0, UART1, UART2],
VARIANT_ESP32S2: [UART0, UART1, USB_CDC],
@ -154,11 +157,11 @@ def uart_selection(value):
def validate_local_no_higher_than_global(value):
global_level = value.get(CONF_LEVEL, "DEBUG")
global_level = LOG_LEVEL_SEVERITY.index(value[CONF_LEVEL])
for tag, level in value.get(CONF_LOGS, {}).items():
if LOG_LEVEL_SEVERITY.index(level) > LOG_LEVEL_SEVERITY.index(global_level):
raise EsphomeError(
f"The local log level {level} for {tag} must be less severe than the global log level {global_level}."
if LOG_LEVEL_SEVERITY.index(level) > global_level:
raise cv.Invalid(
f"The configured log level for {tag} ({level}) must be no more severe than the global log level {value[CONF_LEVEL]}."
)
return value
@ -209,6 +212,7 @@ CONFIG_SCHEMA = cv.All(
cv.string: is_log_level,
}
),
cv.Optional(CONF_INITIAL_LEVEL): is_log_level,
cv.Optional(CONF_ON_MESSAGE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LoggerMessageTrigger),
@ -227,7 +231,14 @@ CONFIG_SCHEMA = cv.All(
@coroutine_with_priority(90.0)
async def to_code(config):
baud_rate = config[CONF_BAUD_RATE]
log = cg.new_Pvariable(config[CONF_ID], baud_rate, config[CONF_TX_BUFFER_SIZE])
level = config[CONF_LEVEL]
initial_level = LOG_LEVELS[config.get(CONF_INITIAL_LEVEL, level)]
log = cg.new_Pvariable(
config[CONF_ID],
baud_rate,
config[CONF_TX_BUFFER_SIZE],
)
cg.add(log.set_log_level(initial_level))
if CONF_HARDWARE_UART in config:
cg.add(
log.set_uart_selection(
@ -236,10 +247,9 @@ async def to_code(config):
)
cg.add(log.pre_setup())
for tag, level in config[CONF_LOGS].items():
cg.add(log.set_log_level(tag, LOG_LEVELS[level]))
for tag, log_level in config[CONF_LOGS].items():
cg.add(log.set_log_level(tag, LOG_LEVELS[log_level]))
level = config[CONF_LEVEL]
cg.add_define("USE_LOGGER")
this_severity = LOG_LEVEL_SEVERITY.index(level)
cg.add_build_flag(f"-DESPHOME_LOG_LEVEL={LOG_LEVELS[level]}")
@ -367,3 +377,27 @@ async def logger_log_action_to_code(config, action_id, template_arg, args):
lambda_ = await cg.process_lambda(Lambda(text), args, return_type=cg.void)
return cg.new_Pvariable(action_id, template_arg, lambda_)
@automation.register_action(
"logger.set_level",
LambdaAction,
cv.maybe_simple_value(
{
cv.GenerateID(CONF_LOGGER_ID): cv.use_id(Logger),
cv.Required(CONF_LEVEL): is_log_level,
cv.Optional(CONF_TAG): cv.string,
},
key=CONF_LEVEL,
),
)
async def logger_set_level_to_code(config, action_id, template_arg, args):
level = LOG_LEVELS[config[CONF_LEVEL]]
logger = await cg.get_variable(config[CONF_LOGGER_ID])
if tag := config.get(CONF_TAG):
text = str(cg.statement(logger.set_log_level(tag, level)))
else:
text = str(cg.statement(logger.set_log_level(level)))
lambda_ = await cg.process_lambda(Lambda(text), args, return_type=cg.void)
return cg.new_Pvariable(action_id, template_arg, lambda_)

View File

@ -102,15 +102,9 @@ void Logger::log_vprintf_(int level, const char *tag, int line, const __FlashStr
#endif
int HOT Logger::level_for(const char *tag) {
// Uses std::vector<> for low memory footprint, though the vector
// could be sorted to minimize lookup times. This feature isn't used that
// much anyway so it doesn't matter too much.
for (auto &it : this->log_levels_) {
if (it.tag == tag) {
return it.level;
}
}
return ESPHOME_LOG_LEVEL;
if (this->log_levels_.count(tag) != 0)
return this->log_levels_[tag];
return this->current_level_;
}
void HOT Logger::log_message_(int level, const char *tag, int offset) {
@ -167,9 +161,7 @@ void Logger::loop() {
#endif
void Logger::set_baud_rate(uint32_t baud_rate) { this->baud_rate_ = baud_rate; }
void Logger::set_log_level(const std::string &tag, int log_level) {
this->log_levels_.push_back(LogLevelOverride{tag, log_level});
}
void Logger::set_log_level(const std::string &tag, int log_level) { this->log_levels_[tag] = log_level; }
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY)
UARTSelection Logger::get_uart() const { return this->uart_; }
@ -183,18 +175,28 @@ const char *const LOG_LEVELS[] = {"NONE", "ERROR", "WARN", "INFO", "CONFIG", "DE
void Logger::dump_config() {
ESP_LOGCONFIG(TAG, "Logger:");
ESP_LOGCONFIG(TAG, " Level: %s", LOG_LEVELS[ESPHOME_LOG_LEVEL]);
ESP_LOGCONFIG(TAG, " Max Level: %s", LOG_LEVELS[ESPHOME_LOG_LEVEL]);
ESP_LOGCONFIG(TAG, " Initial Level: %s", LOG_LEVELS[this->current_level_]);
#ifndef USE_HOST
ESP_LOGCONFIG(TAG, " Log Baud Rate: %" PRIu32, this->baud_rate_);
ESP_LOGCONFIG(TAG, " Hardware UART: %s", get_uart_selection_());
#endif
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.first.c_str(), LOG_LEVELS[it.second]);
}
}
void Logger::write_footer_() { this->write_to_buffer_(ESPHOME_LOG_RESET_COLOR, strlen(ESPHOME_LOG_RESET_COLOR)); }
void Logger::set_log_level(int level) {
if (level > ESPHOME_LOG_LEVEL) {
level = ESPHOME_LOG_LEVEL;
ESP_LOGW(TAG, "Cannot set log level higher than pre-compiled %s", LOG_LEVELS[ESPHOME_LOG_LEVEL]);
}
this->current_level_ = level;
this->level_callback_.call(level);
}
Logger *global_logger = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
} // namespace logger

View File

@ -1,11 +1,12 @@
#pragma once
#include <cstdarg>
#include <vector>
#include <map>
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#ifdef USE_ARDUINO
#if defined(USE_ESP8266) || defined(USE_ESP32)
@ -74,8 +75,11 @@ class Logger : public Component {
UARTSelection get_uart() const;
#endif
/// Set the default log level for this logger.
void set_log_level(int level);
/// Set the log level of the specified tag.
void set_log_level(const std::string &tag, int log_level);
int get_log_level() { return this->current_level_; }
// ========== INTERNAL METHODS ==========
// (In most use cases you won't need these)
@ -88,6 +92,9 @@ class Logger : public Component {
/// Register a callback that will be called for every log message sent
void add_on_log_callback(std::function<void(int, const char *, const char *)> &&callback);
// add a listener for log level changes
void add_listener(std::function<void(int)> &&callback) { this->level_callback_.add(std::move(callback)); }
float get_setup_priority() const override;
void log_vprintf_(int level, const char *tag, int line, const char *format, va_list args); // NOLINT
@ -159,17 +166,14 @@ class Logger : public Component {
#ifdef USE_ESP_IDF
uart_port_t uart_num_;
#endif
struct LogLevelOverride {
std::string tag;
int level;
};
std::vector<LogLevelOverride> log_levels_;
std::map<std::string, int> log_levels_{};
CallbackManager<void(int, const char *, const char *)> log_callback_{};
int current_level_{ESPHOME_LOG_LEVEL_VERY_VERBOSE};
/// Prevents recursive log calls, if true a log message is already being processed.
bool recursion_guard_ = false;
void *main_task_ = nullptr;
CallbackManager<void(int)> level_callback_{};
};
extern Logger *global_logger; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
class LoggerMessageTrigger : public Trigger<int, const char *, const char *> {

View File

@ -0,0 +1,29 @@
import esphome.codegen as cg
from esphome.components import select
import esphome.config_validation as cv
from esphome.const import CONF_LEVEL, CONF_LOGGER, ENTITY_CATEGORY_CONFIG, ICON_BUG
from esphome.core import CORE
from esphome.cpp_helpers import register_component, register_parented
from .. import CONF_LOGGER_ID, LOG_LEVEL_SEVERITY, Logger, logger_ns
CODEOWNERS = ["@clydebarrow"]
LoggerLevelSelect = logger_ns.class_("LoggerLevelSelect", select.Select, cg.Component)
CONFIG_SCHEMA = select.select_schema(
LoggerLevelSelect, icon=ICON_BUG, entity_category=ENTITY_CATEGORY_CONFIG
).extend(
{
cv.GenerateID(CONF_LOGGER_ID): cv.use_id(Logger),
}
)
async def to_code(config):
levels = LOG_LEVEL_SEVERITY
index = levels.index(CORE.config[CONF_LOGGER][CONF_LEVEL])
levels = levels[: index + 1]
var = await select.new_select(config, options=levels)
await register_parented(var, config[CONF_LOGGER_ID])
await register_component(var, config)

View File

@ -0,0 +1,27 @@
#include "logger_level_select.h"
namespace esphome {
namespace logger {
void LoggerLevelSelect::publish_state(int level) {
auto value = this->at(level);
if (!value) {
return;
}
Select::publish_state(value.value());
}
void LoggerLevelSelect::setup() {
this->parent_->add_listener([this](int level) { this->publish_state(level); });
this->publish_state(this->parent_->get_log_level());
}
void LoggerLevelSelect::control(const std::string &value) {
auto level = this->index_of(value);
if (!level)
return;
this->parent_->set_log_level(level.value());
}
} // namespace logger
} // namespace esphome

View File

@ -0,0 +1,15 @@
#pragma once
#include "esphome/components/select/select.h"
#include "esphome/core/component.h"
#include "esphome/components/logger/logger.h"
namespace esphome {
namespace logger {
class LoggerLevelSelect : public Component, public select::Select, public Parented<Logger> {
public:
void publish_state(int level);
void setup() override;
void control(const std::string &value) override;
};
} // namespace logger
} // namespace esphome

View File

@ -1,5 +1,4 @@
from esphome import automation
from esphome.automation import maybe_simple_id
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import (
@ -21,6 +20,16 @@ media_player_ns = cg.esphome_ns.namespace("media_player")
MediaPlayer = media_player_ns.class_("MediaPlayer")
MediaPlayerSupportedFormat = media_player_ns.struct("MediaPlayerSupportedFormat")
MediaPlayerFormatPurpose = media_player_ns.enum(
"MediaPlayerFormatPurpose", is_class=True
)
MEDIA_PLAYER_FORMAT_PURPOSE_ENUM = {
"default": MediaPlayerFormatPurpose.PURPOSE_DEFAULT,
"announcement": MediaPlayerFormatPurpose.PURPOSE_ANNOUNCEMENT,
}
PlayAction = media_player_ns.class_(
"PlayAction", automation.Action, cg.Parented.template(MediaPlayer)
@ -47,7 +56,7 @@ VolumeSetAction = media_player_ns.class_(
"VolumeSetAction", automation.Action, cg.Parented.template(MediaPlayer)
)
CONF_ANNOUNCEMENT = "announcement"
CONF_ON_PLAY = "on_play"
CONF_ON_PAUSE = "on_pause"
CONF_ON_ANNOUNCEMENT = "on_announcement"
@ -125,7 +134,16 @@ MEDIA_PLAYER_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(
)
MEDIA_PLAYER_ACTION_SCHEMA = maybe_simple_id({cv.GenerateID(): cv.use_id(MediaPlayer)})
MEDIA_PLAYER_ACTION_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.use_id(MediaPlayer),
cv.Optional(CONF_ANNOUNCEMENT, default=False): cv.templatable(cv.boolean),
}
)
MEDIA_PLAYER_CONDITION_SCHEMA = automation.maybe_simple_id(
{cv.GenerateID(): cv.use_id(MediaPlayer)}
)
@automation.register_action(
@ -135,6 +153,7 @@ MEDIA_PLAYER_ACTION_SCHEMA = maybe_simple_id({cv.GenerateID(): cv.use_id(MediaPl
{
cv.GenerateID(): cv.use_id(MediaPlayer),
cv.Required(CONF_MEDIA_URL): cv.templatable(cv.url),
cv.Optional(CONF_ANNOUNCEMENT, default=False): cv.templatable(cv.boolean),
},
key=CONF_MEDIA_URL,
),
@ -143,7 +162,9 @@ async def media_player_play_media_action(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
media_url = await cg.templatable(config[CONF_MEDIA_URL], args, cg.std_string)
announcement = await cg.templatable(config[CONF_ANNOUNCEMENT], args, cg.bool_)
cg.add(var.set_media_url(media_url))
cg.add(var.set_announcement(announcement))
return var
@ -161,19 +182,27 @@ async def media_player_play_media_action(config, action_id, template_arg, args):
@automation.register_action(
"media_player.volume_down", VolumeDownAction, MEDIA_PLAYER_ACTION_SCHEMA
)
@automation.register_condition(
"media_player.is_idle", IsIdleCondition, MEDIA_PLAYER_ACTION_SCHEMA
)
@automation.register_condition(
"media_player.is_paused", IsPausedCondition, MEDIA_PLAYER_ACTION_SCHEMA
)
@automation.register_condition(
"media_player.is_playing", IsPlayingCondition, MEDIA_PLAYER_ACTION_SCHEMA
)
@automation.register_condition(
"media_player.is_announcing", IsAnnouncingCondition, MEDIA_PLAYER_ACTION_SCHEMA
)
async def media_player_action(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
announcement = await cg.templatable(config[CONF_ANNOUNCEMENT], args, cg.bool_)
cg.add(var.set_announcement(announcement))
return var
@automation.register_condition(
"media_player.is_idle", IsIdleCondition, MEDIA_PLAYER_CONDITION_SCHEMA
)
@automation.register_condition(
"media_player.is_paused", IsPausedCondition, MEDIA_PLAYER_CONDITION_SCHEMA
)
@automation.register_condition(
"media_player.is_playing", IsPlayingCondition, MEDIA_PLAYER_CONDITION_SCHEMA
)
@automation.register_condition(
"media_player.is_announcing", IsAnnouncingCondition, MEDIA_PLAYER_CONDITION_SCHEMA
)
async def media_player_condition(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
return var

View File

@ -10,7 +10,10 @@ namespace media_player {
template<MediaPlayerCommand Command, typename... Ts>
class MediaPlayerCommandAction : public Action<Ts...>, public Parented<MediaPlayer> {
public:
void play(Ts... x) override { this->parent_->make_call().set_command(Command).perform(); }
TEMPLATABLE_VALUE(bool, announcement);
void play(Ts... x) override {
this->parent_->make_call().set_command(Command).set_announcement(this->announcement_.value(x...)).perform();
}
};
template<typename... Ts>
@ -28,7 +31,13 @@ using VolumeDownAction = MediaPlayerCommandAction<MediaPlayerCommand::MEDIA_PLAY
template<typename... Ts> class PlayMediaAction : public Action<Ts...>, public Parented<MediaPlayer> {
TEMPLATABLE_VALUE(std::string, media_url)
void play(Ts... x) override { this->parent_->make_call().set_media_url(this->media_url_.value(x...)).perform(); }
TEMPLATABLE_VALUE(bool, announcement)
void play(Ts... x) override {
this->parent_->make_call()
.set_media_url(this->media_url_.value(x...))
.set_announcement(this->announcement_.value(x...))
.perform();
}
};
template<typename... Ts> class VolumeSetAction : public Action<Ts...>, public Parented<MediaPlayer> {

View File

@ -41,6 +41,14 @@ const char *media_player_command_to_string(MediaPlayerCommand command) {
return "VOLUME_UP";
case MEDIA_PLAYER_COMMAND_VOLUME_DOWN:
return "VOLUME_DOWN";
case MEDIA_PLAYER_COMMAND_ENQUEUE:
return "ENQUEUE";
case MEDIA_PLAYER_COMMAND_REPEAT_ONE:
return "REPEAT_ONE";
case MEDIA_PLAYER_COMMAND_REPEAT_OFF:
return "REPEAT_OFF";
case MEDIA_PLAYER_COMMAND_CLEAR_PLAYLIST:
return "CLEAR_PLAYLIST";
default:
return "UNKNOWN";
}

View File

@ -24,6 +24,10 @@ enum MediaPlayerCommand : uint8_t {
MEDIA_PLAYER_COMMAND_TOGGLE = 5,
MEDIA_PLAYER_COMMAND_VOLUME_UP = 6,
MEDIA_PLAYER_COMMAND_VOLUME_DOWN = 7,
MEDIA_PLAYER_COMMAND_ENQUEUE = 8,
MEDIA_PLAYER_COMMAND_REPEAT_ONE = 9,
MEDIA_PLAYER_COMMAND_REPEAT_OFF = 10,
MEDIA_PLAYER_COMMAND_CLEAR_PLAYLIST = 11,
};
const char *media_player_command_to_string(MediaPlayerCommand command);
@ -72,10 +76,10 @@ class MediaPlayerCall {
void perform();
const optional<MediaPlayerCommand> &get_command() const { return command_; }
const optional<std::string> &get_media_url() const { return media_url_; }
const optional<float> &get_volume() const { return volume_; }
const optional<bool> &get_announcement() const { return announcement_; }
const optional<MediaPlayerCommand> &get_command() const { return this->command_; }
const optional<std::string> &get_media_url() const { return this->media_url_; }
const optional<float> &get_volume() const { return this->volume_; }
const optional<bool> &get_announcement() const { return this->announcement_; }
protected:
void validate_();

View File

@ -1,8 +1,6 @@
#include "modbus_textsensor.h"
#include "esphome/core/log.h"
#include <iomanip>
#include <sstream>
namespace esphome {
namespace modbus_controller {
@ -12,20 +10,17 @@ static const char *const TAG = "modbus_controller.text_sensor";
void ModbusTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Modbus Controller Text Sensor", this); }
void ModbusTextSensor::parse_and_publish(const std::vector<uint8_t> &data) {
std::ostringstream output;
std::string output_str{};
uint8_t items_left = this->response_bytes;
uint8_t index = this->offset;
char buffer[5];
while ((items_left > 0) && index < data.size()) {
uint8_t b = data[index];
switch (this->encode_) {
case RawEncoding::HEXBYTES:
sprintf(buffer, "%02x", b);
output << buffer;
output_str += str_snprintf("%02x", 2, b);
break;
case RawEncoding::COMMA:
sprintf(buffer, index != this->offset ? ",%d" : "%d", b);
output << buffer;
output_str += str_sprintf(index != this->offset ? ",%d" : "%d", b);
break;
case RawEncoding::ANSI:
if (b < 0x20)
@ -33,25 +28,24 @@ void ModbusTextSensor::parse_and_publish(const std::vector<uint8_t> &data) {
// FALLTHROUGH
// Anything else no encoding
default:
output << (char) b;
output_str += (char) b;
break;
}
items_left--;
index++;
}
auto result = output.str();
// Is there a lambda registered
// call it with the pre converted value and the raw data array
if (this->transform_func_.has_value()) {
// the lambda can parse the response itself
auto val = (*this->transform_func_)(this, result, data);
auto val = (*this->transform_func_)(this, output_str, data);
if (val.has_value()) {
ESP_LOGV(TAG, "Value overwritten by lambda");
result = val.value();
output_str = val.value();
}
}
this->publish_state(result);
this->publish_state(output_str);
}
} // namespace modbus_controller

View File

@ -36,6 +36,7 @@ from esphome.const import (
CONF_PAYLOAD_AVAILABLE,
CONF_PAYLOAD_NOT_AVAILABLE,
CONF_PORT,
CONF_PUBLISH_NAN_AS_NONE,
CONF_QOS,
CONF_REBOOT_TIMEOUT,
CONF_RETAIN,
@ -49,7 +50,6 @@ from esphome.const import (
CONF_USE_ABBREVIATIONS,
CONF_USERNAME,
CONF_WILL_MESSAGE,
CONF_PUBLISH_NAN_AS_NONE,
PLATFORM_BK72XX,
PLATFORM_ESP32,
PLATFORM_ESP8266,
@ -406,7 +406,7 @@ async def to_code(config):
if CONF_SSL_FINGERPRINTS in config:
for fingerprint in config[CONF_SSL_FINGERPRINTS]:
arr = [
cg.RawExpression(f"0x{fingerprint[i:i + 2]}") for i in range(0, 40, 2)
cg.RawExpression(f"0x{fingerprint[i : i + 2]}") for i in range(0, 40, 2)
]
cg.add(var.add_ssl_fingerprint(arr))
cg.add_build_flag("-DASYNC_TCP_SSL_ENABLED=1")

View File

@ -1,9 +1,10 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import binary_sensor
import esphome.config_validation as cv
from esphome.const import CONF_UID
from esphome.core import HexInt
from .. import nfc_ns, Nfcc, NfcTagListener
from .. import Nfcc, NfcTagListener, nfc_ns
DEPENDENCIES = ["nfc"]
@ -25,8 +26,7 @@ def validate_uid(value):
for x in value.split("-"):
if len(x) != 2:
raise cv.Invalid(
"Each part (separated by '-') of the UID must be two characters "
"long."
"Each part (separated by '-') of the UID must be two characters long."
)
try:
x = int(x, 16)

View File

@ -66,7 +66,7 @@ class JPEGFormat(Format):
def actions(self):
cg.add_define("USE_ONLINE_IMAGE_JPEG_SUPPORT")
cg.add_library("JPEGDEC", "1.6.2", "https://github.com/bitbank2/JPEGDEC")
cg.add_library("JPEGDEC", None, "https://github.com/bitbank2/JPEGDEC#ca1e0f2")
class PNGFormat(Format):

View File

@ -9,10 +9,10 @@ namespace online_image {
static const char *const TAG = "online_image.decoder";
bool ImageDecoder::set_size(int width, int height) {
bool resized = this->image_->resize_(width, height);
bool success = this->image_->resize_(width, height) > 0;
this->x_scale_ = static_cast<double>(this->image_->buffer_width_) / width;
this->y_scale_ = static_cast<double>(this->image_->buffer_height_) / height;
return resized;
return success;
}
void ImageDecoder::draw(int x, int y, int w, int h, const Color &color) {
@ -25,6 +25,15 @@ void ImageDecoder::draw(int x, int y, int w, int h, const Color &color) {
}
}
DownloadBuffer::DownloadBuffer(size_t size) : size_(size) {
this->buffer_ = this->allocator_.allocate(size);
this->reset();
if (!this->buffer_) {
ESP_LOGE(TAG, "Initial allocation of download buffer failed!");
this->size_ = 0;
}
}
uint8_t *DownloadBuffer::data(size_t offset) {
if (offset > this->size_) {
ESP_LOGE(TAG, "Tried to access beyond download buffer bounds!!!");
@ -42,16 +51,20 @@ size_t DownloadBuffer::read(size_t len) {
}
size_t DownloadBuffer::resize(size_t size) {
if (this->size_ == size) {
return size;
if (this->size_ >= size) {
// Avoid useless reallocations; if the buffer is big enough, don't reallocate.
return this->size_;
}
this->allocator_.deallocate(this->buffer_, this->size_);
this->size_ = size;
this->buffer_ = this->allocator_.allocate(size);
this->reset();
if (this->buffer_) {
this->size_ = size;
return size;
} else {
ESP_LOGE(TAG, "allocation of %zu bytes failed. Biggest block in heap: %zu Bytes", size,
this->allocator_.get_max_free_block_size());
this->size_ = 0;
return 0;
}
}

View File

@ -29,8 +29,12 @@ class ImageDecoder {
* @brief Initialize the decoder.
*
* @param download_size The total number of bytes that need to be downloaded for the image.
* @return int Returns 0 on success, a {@see DecodeError} value in case of an error.
*/
virtual void prepare(size_t download_size) { this->download_size_ = download_size; }
virtual int prepare(size_t download_size) {
this->download_size_ = download_size;
return 0;
}
/**
* @brief Decode a part of the image. It will try reading from the buffer.
@ -83,10 +87,7 @@ class ImageDecoder {
class DownloadBuffer {
public:
DownloadBuffer(size_t size) : size_(size) {
this->buffer_ = this->allocator_.allocate(size);
this->reset();
}
DownloadBuffer(size_t size);
virtual ~DownloadBuffer() { this->allocator_.deallocate(this->buffer_, this->size_); }

View File

@ -41,13 +41,14 @@ static int draw_callback(JPEGDRAW *jpeg) {
return 1;
}
void JpegDecoder::prepare(size_t download_size) {
int JpegDecoder::prepare(size_t download_size) {
ImageDecoder::prepare(download_size);
auto size = this->image_->resize_download_buffer(download_size);
if (size < download_size) {
ESP_LOGE(TAG, "Resize failed!");
// TODO: return an error code;
ESP_LOGE(TAG, "Download buffer resize failed!");
return DECODE_ERROR_OUT_OF_MEMORY;
}
return 0;
}
int HOT JpegDecoder::decode(uint8_t *buffer, size_t size) {
@ -57,7 +58,7 @@ int HOT JpegDecoder::decode(uint8_t *buffer, size_t size) {
}
if (!this->jpeg_.openRAM(buffer, size, draw_callback)) {
ESP_LOGE(TAG, "Could not open image for decoding.");
ESP_LOGE(TAG, "Could not open image for decoding: %d", this->jpeg_.getLastError());
return DECODE_ERROR_INVALID_TYPE;
}
auto jpeg_type = this->jpeg_.getJPEGType();
@ -72,7 +73,9 @@ int HOT JpegDecoder::decode(uint8_t *buffer, size_t size) {
this->jpeg_.setUserPointer(this);
this->jpeg_.setPixelType(RGB8888);
this->set_size(this->jpeg_.getWidth(), this->jpeg_.getHeight());
if (!this->set_size(this->jpeg_.getWidth(), this->jpeg_.getHeight())) {
return DECODE_ERROR_OUT_OF_MEMORY;
}
if (!this->jpeg_.decode(0, 0, 0)) {
ESP_LOGE(TAG, "Error while decoding.");
this->jpeg_.close();

View File

@ -21,7 +21,7 @@ class JpegDecoder : public ImageDecoder {
JpegDecoder(OnlineImage *image) : ImageDecoder(image) {}
~JpegDecoder() override {}
void prepare(size_t download_size) override;
int prepare(size_t download_size) override;
int HOT decode(uint8_t *buffer, size_t size) override;
protected:

View File

@ -64,33 +64,34 @@ void OnlineImage::release() {
}
}
bool OnlineImage::resize_(int width_in, int height_in) {
size_t OnlineImage::resize_(int width_in, int height_in) {
int width = this->fixed_width_;
int height = this->fixed_height_;
if (this->auto_resize_()) {
if (this->is_auto_resize_()) {
width = width_in;
height = height_in;
if (this->width_ != width && this->height_ != height) {
this->release();
}
}
if (this->buffer_) {
return false;
}
size_t new_size = this->get_buffer_size_(width, height);
if (this->buffer_) {
// Buffer already allocated => no need to resize
return new_size;
}
ESP_LOGD(TAG, "Allocating new buffer of %zu bytes", new_size);
this->buffer_ = this->allocator_.allocate(new_size);
if (this->buffer_ == nullptr) {
ESP_LOGE(TAG, "allocation of %zu bytes failed. Biggest block in heap: %zu Bytes", new_size,
this->allocator_.get_max_free_block_size());
this->end_connection_();
return false;
return 0;
}
this->buffer_width_ = width;
this->buffer_height_ = height;
this->width_ = width;
ESP_LOGV(TAG, "New size: (%d, %d)", width, height);
return true;
return new_size;
}
void OnlineImage::update() {
@ -100,7 +101,35 @@ void OnlineImage::update() {
}
ESP_LOGI(TAG, "Updating image %s", this->url_.c_str());
this->downloader_ = this->parent_->get(this->url_);
std::list<http_request::Header> headers = {};
http_request::Header accept_header;
accept_header.name = "Accept";
std::string accept_mime_type;
switch (this->format_) {
#ifdef USE_ONLINE_IMAGE_BMP_SUPPORT
case ImageFormat::BMP:
accept_mime_type = "image/bmp";
break;
#endif // ONLINE_IMAGE_BMP_SUPPORT
#ifdef USE_ONLINE_IMAGE_JPEG_SUPPORT
case ImageFormat::JPEG:
accept_mime_type = "image/jpeg";
break;
#endif // USE_ONLINE_IMAGE_JPEG_SUPPORT
#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT
case ImageFormat::PNG:
accept_mime_type = "image/png";
break;
#endif // ONLINE_IMAGE_PNG_SUPPORT
default:
accept_mime_type = "image/*";
}
accept_header.value = accept_mime_type + ",*/*;q=0.8";
headers.push_back(accept_header);
this->downloader_ = this->parent_->get(this->url_, headers);
if (this->downloader_ == nullptr) {
ESP_LOGE(TAG, "Download failed.");
@ -150,7 +179,12 @@ void OnlineImage::update() {
this->download_error_callback_.call();
return;
}
this->decoder_->prepare(total_size);
auto prepare_result = this->decoder_->prepare(total_size);
if (prepare_result < 0) {
this->end_connection_();
this->download_error_callback_.call();
return;
}
ESP_LOGI(TAG, "Downloading image (Size: %d)", total_size);
this->start_time_ = ::time(nullptr);
}

View File

@ -99,9 +99,22 @@ class OnlineImage : public PollingComponent,
int get_position_(int x, int y) const { return (x + y * this->buffer_width_) * this->get_bpp() / 8; }
ESPHOME_ALWAYS_INLINE bool auto_resize_() const { return this->fixed_width_ == 0 || this->fixed_height_ == 0; }
ESPHOME_ALWAYS_INLINE bool is_auto_resize_() const { return this->fixed_width_ == 0 || this->fixed_height_ == 0; }
bool resize_(int width, int height);
/**
* @brief Resize the image buffer to the requested dimensions.
*
* The buffer will be allocated if not existing.
* If the dimensions have been fixed in the yaml config, the buffer will be created
* with those dimensions and not resized, even on request.
* Otherwise, the old buffer will be deallocated and a new buffer with the requested
* allocated
*
* @param width
* @param height
* @return 0 if no memory could be allocated, the size of the new buffer otherwise.
*/
size_t resize_(int width, int height);
/**
* @brief Draw a pixel into the buffer.

View File

@ -40,11 +40,16 @@ static void draw_callback(pngle_t *pngle, uint32_t x, uint32_t y, uint32_t w, ui
decoder->draw(x, y, w, h, color);
}
void PngDecoder::prepare(size_t download_size) {
int PngDecoder::prepare(size_t download_size) {
ImageDecoder::prepare(download_size);
if (!this->pngle_) {
ESP_LOGE(TAG, "PNG decoder engine not initialized!");
return DECODE_ERROR_OUT_OF_MEMORY;
}
pngle_set_user_data(this->pngle_, this);
pngle_set_init_callback(this->pngle_, init_callback);
pngle_set_draw_callback(this->pngle_, draw_callback);
return 0;
}
int HOT PngDecoder::decode(uint8_t *buffer, size_t size) {

View File

@ -21,7 +21,7 @@ class PngDecoder : public ImageDecoder {
PngDecoder(OnlineImage *image) : ImageDecoder(image), pngle_(pngle_new()) {}
~PngDecoder() override { pngle_destroy(this->pngle_); }
void prepare(size_t download_size) override;
int prepare(size_t download_size) override;
int HOT decode(uint8_t *buffer, size_t size) override;
protected:

View File

@ -1,8 +1,9 @@
from typing import Any
import esphome.config_validation as cv
from esphome.components import binary_sensor
from .. import const, schema, validate, generate
import esphome.config_validation as cv
from .. import const, generate, schema, validate
DEPENDENCIES = [const.OPENTHERM]
COMPONENT_TYPE = const.BINARY_SENSOR
@ -11,8 +12,7 @@ COMPONENT_TYPE = const.BINARY_SENSOR
def get_entity_validation_schema(entity: schema.BinarySensorSchema) -> cv.Schema:
return binary_sensor.binary_sensor_schema(
device_class=(
entity.device_class
or binary_sensor._UNDEF # pylint: disable=protected-access
entity.device_class or binary_sensor._UNDEF # pylint: disable=protected-access
),
icon=(entity.icon or binary_sensor._UNDEF), # pylint: disable=protected-access
)

View File

@ -3,8 +3,9 @@ from typing import Any, Callable, Optional
import esphome.codegen as cg
from esphome.const import CONF_ID
from . import const
from .schema import TSchema, SettingSchema
from .schema import SettingSchema, TSchema
opentherm_ns = cg.esphome_ns.namespace("opentherm")
OpenthermHub = opentherm_ns.class_("OpenthermHub", cg.Component)
@ -112,11 +113,10 @@ def add_messages(hub: cg.MockObj, keys: list[str], schemas: dict[str, TSchema]):
msg_expr = cg.RawExpression(f"esphome::opentherm::MessageId::{msg}")
if keep_updated:
cg.add(hub.add_repeating_message(msg_expr))
elif order is not None:
cg.add(hub.add_initial_message(msg_expr, order))
else:
if order is not None:
cg.add(hub.add_initial_message(msg_expr, order))
else:
cg.add(hub.add_initial_message(msg_expr))
cg.add(hub.add_initial_message(msg_expr))
def add_property_set(var: cg.MockObj, config_key: str, config: dict[str, Any]) -> None:
@ -128,7 +128,7 @@ Create = Callable[[dict[str, Any], str, cg.MockObj], Awaitable[cg.Pvariable]]
def create_only_conf(
create: Callable[[dict[str, Any]], Awaitable[cg.Pvariable]]
create: Callable[[dict[str, Any]], Awaitable[cg.Pvariable]],
) -> Create:
return lambda conf, _key, _hub: create(conf)

View File

@ -1,8 +1,9 @@
from typing import Any
import esphome.config_validation as cv
from esphome.components import sensor
from .. import const, schema, validate, generate
import esphome.config_validation as cv
from .. import const, generate, schema, validate
DEPENDENCIES = [const.OPENTHERM]
COMPONENT_TYPE = const.SENSOR
@ -22,11 +23,9 @@ MSG_DATA_TYPES = {
def get_entity_validation_schema(entity: schema.SensorSchema) -> cv.Schema:
return sensor.sensor_schema(
unit_of_measurement=entity.unit_of_measurement
or sensor._UNDEF, # pylint: disable=protected-access
unit_of_measurement=entity.unit_of_measurement or sensor._UNDEF, # pylint: disable=protected-access
accuracy_decimals=entity.accuracy_decimals,
device_class=entity.device_class
or sensor._UNDEF, # pylint: disable=protected-access
device_class=entity.device_class or sensor._UNDEF, # pylint: disable=protected-access
icon=entity.icon or sensor._UNDEF, # pylint: disable=protected-access
state_class=entity.state_class,
).extend(

View File

@ -1,9 +1,10 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import binary_sensor
import esphome.config_validation as cv
from esphome.const import CONF_UID
from esphome.core import HexInt
from . import pn532_ns, PN532, CONF_PN532_ID
from . import CONF_PN532_ID, PN532, pn532_ns
DEPENDENCIES = ["pn532"]
@ -13,8 +14,7 @@ def validate_uid(value):
for x in value.split("-"):
if len(x) != 2:
raise cv.Invalid(
"Each part (separated by '-') of the UID must be two characters "
"long."
"Each part (separated by '-') of the UID must be two characters long."
)
try:
x = int(x, 16)

View File

@ -83,6 +83,12 @@ void PrometheusHandler::handleRequest(AsyncWebServerRequest *req) {
this->update_entity_row_(stream, obj, area, node, friendly_name);
#endif
#ifdef USE_VALVE
this->valve_type_(stream);
for (auto *obj : App.get_valves())
this->valve_row_(stream, obj, area, node, friendly_name);
#endif
req->send(stream);
}
@ -770,6 +776,54 @@ void PrometheusHandler::update_entity_row_(AsyncResponseStream *stream, update::
}
#endif
#ifdef USE_VALVE
void PrometheusHandler::valve_type_(AsyncResponseStream *stream) {
stream->print(F("#TYPE esphome_valve_operation gauge\n"));
stream->print(F("#TYPE esphome_valve_failed gauge\n"));
stream->print(F("#TYPE esphome_valve_position gauge\n"));
}
void PrometheusHandler::valve_row_(AsyncResponseStream *stream, valve::Valve *obj, std::string &area, std::string &node,
std::string &friendly_name) {
if (obj->is_internal() && !this->include_internal_)
return;
stream->print(F("esphome_valve_failed{id=\""));
stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area);
add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name);
stream->print(F("\",name=\""));
stream->print(relabel_name_(obj).c_str());
stream->print(F("\"} 0\n"));
// Data itself
stream->print(F("esphome_valve_operation{id=\""));
stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area);
add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name);
stream->print(F("\",name=\""));
stream->print(relabel_name_(obj).c_str());
stream->print(F("\",operation=\""));
stream->print(valve::valve_operation_to_str(obj->current_operation));
stream->print(F("\"} "));
stream->print(F("1.0"));
stream->print(F("\n"));
// Now see if position is supported
if (obj->get_traits().get_supports_position()) {
stream->print(F("esphome_valve_position{id=\""));
stream->print(relabel_id_(obj).c_str());
add_area_label_(stream, area);
add_node_label_(stream, node);
add_friendly_name_label_(stream, friendly_name);
stream->print(F("\",name=\""));
stream->print(relabel_name_(obj).c_str());
stream->print(F("\"} "));
stream->print(obj->position);
stream->print(F("\n"));
}
}
#endif
} // namespace prometheus
} // namespace esphome
#endif

View File

@ -161,6 +161,14 @@ class PrometheusHandler : public AsyncWebHandler, public Component {
void handle_update_state_(AsyncResponseStream *stream, update::UpdateState state);
#endif
#ifdef USE_VALVE
/// Return the type for prometheus
void valve_type_(AsyncResponseStream *stream);
/// Return the valve state as prometheus data point
void valve_row_(AsyncResponseStream *stream, valve::Valve *obj, std::string &area, std::string &node,
std::string &friendly_name);
#endif
web_server_base::WebServerBase *base_;
bool include_internal_{false};
std::map<EntityBase *, std::string> relabel_map_id_;

View File

@ -1,9 +1,10 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import binary_sensor
import esphome.config_validation as cv
from esphome.const import CONF_UID
from esphome.core import HexInt
from . import rc522_ns, RC522, CONF_RC522_ID
from . import CONF_RC522_ID, RC522, rc522_ns
DEPENDENCIES = ["rc522"]
@ -13,8 +14,7 @@ def validate_uid(value):
for x in value.split("-"):
if len(x) != 2:
raise cv.Invalid(
"Each part (separated by '-') of the UID must be two characters "
"long."
"Each part (separated by '-') of the UID must be two characters long."
)
try:
x = int(x, 16)

View File

@ -36,7 +36,7 @@ template<typename... Ts> class TotoAction : public RemoteTransmitterActionBase<T
data.rc_code_2 = this->rc_code_2_.value(x...);
data.command = this->command_.value(x...);
this->set_send_times(this->send_times_.value_or(x..., 3));
this->set_send_wait(this->send_wait_.value_or(x..., 32000));
this->set_send_wait(this->send_wait_.value_or(x..., 36000));
TotoProtocol().encode(dst, data);
}
};

View File

@ -75,7 +75,7 @@ CONFIG_SCHEMA = (
cv.Optional(CONF_UPDATE_INTERVAL, default="60s"): cv.All(
cv.positive_time_period_seconds,
cv.Range(
min=core.TimePeriod(seconds=1), max=core.TimePeriod(seconds=1800)
min=core.TimePeriod(seconds=2), max=core.TimePeriod(seconds=1800)
),
),
}

View File

@ -0,0 +1,458 @@
"""Speaker Media Player Setup."""
import hashlib
import logging
from pathlib import Path
from esphome import automation, external_files
import esphome.codegen as cg
from esphome.components import audio, esp32, media_player, speaker
import esphome.config_validation as cv
from esphome.const import (
CONF_BUFFER_SIZE,
CONF_FILE,
CONF_FILES,
CONF_FORMAT,
CONF_ID,
CONF_NUM_CHANNELS,
CONF_PATH,
CONF_RAW_DATA_ID,
CONF_SAMPLE_RATE,
CONF_SPEAKER,
CONF_TASK_STACK_IN_PSRAM,
CONF_TYPE,
CONF_URL,
)
from esphome.core import CORE, HexInt
from esphome.core.entity_helpers import inherit_property_from
from esphome.external_files import download_content
_LOGGER = logging.getLogger(__name__)
AUTO_LOAD = ["audio", "psram"]
CODEOWNERS = ["@kahrendt", "@synesthesiam"]
DOMAIN = "media_player"
TYPE_LOCAL = "local"
TYPE_WEB = "web"
CONF_ANNOUNCEMENT = "announcement"
CONF_ANNOUNCEMENT_PIPELINE = "announcement_pipeline"
CONF_CODEC_SUPPORT_ENABLED = "codec_support_enabled"
CONF_ENQUEUE = "enqueue"
CONF_MEDIA_FILE = "media_file"
CONF_MEDIA_PIPELINE = "media_pipeline"
CONF_ON_MUTE = "on_mute"
CONF_ON_UNMUTE = "on_unmute"
CONF_ON_VOLUME = "on_volume"
CONF_STREAM = "stream"
CONF_VOLUME_INCREMENT = "volume_increment"
CONF_VOLUME_MIN = "volume_min"
CONF_VOLUME_MAX = "volume_max"
speaker_ns = cg.esphome_ns.namespace("speaker")
SpeakerMediaPlayer = speaker_ns.class_(
"SpeakerMediaPlayer",
media_player.MediaPlayer,
cg.Component,
)
AudioPipeline = speaker_ns.class_("AudioPipeline")
AudioPipelineType = speaker_ns.enum("AudioPipelineType", is_class=True)
AUDIO_PIPELINE_TYPE_ENUM = {
"MEDIA": AudioPipelineType.MEDIA,
"ANNOUNCEMENT": AudioPipelineType.ANNOUNCEMENT,
}
PlayOnDeviceMediaAction = speaker_ns.class_(
"PlayOnDeviceMediaAction",
automation.Action,
cg.Parented.template(SpeakerMediaPlayer),
)
StopStreamAction = speaker_ns.class_(
"StopStreamAction", automation.Action, cg.Parented.template(SpeakerMediaPlayer)
)
def _compute_local_file_path(value: dict) -> Path:
url = value[CONF_URL]
h = hashlib.new("sha256")
h.update(url.encode())
key = h.hexdigest()[:8]
base_dir = external_files.compute_local_file_dir(DOMAIN)
_LOGGER.debug("_compute_local_file_path: base_dir=%s", base_dir / key)
return base_dir / key
def _download_web_file(value):
url = value[CONF_URL]
path = _compute_local_file_path(value)
download_content(url, path)
_LOGGER.debug("download_web_file: path=%s", path)
return value
# Returns a media_player.MediaPlayerSupportedFormat struct with the configured
# format, sample rate, number of channels, purpose, and bytes per sample
def _get_supported_format_struct(pipeline, type):
args = [
media_player.MediaPlayerSupportedFormat,
]
if pipeline[CONF_FORMAT] == "FLAC":
args.append(("format", "flac"))
elif pipeline[CONF_FORMAT] == "MP3":
args.append(("format", "mp3"))
elif pipeline[CONF_FORMAT] == "WAV":
args.append(("format", "wav"))
args.append(("sample_rate", pipeline[CONF_SAMPLE_RATE]))
args.append(("num_channels", pipeline[CONF_NUM_CHANNELS]))
if type == "MEDIA":
args.append(
(
"purpose",
media_player.MEDIA_PLAYER_FORMAT_PURPOSE_ENUM["default"],
)
)
elif type == "ANNOUNCEMENT":
args.append(
(
"purpose",
media_player.MEDIA_PLAYER_FORMAT_PURPOSE_ENUM["announcement"],
)
)
if pipeline[CONF_FORMAT] != "MP3":
args.append(("sample_bytes", 2))
return cg.StructInitializer(*args)
def _file_schema(value):
if isinstance(value, str):
return _validate_file_shorthand(value)
return TYPED_FILE_SCHEMA(value)
def _read_audio_file_and_type(file_config):
conf_file = file_config[CONF_FILE]
file_source = conf_file[CONF_TYPE]
if file_source == TYPE_LOCAL:
path = CORE.relative_config_path(conf_file[CONF_PATH])
elif file_source == TYPE_WEB:
path = _compute_local_file_path(conf_file)
else:
raise cv.Invalid("Unsupported file source.")
with open(path, "rb") as f:
data = f.read()
import puremagic
file_type: str = puremagic.from_string(data)
if file_type.startswith("."):
file_type = file_type[1:]
media_file_type = audio.AUDIO_FILE_TYPE_ENUM["NONE"]
if file_type in ("wav"):
media_file_type = audio.AUDIO_FILE_TYPE_ENUM["WAV"]
elif file_type in ("mp3", "mpeg", "mpga"):
media_file_type = audio.AUDIO_FILE_TYPE_ENUM["MP3"]
elif file_type in ("flac"):
media_file_type = audio.AUDIO_FILE_TYPE_ENUM["FLAC"]
return data, media_file_type
def _validate_file_shorthand(value):
value = cv.string_strict(value)
if value.startswith("http://") or value.startswith("https://"):
return _file_schema(
{
CONF_TYPE: TYPE_WEB,
CONF_URL: value,
}
)
return _file_schema(
{
CONF_TYPE: TYPE_LOCAL,
CONF_PATH: value,
}
)
def _validate_pipeline(config):
# Inherit transcoder settings from speaker if not manually set
inherit_property_from(CONF_NUM_CHANNELS, CONF_SPEAKER)(config)
inherit_property_from(CONF_SAMPLE_RATE, CONF_SPEAKER)(config)
# Validate the transcoder settings is compatible with the speaker
audio.final_validate_audio_schema(
"speaker media_player",
audio_device=CONF_SPEAKER,
bits_per_sample=16,
channels=config.get(CONF_NUM_CHANNELS),
sample_rate=config.get(CONF_SAMPLE_RATE),
)(config)
return config
def _validate_repeated_speaker(config):
if (announcement_config := config.get(CONF_ANNOUNCEMENT_PIPELINE)) and (
media_config := config.get(CONF_MEDIA_PIPELINE)
):
if announcement_config[CONF_SPEAKER] == media_config[CONF_SPEAKER]:
raise cv.Invalid(
"The announcement and media pipelines cannot use the same speaker. Use the `mixer` speaker component to create two source speakers."
)
return config
def _validate_supported_local_file(config):
for file_config in config.get(CONF_FILES, []):
_, media_file_type = _read_audio_file_and_type(file_config)
if str(media_file_type) == str(audio.AUDIO_FILE_TYPE_ENUM["NONE"]):
raise cv.Invalid("Unsupported local media file.")
if not config[CONF_CODEC_SUPPORT_ENABLED] and str(media_file_type) != str(
audio.AUDIO_FILE_TYPE_ENUM["WAV"]
):
# Only wav files are supported
raise cv.Invalid(
f"Unsupported local media file type, set {CONF_CODEC_SUPPORT_ENABLED} to true or convert the media file to wav"
)
return config
LOCAL_SCHEMA = cv.Schema(
{
cv.Required(CONF_PATH): cv.file_,
}
)
WEB_SCHEMA = cv.All(
{
cv.Required(CONF_URL): cv.url,
},
_download_web_file,
)
TYPED_FILE_SCHEMA = cv.typed_schema(
{
TYPE_LOCAL: LOCAL_SCHEMA,
TYPE_WEB: WEB_SCHEMA,
},
)
MEDIA_FILE_TYPE_SCHEMA = cv.Schema(
{
cv.Required(CONF_ID): cv.declare_id(audio.AudioFile),
cv.Required(CONF_FILE): _file_schema,
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
}
)
PIPELINE_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(AudioPipeline),
cv.Required(CONF_SPEAKER): cv.use_id(speaker.Speaker),
cv.Optional(CONF_FORMAT, default="FLAC"): cv.enum(audio.AUDIO_FILE_TYPE_ENUM),
cv.Optional(CONF_SAMPLE_RATE): cv.int_range(min=1),
cv.Optional(CONF_NUM_CHANNELS): cv.int_range(1, 2),
}
)
CONFIG_SCHEMA = cv.All(
media_player.MEDIA_PLAYER_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(SpeakerMediaPlayer),
cv.Required(CONF_ANNOUNCEMENT_PIPELINE): PIPELINE_SCHEMA,
cv.Optional(CONF_MEDIA_PIPELINE): PIPELINE_SCHEMA,
cv.Optional(CONF_BUFFER_SIZE, default=1000000): cv.int_range(
min=4000, max=4000000
),
cv.Optional(CONF_CODEC_SUPPORT_ENABLED, default=True): cv.boolean,
cv.Optional(CONF_FILES): cv.ensure_list(MEDIA_FILE_TYPE_SCHEMA),
cv.Optional(CONF_TASK_STACK_IN_PSRAM, default=False): cv.boolean,
cv.Optional(CONF_VOLUME_INCREMENT, default=0.05): cv.percentage,
cv.Optional(CONF_VOLUME_MAX, default=1.0): cv.percentage,
cv.Optional(CONF_VOLUME_MIN, default=0.0): cv.percentage,
cv.Optional(CONF_ON_MUTE): automation.validate_automation(single=True),
cv.Optional(CONF_ON_UNMUTE): automation.validate_automation(single=True),
cv.Optional(CONF_ON_VOLUME): automation.validate_automation(single=True),
}
),
cv.only_with_esp_idf,
_validate_repeated_speaker,
)
FINAL_VALIDATE_SCHEMA = cv.All(
cv.Schema(
{
cv.Optional(CONF_ANNOUNCEMENT_PIPELINE): _validate_pipeline,
cv.Optional(CONF_MEDIA_PIPELINE): _validate_pipeline,
},
extra=cv.ALLOW_EXTRA,
),
_validate_supported_local_file,
)
async def to_code(config):
if config[CONF_CODEC_SUPPORT_ENABLED]:
# Compile all supported audio codecs and optimize the wifi settings
cg.add_define("USE_AUDIO_FLAC_SUPPORT", True)
cg.add_define("USE_AUDIO_MP3_SUPPORT", True)
# Wifi settings based on https://github.com/espressif/esp-adf/issues/297#issuecomment-783811702
esp32.add_idf_sdkconfig_option("CONFIG_ESP32_WIFI_STATIC_RX_BUFFER_NUM", 16)
esp32.add_idf_sdkconfig_option("CONFIG_ESP32_WIFI_DYNAMIC_RX_BUFFER_NUM", 512)
esp32.add_idf_sdkconfig_option("CONFIG_ESP32_WIFI_STATIC_TX_BUFFER", True)
esp32.add_idf_sdkconfig_option("CONFIG_ESP32_WIFI_TX_BUFFER_TYPE", 0)
esp32.add_idf_sdkconfig_option("CONFIG_ESP32_WIFI_STATIC_TX_BUFFER_NUM", 8)
esp32.add_idf_sdkconfig_option("CONFIG_ESP32_WIFI_CACHE_TX_BUFFER_NUM", 32)
esp32.add_idf_sdkconfig_option("CONFIG_ESP32_WIFI_AMPDU_TX_ENABLED", True)
esp32.add_idf_sdkconfig_option("CONFIG_ESP32_WIFI_TX_BA_WIN", 16)
esp32.add_idf_sdkconfig_option("CONFIG_ESP32_WIFI_AMPDU_RX_ENABLED", True)
esp32.add_idf_sdkconfig_option("CONFIG_ESP32_WIFI_RX_BA_WIN", 32)
esp32.add_idf_sdkconfig_option("CONFIG_LWIP_MAX_ACTIVE_TCP", 16)
esp32.add_idf_sdkconfig_option("CONFIG_LWIP_MAX_LISTENING_TCP", 16)
esp32.add_idf_sdkconfig_option("CONFIG_TCP_MAXRTX", 12)
esp32.add_idf_sdkconfig_option("CONFIG_TCP_SYNMAXRTX", 6)
esp32.add_idf_sdkconfig_option("CONFIG_TCP_MSS", 1436)
esp32.add_idf_sdkconfig_option("CONFIG_TCP_MSL", 60000)
esp32.add_idf_sdkconfig_option("CONFIG_TCP_SND_BUF_DEFAULT", 65535)
esp32.add_idf_sdkconfig_option(
"CONFIG_TCP_WND_DEFAULT", 65535
) # Adjusted from referenced settings to avoid compilation error
esp32.add_idf_sdkconfig_option("CONFIG_TCP_RECVMBOX_SIZE", 512)
esp32.add_idf_sdkconfig_option("CONFIG_TCP_QUEUE_OOSEQ", True)
esp32.add_idf_sdkconfig_option("CONFIG_TCP_OVERSIZE_MSS", True)
esp32.add_idf_sdkconfig_option("CONFIG_LWIP_WND_SCALE", True)
esp32.add_idf_sdkconfig_option("CONFIG_TCP_RCV_SCALE", 3)
esp32.add_idf_sdkconfig_option("CONFIG_LWIP_TCPIP_RECVMBOX_SIZE", 512)
# Allocate wifi buffers in PSRAM
esp32.add_idf_sdkconfig_option("CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP", True)
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await media_player.register_media_player(var, config)
cg.add_define("USE_OTA_STATE_CALLBACK")
cg.add(var.set_buffer_size(config[CONF_BUFFER_SIZE]))
cg.add(var.set_task_stack_in_psram(config[CONF_TASK_STACK_IN_PSRAM]))
if config[CONF_TASK_STACK_IN_PSRAM]:
esp32.add_idf_sdkconfig_option(
"CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY", True
)
cg.add(var.set_volume_increment(config[CONF_VOLUME_INCREMENT]))
cg.add(var.set_volume_max(config[CONF_VOLUME_MAX]))
cg.add(var.set_volume_min(config[CONF_VOLUME_MIN]))
announcement_pipeline_config = config[CONF_ANNOUNCEMENT_PIPELINE]
spkr = await cg.get_variable(announcement_pipeline_config[CONF_SPEAKER])
cg.add(var.set_announcement_speaker(spkr))
if announcement_pipeline_config[CONF_FORMAT] != "NONE":
cg.add(
var.set_announcement_format(
_get_supported_format_struct(
announcement_pipeline_config, "ANNOUNCEMENT"
)
)
)
if media_pipeline_config := config.get(CONF_MEDIA_PIPELINE):
spkr = await cg.get_variable(media_pipeline_config[CONF_SPEAKER])
cg.add(var.set_media_speaker(spkr))
if media_pipeline_config[CONF_FORMAT] != "NONE":
cg.add(
var.set_media_format(
_get_supported_format_struct(media_pipeline_config, "MEDIA")
)
)
if on_mute := config.get(CONF_ON_MUTE):
await automation.build_automation(
var.get_mute_trigger(),
[],
on_mute,
)
if on_unmute := config.get(CONF_ON_UNMUTE):
await automation.build_automation(
var.get_unmute_trigger(),
[],
on_unmute,
)
if on_volume := config.get(CONF_ON_VOLUME):
await automation.build_automation(
var.get_volume_trigger(),
[(cg.float_, "x")],
on_volume,
)
for file_config in config.get(CONF_FILES, []):
data, media_file_type = _read_audio_file_and_type(file_config)
rhs = [HexInt(x) for x in data]
prog_arr = cg.progmem_array(file_config[CONF_RAW_DATA_ID], rhs)
media_files_struct = cg.StructInitializer(
audio.AudioFile,
(
"data",
prog_arr,
),
(
"length",
len(rhs),
),
(
"file_type",
media_file_type,
),
)
cg.new_Pvariable(
file_config[CONF_ID],
media_files_struct,
)
@automation.register_action(
"media_player.speaker.play_on_device_media_file",
PlayOnDeviceMediaAction,
cv.maybe_simple_value(
{
cv.GenerateID(): cv.use_id(SpeakerMediaPlayer),
cv.Required(CONF_MEDIA_FILE): cv.use_id(audio.AudioFile),
cv.Optional(CONF_ANNOUNCEMENT, default=False): cv.templatable(cv.boolean),
cv.Optional(CONF_ENQUEUE, default=False): cv.templatable(cv.boolean),
},
key=CONF_MEDIA_FILE,
),
)
async def play_on_device_media_media_action(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
media_file = await cg.get_variable(config[CONF_MEDIA_FILE])
announcement = await cg.templatable(config[CONF_ANNOUNCEMENT], args, cg.bool_)
enqueue = await cg.templatable(config[CONF_ENQUEUE], args, cg.bool_)
cg.add(var.set_audio_file(media_file))
cg.add(var.set_announcement(announcement))
cg.add(var.set_enqueue(enqueue))
return var

View File

@ -0,0 +1,568 @@
#include "audio_pipeline.h"
#ifdef USE_ESP_IDF
#include "esphome/core/defines.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome {
namespace speaker {
static const uint32_t INITIAL_BUFFER_MS = 1000; // Start playback after buffering this duration of the file
static const uint32_t READ_TASK_STACK_SIZE = 5 * 1024;
static const uint32_t DECODE_TASK_STACK_SIZE = 3 * 1024;
static const uint32_t INFO_ERROR_QUEUE_COUNT = 5;
static const char *const TAG = "speaker_media_player.pipeline";
enum EventGroupBits : uint32_t {
// MESSAGE_* bits are only set by their respective tasks
// Stops all activity in the pipeline elements; cleared by process_state() and set by stop() or by each task
PIPELINE_COMMAND_STOP = (1 << 0),
// Read audio from an HTTP source; cleared by reader task and set by start_url
READER_COMMAND_INIT_HTTP = (1 << 4),
// Read audio from an audio file from the flash; cleared by reader task and set by start_file
READER_COMMAND_INIT_FILE = (1 << 5),
// Audio file type is read after checking it is supported; cleared by decoder task
READER_MESSAGE_LOADED_MEDIA_TYPE = (1 << 6),
// Reader is done (either through a failure or just end of the stream); cleared by reader task
READER_MESSAGE_FINISHED = (1 << 7),
// Error reading the file; cleared by process_state()
READER_MESSAGE_ERROR = (1 << 8),
// Decoder is done (either through a faiilure or the end of the stream); cleared by decoder task
DECODER_MESSAGE_FINISHED = (1 << 12),
// Error decoding the file; cleared by process_state() by decoder task
DECODER_MESSAGE_ERROR = (1 << 13),
};
AudioPipeline::AudioPipeline(speaker::Speaker *speaker, size_t buffer_size, bool task_stack_in_psram,
std::string base_name, UBaseType_t priority)
: base_name_(std::move(base_name)),
priority_(priority),
task_stack_in_psram_(task_stack_in_psram),
speaker_(speaker),
buffer_size_(buffer_size) {
this->allocate_communications_();
this->transfer_buffer_size_ = std::min(buffer_size_ / 4, DEFAULT_TRANSFER_BUFFER_SIZE);
}
void AudioPipeline::start_url(const std::string &uri) {
if (this->is_playing_) {
xEventGroupSetBits(this->event_group_, PIPELINE_COMMAND_STOP);
}
this->current_uri_ = uri;
this->pending_url_ = true;
}
void AudioPipeline::start_file(audio::AudioFile *audio_file) {
if (this->is_playing_) {
xEventGroupSetBits(this->event_group_, PIPELINE_COMMAND_STOP);
}
this->current_audio_file_ = audio_file;
this->pending_file_ = true;
}
esp_err_t AudioPipeline::stop() {
xEventGroupSetBits(this->event_group_, EventGroupBits::PIPELINE_COMMAND_STOP);
return ESP_OK;
}
void AudioPipeline::set_pause_state(bool pause_state) {
this->speaker_->set_pause_state(pause_state);
this->pause_state_ = pause_state;
}
void AudioPipeline::suspend_tasks() {
if (this->read_task_handle_ != nullptr) {
vTaskSuspend(this->read_task_handle_);
}
if (this->decode_task_handle_ != nullptr) {
vTaskSuspend(this->decode_task_handle_);
}
}
void AudioPipeline::resume_tasks() {
if (this->read_task_handle_ != nullptr) {
vTaskResume(this->read_task_handle_);
}
if (this->decode_task_handle_ != nullptr) {
vTaskResume(this->decode_task_handle_);
}
}
AudioPipelineState AudioPipeline::process_state() {
/*
* Log items from info error queue
*/
InfoErrorEvent event;
if (this->info_error_queue_ != nullptr) {
while (xQueueReceive(this->info_error_queue_, &event, 0)) {
switch (event.source) {
case InfoErrorSource::READER:
if (event.err.has_value()) {
ESP_LOGE(TAG, "Media reader encountered an error: %s", esp_err_to_name(event.err.value()));
} else if (event.file_type.has_value()) {
ESP_LOGD(TAG, "Reading %s file type", audio_file_type_to_string(event.file_type.value()));
}
break;
case InfoErrorSource::DECODER:
if (event.err.has_value()) {
ESP_LOGE(TAG, "Decoder encountered an error: %s", esp_err_to_name(event.err.value()));
}
if (event.audio_stream_info.has_value()) {
ESP_LOGD(TAG, "Decoded audio has %d channels, %" PRId32 " Hz sample rate, and %d bits per sample",
event.audio_stream_info.value().get_channels(), event.audio_stream_info.value().get_sample_rate(),
event.audio_stream_info.value().get_bits_per_sample());
}
if (event.decoding_err.has_value()) {
switch (event.decoding_err.value()) {
case DecodingError::FAILED_HEADER:
ESP_LOGE(TAG, "Failed to parse the file's header.");
break;
case DecodingError::INCOMPATIBLE_BITS_PER_SAMPLE:
ESP_LOGE(TAG, "Incompatible bits per sample. Only 16 bits per sample is supported");
break;
case DecodingError::INCOMPATIBLE_CHANNELS:
ESP_LOGE(TAG, "Incompatible number of channels. Only 1 or 2 channel audio is supported.");
break;
}
}
break;
}
}
}
/*
* Determine the current state based on the event group bits and tasks' status
*/
EventBits_t event_bits = xEventGroupGetBits(this->event_group_);
if (this->pending_url_ || this->pending_file_) {
// Init command pending
if (!(event_bits & EventGroupBits::PIPELINE_COMMAND_STOP)) {
// Only start if there is no pending stop command
if ((this->read_task_handle_ == nullptr) || (this->decode_task_handle_ == nullptr)) {
// At least one task isn't running
this->start_tasks_();
}
if (this->pending_url_) {
xEventGroupSetBits(this->event_group_, EventGroupBits::READER_COMMAND_INIT_HTTP);
this->playback_ms_ = 0;
this->pending_url_ = false;
} else if (this->pending_file_) {
xEventGroupSetBits(this->event_group_, EventGroupBits::READER_COMMAND_INIT_FILE);
this->playback_ms_ = 0;
this->pending_file_ = false;
}
this->is_playing_ = true;
return AudioPipelineState::PLAYING;
}
}
if ((event_bits & EventGroupBits::READER_MESSAGE_FINISHED) &&
(!(event_bits & EventGroupBits::READER_MESSAGE_LOADED_MEDIA_TYPE) &&
(event_bits & EventGroupBits::DECODER_MESSAGE_FINISHED))) {
// Tasks are finished and there's no media in between the reader and decoder
if (event_bits & EventGroupBits::PIPELINE_COMMAND_STOP) {
// Stop command is fully processed, so clear the command bit
xEventGroupClearBits(this->event_group_, EventGroupBits::PIPELINE_COMMAND_STOP);
this->hard_stop_ = true;
}
if (!this->is_playing_) {
// The tasks have been stopped for two ``process_state`` calls in a row, so delete the tasks
if ((this->read_task_handle_ != nullptr) || (this->decode_task_handle_ != nullptr)) {
this->delete_tasks_();
if (this->hard_stop_) {
// Stop command was sent, so immediately end of the playback
this->speaker_->stop();
this->hard_stop_ = false;
} else {
// Decoded all the audio, so let the speaker finish playing before stopping
this->speaker_->finish();
}
}
}
this->is_playing_ = false;
return AudioPipelineState::STOPPED;
}
if ((event_bits & EventGroupBits::READER_MESSAGE_ERROR)) {
xEventGroupClearBits(this->event_group_, EventGroupBits::READER_MESSAGE_ERROR);
return AudioPipelineState::ERROR_READING;
}
if ((event_bits & EventGroupBits::DECODER_MESSAGE_ERROR)) {
xEventGroupClearBits(this->event_group_, EventGroupBits::DECODER_MESSAGE_ERROR);
return AudioPipelineState::ERROR_DECODING;
}
if (this->pause_state_) {
return AudioPipelineState::PAUSED;
}
if ((this->read_task_handle_ == nullptr) && (this->decode_task_handle_ == nullptr)) {
// No tasks are running, so the pipeline is stopped.
xEventGroupClearBits(this->event_group_, EventGroupBits::PIPELINE_COMMAND_STOP);
return AudioPipelineState::STOPPED;
}
this->is_playing_ = true;
return AudioPipelineState::PLAYING;
}
esp_err_t AudioPipeline::allocate_communications_() {
if (this->event_group_ == nullptr)
this->event_group_ = xEventGroupCreate();
if (this->event_group_ == nullptr) {
return ESP_ERR_NO_MEM;
}
if (this->info_error_queue_ == nullptr)
this->info_error_queue_ = xQueueCreate(INFO_ERROR_QUEUE_COUNT, sizeof(InfoErrorEvent));
if (this->info_error_queue_ == nullptr)
return ESP_ERR_NO_MEM;
return ESP_OK;
}
esp_err_t AudioPipeline::start_tasks_() {
if (this->read_task_handle_ == nullptr) {
if (this->read_task_stack_buffer_ == nullptr) {
if (this->task_stack_in_psram_) {
RAMAllocator<StackType_t> stack_allocator(RAMAllocator<StackType_t>::ALLOC_EXTERNAL);
this->read_task_stack_buffer_ = stack_allocator.allocate(READ_TASK_STACK_SIZE);
} else {
RAMAllocator<StackType_t> stack_allocator(RAMAllocator<StackType_t>::ALLOC_INTERNAL);
this->read_task_stack_buffer_ = stack_allocator.allocate(READ_TASK_STACK_SIZE);
}
}
if (this->read_task_stack_buffer_ == nullptr) {
return ESP_ERR_NO_MEM;
}
if (this->read_task_handle_ == nullptr) {
this->read_task_handle_ =
xTaskCreateStatic(read_task, (this->base_name_ + "_read").c_str(), READ_TASK_STACK_SIZE, (void *) this,
this->priority_, this->read_task_stack_buffer_, &this->read_task_stack_);
}
if (this->read_task_handle_ == nullptr) {
return ESP_ERR_INVALID_STATE;
}
}
if (this->decode_task_handle_ == nullptr) {
if (this->decode_task_stack_buffer_ == nullptr) {
if (this->task_stack_in_psram_) {
RAMAllocator<StackType_t> stack_allocator(RAMAllocator<StackType_t>::ALLOC_EXTERNAL);
this->decode_task_stack_buffer_ = stack_allocator.allocate(DECODE_TASK_STACK_SIZE);
} else {
RAMAllocator<StackType_t> stack_allocator(RAMAllocator<StackType_t>::ALLOC_INTERNAL);
this->decode_task_stack_buffer_ = stack_allocator.allocate(DECODE_TASK_STACK_SIZE);
}
}
if (this->decode_task_stack_buffer_ == nullptr) {
return ESP_ERR_NO_MEM;
}
if (this->decode_task_handle_ == nullptr) {
this->decode_task_handle_ =
xTaskCreateStatic(decode_task, (this->base_name_ + "_decode").c_str(), DECODE_TASK_STACK_SIZE, (void *) this,
this->priority_, this->decode_task_stack_buffer_, &this->decode_task_stack_);
}
if (this->decode_task_handle_ == nullptr) {
return ESP_ERR_INVALID_STATE;
}
}
return ESP_OK;
}
void AudioPipeline::delete_tasks_() {
if (this->read_task_handle_ != nullptr) {
vTaskDelete(this->read_task_handle_);
if (this->read_task_stack_buffer_ != nullptr) {
if (this->task_stack_in_psram_) {
RAMAllocator<StackType_t> stack_allocator(RAMAllocator<StackType_t>::ALLOC_EXTERNAL);
stack_allocator.deallocate(this->read_task_stack_buffer_, READ_TASK_STACK_SIZE);
} else {
RAMAllocator<StackType_t> stack_allocator(RAMAllocator<StackType_t>::ALLOC_INTERNAL);
stack_allocator.deallocate(this->read_task_stack_buffer_, READ_TASK_STACK_SIZE);
}
this->read_task_stack_buffer_ = nullptr;
this->read_task_handle_ = nullptr;
}
}
if (this->decode_task_handle_ != nullptr) {
vTaskDelete(this->decode_task_handle_);
if (this->decode_task_stack_buffer_ != nullptr) {
if (this->task_stack_in_psram_) {
RAMAllocator<StackType_t> stack_allocator(RAMAllocator<StackType_t>::ALLOC_EXTERNAL);
stack_allocator.deallocate(this->decode_task_stack_buffer_, DECODE_TASK_STACK_SIZE);
} else {
RAMAllocator<StackType_t> stack_allocator(RAMAllocator<StackType_t>::ALLOC_INTERNAL);
stack_allocator.deallocate(this->decode_task_stack_buffer_, DECODE_TASK_STACK_SIZE);
}
this->decode_task_stack_buffer_ = nullptr;
this->decode_task_handle_ = nullptr;
}
}
}
void AudioPipeline::read_task(void *params) {
AudioPipeline *this_pipeline = (AudioPipeline *) params;
while (true) {
xEventGroupSetBits(this_pipeline->event_group_, EventGroupBits::READER_MESSAGE_FINISHED);
// Wait until the pipeline notifies us the source of the media file
EventBits_t event_bits =
xEventGroupWaitBits(this_pipeline->event_group_,
EventGroupBits::READER_COMMAND_INIT_FILE | EventGroupBits::READER_COMMAND_INIT_HTTP |
EventGroupBits::PIPELINE_COMMAND_STOP, // Bit message to read
pdFALSE, // Clear the bit on exit
pdFALSE, // Wait for all the bits,
portMAX_DELAY); // Block indefinitely until bit is set
if (!(event_bits & EventGroupBits::PIPELINE_COMMAND_STOP)) {
xEventGroupClearBits(this_pipeline->event_group_, EventGroupBits::READER_MESSAGE_FINISHED |
EventGroupBits::READER_COMMAND_INIT_FILE |
EventGroupBits::READER_COMMAND_INIT_HTTP);
InfoErrorEvent event;
event.source = InfoErrorSource::READER;
esp_err_t err = ESP_OK;
std::unique_ptr<audio::AudioReader> reader =
make_unique<audio::AudioReader>(this_pipeline->transfer_buffer_size_);
if (event_bits & EventGroupBits::READER_COMMAND_INIT_FILE) {
err = reader->start(this_pipeline->current_audio_file_, this_pipeline->current_audio_file_type_);
} else {
err = reader->start(this_pipeline->current_uri_, this_pipeline->current_audio_file_type_);
}
if (err == ESP_OK) {
size_t file_ring_buffer_size = this_pipeline->buffer_size_;
std::shared_ptr<RingBuffer> temp_ring_buffer;
if (!this_pipeline->raw_file_ring_buffer_.use_count()) {
temp_ring_buffer = RingBuffer::create(file_ring_buffer_size);
this_pipeline->raw_file_ring_buffer_ = temp_ring_buffer;
}
if (!this_pipeline->raw_file_ring_buffer_.use_count()) {
err = ESP_ERR_NO_MEM;
} else {
reader->add_sink(this_pipeline->raw_file_ring_buffer_);
}
}
if (err != ESP_OK) {
// Send specific error message
event.err = err;
xQueueSend(this_pipeline->info_error_queue_, &event, portMAX_DELAY);
// Setting up the reader failed, stop the pipeline
xEventGroupSetBits(this_pipeline->event_group_,
EventGroupBits::READER_MESSAGE_ERROR | EventGroupBits::PIPELINE_COMMAND_STOP);
} else {
// Send the file type to the pipeline
event.file_type = this_pipeline->current_audio_file_type_;
xQueueSend(this_pipeline->info_error_queue_, &event, portMAX_DELAY);
xEventGroupSetBits(this_pipeline->event_group_, EventGroupBits::READER_MESSAGE_LOADED_MEDIA_TYPE);
}
while (true) {
event_bits = xEventGroupGetBits(this_pipeline->event_group_);
if (event_bits & EventGroupBits::PIPELINE_COMMAND_STOP) {
break;
}
audio::AudioReaderState reader_state = reader->read();
if (reader_state == audio::AudioReaderState::FINISHED) {
break;
} else if (reader_state == audio::AudioReaderState::FAILED) {
xEventGroupSetBits(this_pipeline->event_group_,
EventGroupBits::READER_MESSAGE_ERROR | EventGroupBits::PIPELINE_COMMAND_STOP);
break;
}
}
event_bits = xEventGroupGetBits(this_pipeline->event_group_);
if ((event_bits & EventGroupBits::READER_MESSAGE_LOADED_MEDIA_TYPE) ||
(this_pipeline->raw_file_ring_buffer_.use_count() == 1)) {
// Decoder task hasn't started yet, so delay a bit before releasing ownership of the ring buffer
delay(10);
}
}
}
}
void AudioPipeline::decode_task(void *params) {
AudioPipeline *this_pipeline = (AudioPipeline *) params;
while (true) {
xEventGroupSetBits(this_pipeline->event_group_, EventGroupBits::DECODER_MESSAGE_FINISHED);
// Wait until the reader notifies us that the media type is available
EventBits_t event_bits = xEventGroupWaitBits(this_pipeline->event_group_,
EventGroupBits::READER_MESSAGE_LOADED_MEDIA_TYPE |
EventGroupBits::PIPELINE_COMMAND_STOP, // Bit message to read
pdFALSE, // Clear the bit on exit
pdFALSE, // Wait for all the bits,
portMAX_DELAY); // Block indefinitely until bit is set
if (!(event_bits & EventGroupBits::PIPELINE_COMMAND_STOP)) {
xEventGroupClearBits(this_pipeline->event_group_,
EventGroupBits::DECODER_MESSAGE_FINISHED | EventGroupBits::READER_MESSAGE_LOADED_MEDIA_TYPE);
InfoErrorEvent event;
event.source = InfoErrorSource::DECODER;
std::unique_ptr<audio::AudioDecoder> decoder =
make_unique<audio::AudioDecoder>(this_pipeline->transfer_buffer_size_, this_pipeline->transfer_buffer_size_);
esp_err_t err = decoder->start(this_pipeline->current_audio_file_type_);
decoder->add_source(this_pipeline->raw_file_ring_buffer_);
if (err != ESP_OK) {
// Send specific error message
event.err = err;
xQueueSend(this_pipeline->info_error_queue_, &event, portMAX_DELAY);
// Setting up the decoder failed, stop the pipeline
xEventGroupSetBits(this_pipeline->event_group_,
EventGroupBits::DECODER_MESSAGE_ERROR | EventGroupBits::PIPELINE_COMMAND_STOP);
}
bool has_stream_info = false;
bool started_playback = false;
size_t initial_bytes_to_buffer = 0;
while (true) {
event_bits = xEventGroupGetBits(this_pipeline->event_group_);
if (event_bits & EventGroupBits::PIPELINE_COMMAND_STOP) {
break;
}
// Update pause state
if (!started_playback) {
if (!(event_bits & EventGroupBits::READER_MESSAGE_FINISHED)) {
decoder->set_pause_output_state(true);
} else {
started_playback = true;
}
} else {
decoder->set_pause_output_state(this_pipeline->pause_state_);
}
// Stop gracefully if the reader has finished
audio::AudioDecoderState decoder_state = decoder->decode(event_bits & EventGroupBits::READER_MESSAGE_FINISHED);
if ((decoder_state == audio::AudioDecoderState::DECODING) ||
(decoder_state == audio::AudioDecoderState::FINISHED)) {
this_pipeline->playback_ms_ = decoder->get_playback_ms();
}
if (decoder_state == audio::AudioDecoderState::FINISHED) {
break;
} else if (decoder_state == audio::AudioDecoderState::FAILED) {
if (!has_stream_info) {
event.decoding_err = DecodingError::FAILED_HEADER;
xQueueSend(this_pipeline->info_error_queue_, &event, portMAX_DELAY);
}
xEventGroupSetBits(this_pipeline->event_group_,
EventGroupBits::DECODER_MESSAGE_ERROR | EventGroupBits::PIPELINE_COMMAND_STOP);
break;
}
if (!has_stream_info && decoder->get_audio_stream_info().has_value()) {
has_stream_info = true;
this_pipeline->current_audio_stream_info_ = decoder->get_audio_stream_info().value();
// Send the stream information to the pipeline
event.audio_stream_info = this_pipeline->current_audio_stream_info_;
if (this_pipeline->current_audio_stream_info_.get_bits_per_sample() != 16) {
// Error state, incompatible bits per sample
event.decoding_err = DecodingError::INCOMPATIBLE_BITS_PER_SAMPLE;
xEventGroupSetBits(this_pipeline->event_group_,
EventGroupBits::DECODER_MESSAGE_ERROR | EventGroupBits::PIPELINE_COMMAND_STOP);
} else if ((this_pipeline->current_audio_stream_info_.get_channels() > 2)) {
// Error state, incompatible number of channels
event.decoding_err = DecodingError::INCOMPATIBLE_CHANNELS;
xEventGroupSetBits(this_pipeline->event_group_,
EventGroupBits::DECODER_MESSAGE_ERROR | EventGroupBits::PIPELINE_COMMAND_STOP);
} else {
// Send audio directly to the speaker
this_pipeline->speaker_->set_audio_stream_info(this_pipeline->current_audio_stream_info_);
decoder->add_sink(this_pipeline->speaker_);
}
initial_bytes_to_buffer = std::min(this_pipeline->current_audio_stream_info_.ms_to_bytes(INITIAL_BUFFER_MS),
this_pipeline->buffer_size_ * 3 / 4);
switch (this_pipeline->current_audio_file_type_) {
#ifdef USE_AUDIO_MP3_SUPPORT
case audio::AudioFileType::MP3:
initial_bytes_to_buffer /= 8; // Estimate the MP3 compression factor is 8
break;
#endif
#ifdef USE_AUDIO_FLAC_SUPPORT
case audio::AudioFileType::FLAC:
initial_bytes_to_buffer /= 2; // Estimate the FLAC compression factor is 2
break;
#endif
default:
break;
}
xQueueSend(this_pipeline->info_error_queue_, &event, portMAX_DELAY);
}
if (!started_playback && has_stream_info) {
// Verify enough data is available before starting playback
std::shared_ptr<RingBuffer> temp_ring_buffer = this_pipeline->raw_file_ring_buffer_.lock();
if (temp_ring_buffer->available() >= initial_bytes_to_buffer) {
started_playback = true;
}
}
}
}
}
}
} // namespace speaker
} // namespace esphome
#endif

View File

@ -0,0 +1,159 @@
#pragma once
#ifdef USE_ESP_IDF
#include "esphome/components/audio/audio.h"
#include "esphome/components/audio/audio_reader.h"
#include "esphome/components/audio/audio_decoder.h"
#include "esphome/components/speaker/speaker.h"
#include "esphome/core/ring_buffer.h"
#include "esp_err.h"
#include <freertos/FreeRTOS.h>
#include <freertos/event_groups.h>
#include <freertos/queue.h>
namespace esphome {
namespace speaker {
// Internal sink/source buffers for reader and decoder
static const size_t DEFAULT_TRANSFER_BUFFER_SIZE = 24 * 1024;
enum class AudioPipelineType : uint8_t {
MEDIA,
ANNOUNCEMENT,
};
enum class AudioPipelineState : uint8_t {
STARTING_FILE,
STARTING_URL,
PLAYING,
STOPPING,
STOPPED,
PAUSED,
ERROR_READING,
ERROR_DECODING,
};
enum class InfoErrorSource : uint8_t {
READER = 0,
DECODER,
};
enum class DecodingError : uint8_t {
FAILED_HEADER = 0,
INCOMPATIBLE_BITS_PER_SAMPLE,
INCOMPATIBLE_CHANNELS,
};
// Used to pass information from each task.
struct InfoErrorEvent {
InfoErrorSource source;
optional<esp_err_t> err;
optional<audio::AudioFileType> file_type;
optional<audio::AudioStreamInfo> audio_stream_info;
optional<DecodingError> decoding_err;
};
class AudioPipeline {
public:
/// @param speaker ESPHome speaker component for pipeline's audio output
/// @param buffer_size Size of the buffer in bytes between the reader and decoder
/// @param task_stack_in_psram True if the task stack should be allocated in PSRAM, false otherwise
/// @param task_name FreeRTOS task base name
/// @param priority FreeRTOS task priority
AudioPipeline(speaker::Speaker *speaker, size_t buffer_size, bool task_stack_in_psram, std::string base_name,
UBaseType_t priority);
/// @brief Starts an audio pipeline given a media url
/// @param uri media file url
/// @return ESP_OK if successful or an appropriate error if not
void start_url(const std::string &uri);
/// @brief Starts an audio pipeline given a AudioFile pointer
/// @param audio_file pointer to an AudioFile object
/// @return ESP_OK if successful or an appropriate error if not
void start_file(audio::AudioFile *audio_file);
/// @brief Stops the pipeline. Sends a stop signal to each task (if running) and clears the ring buffers.
/// @return ESP_OK if successful or ESP_ERR_TIMEOUT if the tasks did not indicate they stopped
esp_err_t stop();
/// @brief Processes the state of the audio pipeline based on the info_error_queue_ and event_group_. Handles creating
/// and stopping the pipeline tasks. Needs to be regularly called to update the internal pipeline state.
/// @return AudioPipelineState
AudioPipelineState process_state();
/// @brief Suspends any running tasks
void suspend_tasks();
/// @brief Resumes any running tasks
void resume_tasks();
uint32_t get_playback_ms() { return this->playback_ms_; }
void set_pause_state(bool pause_state);
protected:
/// @brief Allocates the event group and info error queue.
/// @return ESP_OK if successful or ESP_ERR_NO_MEM if it is unable to allocate all parts
esp_err_t allocate_communications_();
/// @brief Common start code for the pipeline, regardless if the source is a file or url.
/// @return ESP_OK if successful or an appropriate error if not
esp_err_t start_tasks_();
/// @brief Resets the task related pointers and deallocates their stacks.
void delete_tasks_();
std::string base_name_;
UBaseType_t priority_;
uint32_t playback_ms_{0};
bool hard_stop_{false};
bool is_playing_{false};
bool pause_state_{false};
bool task_stack_in_psram_;
// Pending file start state used to ensure the pipeline fully stops before attempting to start the next file
bool pending_url_{false};
bool pending_file_{false};
speaker::Speaker *speaker_{nullptr};
std::string current_uri_{};
audio::AudioFile *current_audio_file_{nullptr};
audio::AudioFileType current_audio_file_type_;
audio::AudioStreamInfo current_audio_stream_info_;
size_t buffer_size_; // Ring buffer between reader and decoder
size_t transfer_buffer_size_; // Internal source/sink buffers for the audio reader and decoder
std::weak_ptr<RingBuffer> raw_file_ring_buffer_;
// Handles basic control/state of the three tasks
EventGroupHandle_t event_group_{nullptr};
// Receives detailed info (file type, stream info, resampling info) or specific errors from the three tasks
QueueHandle_t info_error_queue_{nullptr};
// Handles reading the media file from flash or a url
static void read_task(void *params);
TaskHandle_t read_task_handle_{nullptr};
StaticTask_t read_task_stack_;
StackType_t *read_task_stack_buffer_{nullptr};
// Decodes the media file into PCM audio
static void decode_task(void *params);
TaskHandle_t decode_task_handle_{nullptr};
StaticTask_t decode_task_stack_;
StackType_t *decode_task_stack_buffer_{nullptr};
};
} // namespace speaker
} // namespace esphome
#endif

View File

@ -0,0 +1,26 @@
#pragma once
#include "speaker_media_player.h"
#ifdef USE_ESP_IDF
#include "esphome/components/audio/audio.h"
#include "esphome/core/automation.h"
namespace esphome {
namespace speaker {
template<typename... Ts> class PlayOnDeviceMediaAction : public Action<Ts...>, public Parented<SpeakerMediaPlayer> {
TEMPLATABLE_VALUE(audio::AudioFile *, audio_file)
TEMPLATABLE_VALUE(bool, announcement)
TEMPLATABLE_VALUE(bool, enqueue)
void play(Ts... x) override {
this->parent_->play_file(this->audio_file_.value(x...), this->announcement_.value(x...),
this->enqueue_.value(x...));
}
};
} // namespace speaker
} // namespace esphome
#endif

View File

@ -0,0 +1,577 @@
#include "speaker_media_player.h"
#ifdef USE_ESP_IDF
#include "esphome/core/log.h"
#include "esphome/components/audio/audio.h"
#ifdef USE_OTA
#include "esphome/components/ota/ota_backend.h"
#endif
namespace esphome {
namespace speaker {
// Framework:
// - Media player that can handle two streams: one for media and one for announcements
// - Each stream has an individual speaker component for output
// - Each stream is handled by an ``AudioPipeline`` object with two parts/tasks
// - ``AudioReader`` handles reading from an HTTP source or from a PROGMEM flash set at compile time
// - ``AudioDecoder`` handles decoding the audio file. All formats are limited to two channels and 16 bits per sample
// - FLAC
// - MP3 (based on the libhelix decoder)
// - WAV
// - Each task runs until it is done processing the file or it receives a stop command
// - Inter-task communication uses a FreeRTOS Event Group
// - The ``AudioPipeline`` sets up a ring buffer between the reader and decoder tasks. The decoder task outputs audio
// directly to a speaker component.
// - The pipelines internal state needs to be processed by regularly calling ``process_state``.
// - Generic media player commands are received by the ``control`` function. The commands are added to the
// ``media_control_command_queue_`` to be processed in the component's loop
// - Local file play back is initiatied with ``play_file`` and adds it to the ``media_control_command_queue_``
// - Starting a stream intializes the appropriate pipeline or stops it if it is already running
// - Volume and mute commands are achieved by the ``mute``, ``unmute``, ``set_volume`` functions.
// - Volume commands are ignored if the media control queue is full to avoid crashing with rapid volume
// increases/decreases.
// - These functions all send the appropriate information to the speakers to implement.
// - Pausing is implemented in the decoder task and is also sent directly to the media speaker component to decrease
// latency.
// - The components main loop performs housekeeping:
// - It reads the media control queue and processes it directly
// - It determines the overall state of the media player by considering the state of each pipeline
// - announcement playback takes highest priority
// - Handles playlists and repeating by starting the appropriate file when a previous file is finished
// - Logging only happens in the main loop task to reduce task stack memory usage.
static const uint32_t MEDIA_CONTROLS_QUEUE_LENGTH = 20;
static const UBaseType_t MEDIA_PIPELINE_TASK_PRIORITY = 1;
static const UBaseType_t ANNOUNCEMENT_PIPELINE_TASK_PRIORITY = 1;
static const float FIRST_BOOT_DEFAULT_VOLUME = 0.5f;
static const char *const TAG = "speaker_media_player";
void SpeakerMediaPlayer::setup() {
state = media_player::MEDIA_PLAYER_STATE_IDLE;
this->media_control_command_queue_ = xQueueCreate(MEDIA_CONTROLS_QUEUE_LENGTH, sizeof(MediaCallCommand));
this->pref_ = global_preferences->make_preference<VolumeRestoreState>(this->get_object_id_hash());
VolumeRestoreState volume_restore_state;
if (this->pref_.load(&volume_restore_state)) {
this->set_volume_(volume_restore_state.volume);
this->set_mute_state_(volume_restore_state.is_muted);
} else {
this->set_volume_(FIRST_BOOT_DEFAULT_VOLUME);
this->set_mute_state_(false);
}
#ifdef USE_OTA
ota::get_global_ota_callback()->add_on_state_callback(
[this](ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) {
if (state == ota::OTA_STARTED) {
if (this->media_pipeline_ != nullptr) {
this->media_pipeline_->suspend_tasks();
}
if (this->announcement_pipeline_ != nullptr) {
this->announcement_pipeline_->suspend_tasks();
}
} else if (state == ota::OTA_ERROR) {
if (this->media_pipeline_ != nullptr) {
this->media_pipeline_->resume_tasks();
}
if (this->announcement_pipeline_ != nullptr) {
this->announcement_pipeline_->resume_tasks();
}
}
});
#endif
this->announcement_pipeline_ =
make_unique<AudioPipeline>(this->announcement_speaker_, this->buffer_size_, this->task_stack_in_psram_, "ann",
ANNOUNCEMENT_PIPELINE_TASK_PRIORITY);
if (this->announcement_pipeline_ == nullptr) {
ESP_LOGE(TAG, "Failed to create announcement pipeline");
this->mark_failed();
}
if (!this->single_pipeline_()) {
this->media_pipeline_ = make_unique<AudioPipeline>(this->media_speaker_, this->buffer_size_,
this->task_stack_in_psram_, "ann", MEDIA_PIPELINE_TASK_PRIORITY);
if (this->media_pipeline_ == nullptr) {
ESP_LOGE(TAG, "Failed to create media pipeline");
this->mark_failed();
}
// Setup callback to track the duration of audio played by the media pipeline
this->media_speaker_->add_audio_output_callback(
[this](uint32_t new_playback_ms, uint32_t remainder_us, uint32_t pending_ms, uint32_t write_timestamp) {
this->playback_ms_ += new_playback_ms;
this->remainder_us_ = remainder_us;
this->pending_ms_ = pending_ms;
this->last_audio_write_timestamp_ = write_timestamp;
this->playback_us_ = this->playback_ms_ * 1000 + this->remainder_us_;
});
}
ESP_LOGI(TAG, "Set up speaker media player");
}
void SpeakerMediaPlayer::set_playlist_delay_ms(AudioPipelineType pipeline_type, uint32_t delay_ms) {
switch (pipeline_type) {
case AudioPipelineType::ANNOUNCEMENT:
this->announcement_playlist_delay_ms_ = delay_ms;
break;
case AudioPipelineType::MEDIA:
this->media_playlist_delay_ms_ = delay_ms;
break;
}
}
void SpeakerMediaPlayer::watch_media_commands_() {
if (!this->is_ready()) {
return;
}
MediaCallCommand media_command;
esp_err_t err = ESP_OK;
if (xQueueReceive(this->media_control_command_queue_, &media_command, 0) == pdTRUE) {
bool new_url = media_command.new_url.has_value() && media_command.new_url.value();
bool new_file = media_command.new_file.has_value() && media_command.new_file.value();
if (new_url || new_file) {
bool enqueue = media_command.enqueue.has_value() && media_command.enqueue.value();
if (this->single_pipeline_() || (media_command.announce.has_value() && media_command.announce.value())) {
// Announcement playlist/pipeline
if (!enqueue) {
// Clear the queue and ensure the loaded next item doesn't start playing
this->cancel_timeout("next_ann");
this->announcement_playlist_.clear();
}
PlaylistItem playlist_item;
if (new_url) {
playlist_item.url = this->announcement_url_;
if (!enqueue) {
// Not adding to the queue, so directly start playback and internally unpause the pipeline
this->announcement_pipeline_->start_url(playlist_item.url.value());
this->announcement_pipeline_->set_pause_state(false);
}
} else {
playlist_item.file = this->announcement_file_;
if (!enqueue) {
// Not adding to the queue, so directly start playback and internally unpause the pipeline
this->announcement_pipeline_->start_file(playlist_item.file.value());
this->announcement_pipeline_->set_pause_state(false);
}
}
this->announcement_playlist_.push_back(playlist_item);
} else {
// Media playlist/pipeline
if (!enqueue) {
// Clear the queue and ensure the loaded next item doesn't start playing
this->cancel_timeout("next_media");
this->media_playlist_.clear();
}
this->is_paused_ = false;
PlaylistItem playlist_item;
if (new_url) {
playlist_item.url = this->media_url_;
if (!enqueue) {
// Not adding to the queue, so directly start playback and internally unpause the pipeline
this->media_pipeline_->start_url(playlist_item.url.value());
this->media_pipeline_->set_pause_state(false);
}
} else {
playlist_item.file = this->media_file_;
if (!enqueue) {
// Not adding to the queue, so directly start playback and internally unpause the pipeline
this->media_pipeline_->start_file(playlist_item.file.value());
this->media_pipeline_->set_pause_state(false);
}
}
this->media_playlist_.push_back(playlist_item);
}
if (err != ESP_OK) {
ESP_LOGE(TAG, "Error starting the audio pipeline: %s", esp_err_to_name(err));
this->status_set_error();
} else {
this->status_clear_error();
}
return; // Don't process the new file play command further
}
if (media_command.volume.has_value()) {
this->set_volume_(media_command.volume.value());
this->publish_state();
}
if (media_command.command.has_value()) {
switch (media_command.command.value()) {
case media_player::MEDIA_PLAYER_COMMAND_PLAY:
if ((this->media_pipeline_ != nullptr) && (this->is_paused_)) {
this->media_pipeline_->set_pause_state(false);
}
this->is_paused_ = false;
break;
case media_player::MEDIA_PLAYER_COMMAND_PAUSE:
if ((this->media_pipeline_ != nullptr) && (!this->is_paused_)) {
this->media_pipeline_->set_pause_state(true);
}
this->is_paused_ = true;
break;
case media_player::MEDIA_PLAYER_COMMAND_STOP:
if (this->single_pipeline_() || (media_command.announce.has_value() && media_command.announce.value())) {
if (this->announcement_pipeline_ != nullptr) {
this->cancel_timeout("next_ann");
this->announcement_playlist_.clear();
this->announcement_pipeline_->stop();
}
} else {
if (this->media_pipeline_ != nullptr) {
this->cancel_timeout("next_media");
this->media_playlist_.clear();
this->media_pipeline_->stop();
}
}
break;
case media_player::MEDIA_PLAYER_COMMAND_TOGGLE:
if (this->media_pipeline_ != nullptr) {
if (this->is_paused_) {
this->media_pipeline_->set_pause_state(false);
this->is_paused_ = false;
} else {
this->media_pipeline_->set_pause_state(true);
this->is_paused_ = true;
}
}
break;
case media_player::MEDIA_PLAYER_COMMAND_MUTE: {
this->set_mute_state_(true);
this->publish_state();
break;
}
case media_player::MEDIA_PLAYER_COMMAND_UNMUTE:
this->set_mute_state_(false);
this->publish_state();
break;
case media_player::MEDIA_PLAYER_COMMAND_VOLUME_UP:
this->set_volume_(std::min(1.0f, this->volume + this->volume_increment_));
this->publish_state();
break;
case media_player::MEDIA_PLAYER_COMMAND_VOLUME_DOWN:
this->set_volume_(std::max(0.0f, this->volume - this->volume_increment_));
this->publish_state();
break;
case media_player::MEDIA_PLAYER_COMMAND_REPEAT_ONE:
if (this->single_pipeline_() || (media_command.announce.has_value() && media_command.announce.value())) {
this->announcement_repeat_one_ = true;
} else {
this->media_repeat_one_ = true;
}
break;
case media_player::MEDIA_PLAYER_COMMAND_REPEAT_OFF:
if (this->single_pipeline_() || (media_command.announce.has_value() && media_command.announce.value())) {
this->announcement_repeat_one_ = false;
} else {
this->media_repeat_one_ = false;
}
break;
case media_player::MEDIA_PLAYER_COMMAND_CLEAR_PLAYLIST:
if (this->single_pipeline_() || (media_command.announce.has_value() && media_command.announce.value())) {
if (this->announcement_playlist_.empty()) {
this->announcement_playlist_.resize(1);
}
} else {
if (this->media_playlist_.empty()) {
this->media_playlist_.resize(1);
}
}
break;
default:
break;
}
}
}
}
void SpeakerMediaPlayer::loop() {
this->watch_media_commands_();
// Determine state of the media player
media_player::MediaPlayerState old_state = this->state;
AudioPipelineState old_media_pipeline_state = this->media_pipeline_state_;
if (this->media_pipeline_ != nullptr) {
this->media_pipeline_state_ = this->media_pipeline_->process_state();
this->decoded_playback_ms_ = this->media_pipeline_->get_playback_ms();
}
if (this->media_pipeline_state_ == AudioPipelineState::ERROR_READING) {
ESP_LOGE(TAG, "The media pipeline's file reader encountered an error.");
} else if (this->media_pipeline_state_ == AudioPipelineState::ERROR_DECODING) {
ESP_LOGE(TAG, "The media pipeline's audio decoder encountered an error.");
}
AudioPipelineState old_announcement_pipeline_state = this->announcement_pipeline_state_;
if (this->announcement_pipeline_ != nullptr) {
this->announcement_pipeline_state_ = this->announcement_pipeline_->process_state();
}
if (this->announcement_pipeline_state_ == AudioPipelineState::ERROR_READING) {
ESP_LOGE(TAG, "The announcement pipeline's file reader encountered an error.");
} else if (this->announcement_pipeline_state_ == AudioPipelineState::ERROR_DECODING) {
ESP_LOGE(TAG, "The announcement pipeline's audio decoder encountered an error.");
}
if (this->announcement_pipeline_state_ != AudioPipelineState::STOPPED) {
this->state = media_player::MEDIA_PLAYER_STATE_ANNOUNCING;
} else {
if (!this->announcement_playlist_.empty()) {
uint32_t timeout_ms = 0;
if (old_announcement_pipeline_state == AudioPipelineState::PLAYING) {
// Finished the current announcement file
if (!this->announcement_repeat_one_) {
// Pop item off the playlist if repeat is disabled
this->announcement_playlist_.pop_front();
}
// Only delay starting playback if moving on the next playlist item or repeating the current item
timeout_ms = this->announcement_playlist_delay_ms_;
}
if (!this->announcement_playlist_.empty()) {
// Start the next announcement file
PlaylistItem playlist_item = this->announcement_playlist_.front();
if (playlist_item.url.has_value()) {
this->announcement_pipeline_->start_url(playlist_item.url.value());
} else if (playlist_item.file.has_value()) {
this->announcement_pipeline_->start_file(playlist_item.file.value());
}
if (timeout_ms > 0) {
// Pause pipeline internally to facilitiate delay between items
this->announcement_pipeline_->set_pause_state(true);
// Internally unpause the pipeline after the delay between playlist items
this->set_timeout("next_ann", timeout_ms,
[this]() { this->announcement_pipeline_->set_pause_state(this->is_paused_); });
}
}
} else {
if (this->is_paused_) {
this->state = media_player::MEDIA_PLAYER_STATE_PAUSED;
} else if (this->media_pipeline_state_ == AudioPipelineState::PLAYING) {
this->state = media_player::MEDIA_PLAYER_STATE_PLAYING;
} else if (this->media_pipeline_state_ == AudioPipelineState::STOPPED) {
// Reset playback durations
this->decoded_playback_ms_ = 0;
this->playback_us_ = 0;
this->playback_ms_ = 0;
this->remainder_us_ = 0;
this->pending_ms_ = 0;
if (!media_playlist_.empty()) {
uint32_t timeout_ms = 0;
if (old_media_pipeline_state == AudioPipelineState::PLAYING) {
// Finished the current media file
if (!this->media_repeat_one_) {
// Pop item off the playlist if repeat is disabled
this->media_playlist_.pop_front();
}
// Only delay starting playback if moving on the next playlist item or repeating the current item
timeout_ms = this->announcement_playlist_delay_ms_;
}
if (!this->media_playlist_.empty()) {
PlaylistItem playlist_item = this->media_playlist_.front();
if (playlist_item.url.has_value()) {
this->media_pipeline_->start_url(playlist_item.url.value());
} else if (playlist_item.file.has_value()) {
this->media_pipeline_->start_file(playlist_item.file.value());
}
if (timeout_ms > 0) {
// Pause pipeline internally to facilitiate delay between items
this->media_pipeline_->set_pause_state(true);
// Internally unpause the pipeline after the delay between playlist items
this->set_timeout("next_media", timeout_ms,
[this]() { this->media_pipeline_->set_pause_state(this->is_paused_); });
}
}
} else {
this->state = media_player::MEDIA_PLAYER_STATE_IDLE;
}
}
}
}
if (this->state != old_state) {
this->publish_state();
ESP_LOGD(TAG, "State changed to %s", media_player::media_player_state_to_string(this->state));
}
}
void SpeakerMediaPlayer::play_file(audio::AudioFile *media_file, bool announcement, bool enqueue) {
if (!this->is_ready()) {
// Ignore any commands sent before the media player is setup
return;
}
MediaCallCommand media_command;
media_command.new_file = true;
if (this->single_pipeline_() || announcement) {
this->announcement_file_ = media_file;
media_command.announce = true;
} else {
this->media_file_ = media_file;
media_command.announce = false;
}
media_command.enqueue = enqueue;
xQueueSend(this->media_control_command_queue_, &media_command, portMAX_DELAY);
}
void SpeakerMediaPlayer::control(const media_player::MediaPlayerCall &call) {
if (!this->is_ready()) {
// Ignore any commands sent before the media player is setup
return;
}
MediaCallCommand media_command;
if (this->single_pipeline_() || (call.get_announcement().has_value() && call.get_announcement().value())) {
media_command.announce = true;
} else {
media_command.announce = false;
}
if (call.get_media_url().has_value()) {
std::string new_uri = call.get_media_url().value();
media_command.new_url = true;
if (this->single_pipeline_() || (call.get_announcement().has_value() && call.get_announcement().value())) {
this->announcement_url_ = new_uri;
} else {
this->media_url_ = new_uri;
}
if (call.get_command().has_value()) {
if (call.get_command().value() == media_player::MEDIA_PLAYER_COMMAND_ENQUEUE) {
media_command.enqueue = true;
}
}
xQueueSend(this->media_control_command_queue_, &media_command, portMAX_DELAY);
return;
}
if (call.get_volume().has_value()) {
media_command.volume = call.get_volume().value();
// Wait 0 ticks for queue to be free, volume sets aren't that important!
xQueueSend(this->media_control_command_queue_, &media_command, 0);
return;
}
if (call.get_command().has_value()) {
media_command.command = call.get_command().value();
TickType_t ticks_to_wait = portMAX_DELAY;
if ((call.get_command().value() == media_player::MEDIA_PLAYER_COMMAND_VOLUME_UP) ||
(call.get_command().value() == media_player::MEDIA_PLAYER_COMMAND_VOLUME_DOWN)) {
ticks_to_wait = 0; // Wait 0 ticks for queue to be free, volume sets aren't that important!
}
xQueueSend(this->media_control_command_queue_, &media_command, ticks_to_wait);
return;
}
}
media_player::MediaPlayerTraits SpeakerMediaPlayer::get_traits() {
auto traits = media_player::MediaPlayerTraits();
if (!this->single_pipeline_()) {
traits.set_supports_pause(true);
}
if (this->announcement_format_.has_value()) {
traits.get_supported_formats().push_back(this->announcement_format_.value());
}
if (this->media_format_.has_value()) {
traits.get_supported_formats().push_back(this->media_format_.value());
} else if (this->single_pipeline_() && this->announcement_format_.has_value()) {
// Only one pipeline is defined, so use the announcement format (if configured) for the default purpose
media_player::MediaPlayerSupportedFormat media_format = this->announcement_format_.value();
media_format.purpose = media_player::MediaPlayerFormatPurpose::PURPOSE_DEFAULT;
traits.get_supported_formats().push_back(media_format);
}
return traits;
};
void SpeakerMediaPlayer::save_volume_restore_state_() {
VolumeRestoreState volume_restore_state;
volume_restore_state.volume = this->volume;
volume_restore_state.is_muted = this->is_muted_;
this->pref_.save(&volume_restore_state);
}
void SpeakerMediaPlayer::set_mute_state_(bool mute_state) {
if (this->media_speaker_ != nullptr) {
this->media_speaker_->set_mute_state(mute_state);
}
if (this->announcement_speaker_ != nullptr) {
this->announcement_speaker_->set_mute_state(mute_state);
}
bool old_mute_state = this->is_muted_;
this->is_muted_ = mute_state;
this->save_volume_restore_state_();
if (old_mute_state != mute_state) {
if (mute_state) {
this->defer([this]() { this->mute_trigger_->trigger(); });
} else {
this->defer([this]() { this->unmute_trigger_->trigger(); });
}
}
}
void SpeakerMediaPlayer::set_volume_(float volume, bool publish) {
// Remap the volume to fit with in the configured limits
float bounded_volume = remap<float, float>(volume, 0.0f, 1.0f, this->volume_min_, this->volume_max_);
if (this->media_speaker_ != nullptr) {
this->media_speaker_->set_volume(bounded_volume);
}
if (this->announcement_speaker_ != nullptr) {
this->announcement_speaker_->set_volume(bounded_volume);
}
if (publish) {
this->volume = volume;
this->save_volume_restore_state_();
}
// Turn on the mute state if the volume is effectively zero, off otherwise
if (volume < 0.001) {
this->set_mute_state_(true);
} else {
this->set_mute_state_(false);
}
this->defer([this, volume]() { this->volume_trigger_->trigger(volume); });
}
} // namespace speaker
} // namespace esphome
#endif

View File

@ -0,0 +1,160 @@
#pragma once
#ifdef USE_ESP_IDF
#include "audio_pipeline.h"
#include "esphome/components/audio/audio.h"
#include "esphome/components/media_player/media_player.h"
#include "esphome/components/speaker/speaker.h"
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/preferences.h"
#include <deque>
#include <freertos/FreeRTOS.h>
#include <freertos/queue.h>
namespace esphome {
namespace speaker {
struct MediaCallCommand {
optional<media_player::MediaPlayerCommand> command;
optional<float> volume;
optional<bool> announce;
optional<bool> new_url;
optional<bool> new_file;
optional<bool> enqueue;
};
struct PlaylistItem {
optional<std::string> url;
optional<audio::AudioFile *> file;
};
struct VolumeRestoreState {
float volume;
bool is_muted;
};
class SpeakerMediaPlayer : public Component, public media_player::MediaPlayer {
public:
float get_setup_priority() const override { return esphome::setup_priority::PROCESSOR; }
void setup() override;
void loop() override;
// MediaPlayer implementations
media_player::MediaPlayerTraits get_traits() override;
bool is_muted() const override { return this->is_muted_; }
void set_buffer_size(size_t buffer_size) { this->buffer_size_ = buffer_size; }
void set_task_stack_in_psram(bool task_stack_in_psram) { this->task_stack_in_psram_ = task_stack_in_psram; }
// Percentage to increase or decrease the volume for volume up or volume down commands
void set_volume_increment(float volume_increment) { this->volume_increment_ = volume_increment; }
void set_volume_max(float volume_max) { this->volume_max_ = volume_max; }
void set_volume_min(float volume_min) { this->volume_min_ = volume_min; }
void set_announcement_speaker(Speaker *announcement_speaker) { this->announcement_speaker_ = announcement_speaker; }
void set_announcement_format(const media_player::MediaPlayerSupportedFormat &announcement_format) {
this->announcement_format_ = announcement_format;
}
void set_media_speaker(Speaker *media_speaker) { this->media_speaker_ = media_speaker; }
void set_media_format(const media_player::MediaPlayerSupportedFormat &media_format) {
this->media_format_ = media_format;
}
Trigger<> *get_mute_trigger() const { return this->mute_trigger_; }
Trigger<> *get_unmute_trigger() const { return this->unmute_trigger_; }
Trigger<float> *get_volume_trigger() const { return this->volume_trigger_; }
void play_file(audio::AudioFile *media_file, bool announcement, bool enqueue);
uint32_t get_playback_ms() const { return this->playback_ms_; }
uint32_t get_playback_us() const { return this->playback_us_; }
uint32_t get_decoded_playback_ms() const { return this->decoded_playback_ms_; }
void set_playlist_delay_ms(AudioPipelineType pipeline_type, uint32_t delay_ms);
protected:
// Receives commands from HA or from the voice assistant component
// Sends commands to the media_control_commanda_queue_
void control(const media_player::MediaPlayerCall &call) override;
/// @brief Updates this->volume and saves volume/mute state to flash for restortation if publish is true.
void set_volume_(float volume, bool publish = true);
/// @brief Sets the mute state. Restores previous volume if unmuting. Always saves volume/mute state to flash for
/// restoration.
/// @param mute_state If true, audio will be muted. If false, audio will be unmuted
void set_mute_state_(bool mute_state);
/// @brief Saves the current volume and mute state to the flash for restoration.
void save_volume_restore_state_();
/// Returns true if the media player has only the announcement pipeline defined, false if both the announcement and
/// media pipelines are defined.
inline bool single_pipeline_() { return (this->media_speaker_ == nullptr); }
// Processes commands from media_control_command_queue_.
void watch_media_commands_();
std::unique_ptr<AudioPipeline> announcement_pipeline_;
std::unique_ptr<AudioPipeline> media_pipeline_;
Speaker *media_speaker_{nullptr};
Speaker *announcement_speaker_{nullptr};
optional<media_player::MediaPlayerSupportedFormat> media_format_;
AudioPipelineState media_pipeline_state_{AudioPipelineState::STOPPED};
std::string media_url_{}; // only modified by control function
audio::AudioFile *media_file_{}; // only modified by play_file function
bool media_repeat_one_{false};
uint32_t media_playlist_delay_ms_{0};
optional<media_player::MediaPlayerSupportedFormat> announcement_format_;
AudioPipelineState announcement_pipeline_state_{AudioPipelineState::STOPPED};
std::string announcement_url_{}; // only modified by control function
audio::AudioFile *announcement_file_{}; // only modified by play_file function
bool announcement_repeat_one_{false};
uint32_t announcement_playlist_delay_ms_{0};
QueueHandle_t media_control_command_queue_;
std::deque<PlaylistItem> announcement_playlist_;
std::deque<PlaylistItem> media_playlist_;
size_t buffer_size_;
bool task_stack_in_psram_;
bool is_paused_{false};
bool is_muted_{false};
// The amount to change the volume on volume up/down commands
float volume_increment_;
float volume_max_;
float volume_min_;
// Used to save volume/mute state for restoration on reboot
ESPPreferenceObject pref_;
Trigger<> *mute_trigger_ = new Trigger<>();
Trigger<> *unmute_trigger_ = new Trigger<>();
Trigger<float> *volume_trigger_ = new Trigger<float>();
uint32_t decoded_playback_ms_{0};
uint32_t playback_us_{0};
uint32_t playback_ms_{0};
uint32_t remainder_us_{0};
uint32_t pending_ms_{0};
uint32_t last_audio_write_timestamp_{0};
};
} // namespace speaker
} // namespace esphome
#endif

View File

@ -137,7 +137,7 @@ def validate_temperature_preset(preset, root_config, name, requirements):
def generate_comparable_preset(config, name):
comparable_preset = f"{CONF_PRESET}:\n" f" - {CONF_NAME}: {name}\n"
comparable_preset = f"{CONF_PRESET}:\n - {CONF_NAME}: {name}\n"
if CONF_DEFAULT_TARGET_TEMPERATURE_LOW in config:
comparable_preset += f" {CONF_DEFAULT_TARGET_TEMPERATURE_LOW}: {config[CONF_DEFAULT_TARGET_TEMPERATURE_LOW]}\n"

View File

@ -1,4 +1,5 @@
#include "voice_assistant.h"
#include "esphome/core/defines.h"
#ifdef USE_VOICE_ASSISTANT
@ -127,7 +128,7 @@ void VoiceAssistant::clear_buffers_() {
}
#ifdef USE_SPEAKER
if (this->speaker_buffer_ != nullptr) {
if ((this->speaker_ != nullptr) && (this->speaker_buffer_ != nullptr)) {
memset(this->speaker_buffer_, 0, SPEAKER_BUFFER_SIZE);
this->speaker_buffer_size_ = 0;
@ -159,7 +160,7 @@ void VoiceAssistant::deallocate_buffers_() {
this->input_buffer_ = nullptr;
#ifdef USE_SPEAKER
if (this->speaker_buffer_ != nullptr) {
if ((this->speaker_ != nullptr) && (this->speaker_buffer_ != nullptr)) {
ExternalRAMAllocator<uint8_t> speaker_deallocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
speaker_deallocator.deallocate(this->speaker_buffer_, SPEAKER_BUFFER_SIZE);
this->speaker_buffer_ = nullptr;
@ -389,14 +390,7 @@ void VoiceAssistant::loop() {
}
#endif
if (playing) {
this->set_timeout("playing", 2000, [this]() {
this->cancel_timeout("speaker-timeout");
this->set_state_(State::IDLE, State::IDLE);
api::VoiceAssistantAnnounceFinished msg;
msg.success = true;
this->api_client_->send_voice_assistant_announce_finished(msg);
});
this->start_playback_timeout_();
}
break;
}
@ -614,6 +608,8 @@ void VoiceAssistant::request_stop() {
this->desired_state_ = State::IDLE;
break;
case State::AWAITING_RESPONSE:
this->signal_stop_();
break;
case State::STREAMING_RESPONSE:
case State::RESPONSE_FINISHED:
break; // Let the incoming audio stream finish then it will go to idle.
@ -631,6 +627,17 @@ void VoiceAssistant::signal_stop_() {
this->api_client_->send_voice_assistant_request(msg);
}
void VoiceAssistant::start_playback_timeout_() {
this->set_timeout("playing", 100, [this]() {
this->cancel_timeout("speaker-timeout");
this->set_state_(State::IDLE, State::IDLE);
api::VoiceAssistantAnnounceFinished msg;
msg.success = true;
this->api_client_->send_voice_assistant_announce_finished(msg);
});
}
void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) {
ESP_LOGD(TAG, "Event Type: %" PRId32, msg.event_type);
switch (msg.event_type) {
@ -715,6 +722,8 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) {
#ifdef USE_MEDIA_PLAYER
if (this->media_player_ != nullptr) {
this->media_player_->make_call().set_media_url(url).set_announcement(true).perform();
// Start the playback timeout, as the media player state isn't immediately updated
this->start_playback_timeout_();
}
#endif
this->tts_end_trigger_->trigger(url);
@ -725,7 +734,11 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) {
}
case api::enums::VOICE_ASSISTANT_RUN_END: {
ESP_LOGD(TAG, "Assist Pipeline ended");
if (this->state_ == State::STREAMING_MICROPHONE) {
if ((this->state_ == State::STARTING_PIPELINE) || (this->state_ == State::AWAITING_RESPONSE)) {
// Pipeline ended before starting microphone
// Or there wasn't a TTS start event ("nevermind")
this->set_state_(State::IDLE, State::IDLE);
} else if (this->state_ == State::STREAMING_MICROPHONE) {
this->ring_buffer_->reset();
#ifdef USE_ESP_ADF
if (this->use_wake_word_) {
@ -736,9 +749,6 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) {
{
this->set_state_(State::IDLE, State::IDLE);
}
} else if (this->state_ == State::AWAITING_RESPONSE) {
// No TTS start event ("nevermind")
this->set_state_(State::IDLE, State::IDLE);
}
this->defer([this]() { this->end_trigger_->trigger(); });
break;

View File

@ -40,6 +40,7 @@ enum VoiceAssistantFeature : uint32_t {
FEATURE_SPEAKER = 1 << 1,
FEATURE_API_AUDIO = 1 << 2,
FEATURE_TIMERS = 1 << 3,
FEATURE_ANNOUNCE = 1 << 4,
};
enum class State {
@ -136,6 +137,12 @@ class VoiceAssistant : public Component {
flags |= VoiceAssistantFeature::FEATURE_TIMERS;
}
#ifdef USE_MEDIA_PLAYER
if (this->media_player_ != nullptr) {
flags |= VoiceAssistantFeature::FEATURE_ANNOUNCE;
}
#endif
return flags;
}
@ -209,6 +216,7 @@ class VoiceAssistant : public Component {
void set_state_(State state);
void set_state_(State state, State desired_state);
void signal_stop_();
void start_playback_timeout_();
std::unique_ptr<socket::Socket> socket_ = nullptr;
struct sockaddr_storage dest_addr_;

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