mirror of
https://github.com/esphome/esphome.git
synced 2025-03-13 22:28:14 +00:00
Merge branch 'dev' into download
This commit is contained in:
commit
a6b5668282
@ -31,7 +31,7 @@
|
||||
"ms-python.python",
|
||||
"ms-python.pylint",
|
||||
"ms-python.flake8",
|
||||
"ms-python.black-formatter",
|
||||
"charliermarsh.ruff",
|
||||
"visualstudioexptteam.vscodeintellicode",
|
||||
// yaml
|
||||
"redhat.vscode-yaml",
|
||||
@ -49,14 +49,11 @@
|
||||
"flake8.args": [
|
||||
"--config=${workspaceFolder}/.flake8"
|
||||
],
|
||||
"black-formatter.args": [
|
||||
"--config",
|
||||
"${workspaceFolder}/pyproject.toml"
|
||||
],
|
||||
"ruff.configuration": "${workspaceFolder}/pyproject.toml",
|
||||
"[python]": {
|
||||
// VS will say "Value is not accepted" before building the devcontainer, but the warning
|
||||
// should go away after build is completed.
|
||||
"editor.defaultFormatter": "ms-python.black-formatter"
|
||||
"editor.defaultFormatter": "charliermarsh.ruff"
|
||||
},
|
||||
"editor.formatOnPaste": false,
|
||||
"editor.formatOnSave": true,
|
||||
|
4
.github/actions/build-image/action.yaml
vendored
4
.github/actions/build-image/action.yaml
vendored
@ -46,7 +46,7 @@ runs:
|
||||
|
||||
- name: Build and push to ghcr by digest
|
||||
id: build-ghcr
|
||||
uses: docker/build-push-action@v6.13.0
|
||||
uses: docker/build-push-action@v6.14.0
|
||||
env:
|
||||
DOCKER_BUILD_SUMMARY: false
|
||||
DOCKER_BUILD_RECORD_UPLOAD: false
|
||||
@ -72,7 +72,7 @@ runs:
|
||||
|
||||
- name: Build and push to dockerhub by digest
|
||||
id: build-dockerhub
|
||||
uses: docker/build-push-action@v6.13.0
|
||||
uses: docker/build-push-action@v6.14.0
|
||||
env:
|
||||
DOCKER_BUILD_SUMMARY: false
|
||||
DOCKER_BUILD_RECORD_UPLOAD: false
|
||||
|
2
.github/actions/restore-python/action.yml
vendored
2
.github/actions/restore-python/action.yml
vendored
@ -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
|
||||
|
10
.github/workflows/ci-docker.yml
vendored
10
.github/workflows/ci-docker.yml
vendored
@ -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
|
||||
@ -46,9 +46,7 @@ jobs:
|
||||
with:
|
||||
python-version: "3.9"
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.8.0
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.3.0
|
||||
uses: docker/setup-buildx-action@v3.9.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
|
||||
|
18
.github/workflows/ci.yml
vendored
18
.github/workflows/ci.yml
vendored
@ -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
|
||||
|
4
.github/workflows/matchers/lint-python.json
vendored
4
.github/workflows/matchers/lint-python.json
vendored
@ -1,11 +1,11 @@
|
||||
{
|
||||
"problemMatcher": [
|
||||
{
|
||||
"owner": "black",
|
||||
"owner": "ruff",
|
||||
"severity": "error",
|
||||
"pattern": [
|
||||
{
|
||||
"regexp": "^(.*): (Please format this file with the black formatter)",
|
||||
"regexp": "^(.*): (Please format this file with the ruff formatter)",
|
||||
"file": 1,
|
||||
"message": 2
|
||||
}
|
||||
|
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@ -89,10 +89,10 @@ jobs:
|
||||
python-version: "3.9"
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.8.0
|
||||
uses: docker/setup-buildx-action@v3.9.0
|
||||
- name: Set up QEMU
|
||||
if: matrix.platform != 'linux/amd64'
|
||||
uses: docker/setup-qemu-action@v3.3.0
|
||||
uses: docker/setup-qemu-action@v3.4.0
|
||||
|
||||
- name: Log in to docker hub
|
||||
uses: docker/login-action@v3.3.0
|
||||
@ -183,7 +183,7 @@ jobs:
|
||||
merge-multiple: true
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.8.0
|
||||
uses: docker/setup-buildx-action@v3.9.0
|
||||
|
||||
- name: Log in to docker hub
|
||||
if: matrix.registry == 'dockerhub'
|
||||
|
@ -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]
|
||||
|
@ -234,6 +234,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
|
||||
@ -242,6 +243,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
|
||||
@ -277,6 +279,7 @@ esphome/components/mics_4514/* @jesserockz
|
||||
esphome/components/midea/* @dudanov
|
||||
esphome/components/midea_ir/* @dudanov
|
||||
esphome/components/mitsubishi/* @RubyBailey
|
||||
esphome/components/mixer/speaker/* @kahrendt
|
||||
esphome/components/mlx90393/* @functionpointer
|
||||
esphome/components/mlx90614/* @jesserockz
|
||||
esphome/components/mmc5603/* @benhoff
|
||||
@ -343,6 +346,7 @@ esphome/components/radon_eye_rd200/* @jeffeb3
|
||||
esphome/components/rc522/* @glmnet
|
||||
esphome/components/rc522_i2c/* @glmnet
|
||||
esphome/components/rc522_spi/* @glmnet
|
||||
esphome/components/resampler/speaker/* @kahrendt
|
||||
esphome/components/restart/* @esphome/core
|
||||
esphome/components/rf_bridge/* @jesserockz
|
||||
esphome/components/rgbct/* @jesserockz
|
||||
@ -355,7 +359,7 @@ esphome/components/rtttl/* @glmnet
|
||||
esphome/components/safe_mode/* @jsuanet @kbx81 @paulmonigatti
|
||||
esphome/components/scd4x/* @martgras @sjtrny
|
||||
esphome/components/script/* @esphome/core
|
||||
esphome/components/sdl/* @clydebarrow
|
||||
esphome/components/sdl/* @bdm310 @clydebarrow
|
||||
esphome/components/sdm_meter/* @jesserockz @polyfaces
|
||||
esphome/components/sdp3x/* @Azimath
|
||||
esphome/components/seeed_mr24hpc1/* @limengdu
|
||||
@ -387,6 +391,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
|
||||
@ -497,5 +502,6 @@ esphome/components/xiaomi_mhoc401/* @vevsvevs
|
||||
esphome/components/xiaomi_rtcgq02lm/* @jesserockz
|
||||
esphome/components/xl9535/* @mreditor97
|
||||
esphome/components/xpt2046/touchscreen/* @nielsnl68 @numo68
|
||||
esphome/components/xxtea/* @clydebarrow
|
||||
esphome/components/zhlt01/* @cfeenstra1024
|
||||
esphome/components/zio_ultrasonic/* @kahrendt
|
||||
|
@ -1,12 +1,14 @@
|
||||
# Contributing to ESPHome
|
||||
# Contributing to ESPHome [](https://discord.gg/KhAMKrd) [](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)
|
||||
|
||||
---
|
||||
|
||||
[](https://www.openhomefoundation.org/)
|
||||
|
@ -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).
|
||||
---
|
||||
|
||||
[](https://www.openhomefoundation.org/)
|
||||
|
@ -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 \
|
||||
|
@ -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}")
|
||||
|
@ -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")
|
||||
|
||||
|
@ -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};
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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"; }
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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":
|
||||
|
@ -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__)
|
||||
|
||||
|
||||
|
@ -280,8 +280,7 @@ FileDecoderState AudioDecoder::decode_mp3_() {
|
||||
if (err) {
|
||||
switch (err) {
|
||||
case esp_audio_libs::helix_decoder::ERR_MP3_OUT_OF_MEMORY:
|
||||
return FileDecoderState::FAILED;
|
||||
break;
|
||||
// Intentional fallthrough
|
||||
case esp_audio_libs::helix_decoder::ERR_MP3_NULL_POINTER:
|
||||
return FileDecoderState::FAILED;
|
||||
break;
|
||||
|
@ -5,12 +5,13 @@
|
||||
#include "audio.h"
|
||||
#include "audio_transfer_buffer.h"
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/ring_buffer.h"
|
||||
|
||||
#ifdef USE_SPEAKER
|
||||
#include "esphome/components/speaker/speaker.h"
|
||||
#endif
|
||||
|
||||
#include "esphome/core/ring_buffer.h"
|
||||
|
||||
#include "esp_err.h"
|
||||
|
||||
#include <resampler.h> // esp-audio-libs
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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; }
|
||||
|
@ -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])
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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__)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
|
@ -39,6 +39,10 @@ void IDFI2CBus::setup() {
|
||||
conf.scl_io_num = scl_pin_;
|
||||
conf.scl_pullup_en = scl_pullup_enabled_;
|
||||
conf.master.clk_speed = frequency_;
|
||||
#ifdef USE_ESP32_VARIANT_ESP32S2
|
||||
// workaround for https://github.com/esphome/issues/issues/6718
|
||||
conf.clk_flags = I2C_SCLK_SRC_FLAG_AWARE_DFS;
|
||||
#endif
|
||||
esp_err_t err = i2c_param_config(port_, &conf);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "i2c_param_config failed: %s", esp_err_to_name(err));
|
||||
|
51
esphome/components/ld2450/__init__.py
Normal file
51
esphome/components/ld2450/__init__.py
Normal 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]))
|
47
esphome/components/ld2450/binary_sensor.py
Normal file
47
esphome/components/ld2450/binary_sensor.py
Normal 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))
|
45
esphome/components/ld2450/button/__init__.py
Normal file
45
esphome/components/ld2450/button/__init__.py
Normal 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))
|
9
esphome/components/ld2450/button/reset_button.cpp
Normal file
9
esphome/components/ld2450/button/reset_button.cpp
Normal 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
|
18
esphome/components/ld2450/button/reset_button.h
Normal file
18
esphome/components/ld2450/button/reset_button.h
Normal 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
|
9
esphome/components/ld2450/button/restart_button.cpp
Normal file
9
esphome/components/ld2450/button/restart_button.cpp
Normal 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
|
18
esphome/components/ld2450/button/restart_button.h
Normal file
18
esphome/components/ld2450/button/restart_button.h
Normal 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
|
867
esphome/components/ld2450/ld2450.cpp
Normal file
867
esphome/components/ld2450/ld2450.cpp
Normal 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
|
231
esphome/components/ld2450/ld2450.h
Normal file
231
esphome/components/ld2450/ld2450.h
Normal 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
|
120
esphome/components/ld2450/number/__init__.py
Normal file
120
esphome/components/ld2450/number/__init__.py
Normal 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))
|
12
esphome/components/ld2450/number/presence_timeout_number.cpp
Normal file
12
esphome/components/ld2450/number/presence_timeout_number.cpp
Normal 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
|
18
esphome/components/ld2450/number/presence_timeout_number.h
Normal file
18
esphome/components/ld2450/number/presence_timeout_number.h
Normal 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
|
14
esphome/components/ld2450/number/zone_coordinate_number.cpp
Normal file
14
esphome/components/ld2450/number/zone_coordinate_number.cpp
Normal 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
|
19
esphome/components/ld2450/number/zone_coordinate_number.h
Normal file
19
esphome/components/ld2450/number/zone_coordinate_number.h
Normal 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
|
56
esphome/components/ld2450/select/__init__.py
Normal file
56
esphome/components/ld2450/select/__init__.py
Normal 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))
|
12
esphome/components/ld2450/select/baud_rate_select.cpp
Normal file
12
esphome/components/ld2450/select/baud_rate_select.cpp
Normal 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
|
18
esphome/components/ld2450/select/baud_rate_select.h
Normal file
18
esphome/components/ld2450/select/baud_rate_select.h
Normal 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
|
12
esphome/components/ld2450/select/zone_type_select.cpp
Normal file
12
esphome/components/ld2450/select/zone_type_select.cpp
Normal 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
|
18
esphome/components/ld2450/select/zone_type_select.h
Normal file
18
esphome/components/ld2450/select/zone_type_select.h
Normal 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
|
156
esphome/components/ld2450/sensor.py
Normal file
156
esphome/components/ld2450/sensor.py
Normal 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))
|
45
esphome/components/ld2450/switch/__init__.py
Normal file
45
esphome/components/ld2450/switch/__init__.py
Normal 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))
|
12
esphome/components/ld2450/switch/bluetooth_switch.cpp
Normal file
12
esphome/components/ld2450/switch/bluetooth_switch.cpp
Normal 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
|
18
esphome/components/ld2450/switch/bluetooth_switch.h
Normal file
18
esphome/components/ld2450/switch/bluetooth_switch.h
Normal 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
|
12
esphome/components/ld2450/switch/multi_target_switch.cpp
Normal file
12
esphome/components/ld2450/switch/multi_target_switch.cpp
Normal 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
|
18
esphome/components/ld2450/switch/multi_target_switch.h
Normal file
18
esphome/components/ld2450/switch/multi_target_switch.h
Normal 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
|
62
esphome/components/ld2450/text_sensor.py
Normal file
62
esphome/components/ld2450/text_sensor.py
Normal 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))
|
@ -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_)
|
||||
|
@ -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
|
||||
|
@ -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 *> {
|
||||
|
29
esphome/components/logger/select/__init__.py
Normal file
29
esphome/components/logger/select/__init__.py
Normal 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)
|
27
esphome/components/logger/select/logger_level_select.cpp
Normal file
27
esphome/components/logger/select/logger_level_select.cpp
Normal 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
|
15
esphome/components/logger/select/logger_level_select.h
Normal file
15
esphome/components/logger/select/logger_level_select.h
Normal 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
|
@ -61,7 +61,14 @@ from .types import (
|
||||
lv_style_t,
|
||||
lvgl_ns,
|
||||
)
|
||||
from .widgets import Widget, add_widgets, get_scr_act, set_obj_properties, styles_used
|
||||
from .widgets import (
|
||||
LvScrActType,
|
||||
Widget,
|
||||
add_widgets,
|
||||
get_scr_act,
|
||||
set_obj_properties,
|
||||
styles_used,
|
||||
)
|
||||
from .widgets.animimg import animimg_spec
|
||||
from .widgets.arc import arc_spec
|
||||
from .widgets.button import button_spec
|
||||
@ -318,7 +325,7 @@ async def to_code(configs):
|
||||
config[df.CONF_RESUME_ON_INPUT],
|
||||
)
|
||||
await cg.register_component(lv_component, config)
|
||||
Widget.create(config[CONF_ID], lv_component, obj_spec, config)
|
||||
Widget.create(config[CONF_ID], lv_component, LvScrActType(), config)
|
||||
|
||||
lv_scr_act = get_scr_act(lv_component)
|
||||
async with LvContext():
|
||||
@ -389,75 +396,87 @@ def add_hello_world(config):
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = final_validation
|
||||
|
||||
LVGL_SCHEMA = (
|
||||
cv.polling_component_schema("1s")
|
||||
.extend(obj_schema(obj_spec))
|
||||
.extend(
|
||||
{
|
||||
cv.GenerateID(CONF_ID): cv.declare_id(LvglComponent),
|
||||
cv.GenerateID(df.CONF_DISPLAYS): display_schema,
|
||||
cv.Optional(df.CONF_COLOR_DEPTH, default=16): cv.one_of(16),
|
||||
cv.Optional(df.CONF_DEFAULT_FONT, default="montserrat_14"): lvalid.lv_font,
|
||||
cv.Optional(df.CONF_FULL_REFRESH, default=False): cv.boolean,
|
||||
cv.Optional(df.CONF_DRAW_ROUNDING, default=2): cv.positive_int,
|
||||
cv.Optional(CONF_BUFFER_SIZE, default="100%"): cv.percentage,
|
||||
cv.Optional(df.CONF_LOG_LEVEL, default="WARN"): cv.one_of(
|
||||
*df.LV_LOG_LEVELS, upper=True
|
||||
),
|
||||
cv.Optional(df.CONF_BYTE_ORDER, default="big_endian"): cv.one_of(
|
||||
"big_endian", "little_endian"
|
||||
),
|
||||
cv.Optional(df.CONF_STYLE_DEFINITIONS): cv.ensure_list(
|
||||
cv.Schema({cv.Required(CONF_ID): cv.declare_id(lv_style_t)})
|
||||
.extend(STYLE_SCHEMA)
|
||||
.extend(
|
||||
LVGL_SCHEMA = cv.All(
|
||||
container_schema(
|
||||
obj_spec,
|
||||
cv.polling_component_schema("1s")
|
||||
.extend(
|
||||
{
|
||||
cv.GenerateID(CONF_ID): cv.declare_id(LvglComponent),
|
||||
cv.GenerateID(df.CONF_DISPLAYS): display_schema,
|
||||
cv.Optional(df.CONF_COLOR_DEPTH, default=16): cv.one_of(16),
|
||||
cv.Optional(
|
||||
df.CONF_DEFAULT_FONT, default="montserrat_14"
|
||||
): lvalid.lv_font,
|
||||
cv.Optional(df.CONF_FULL_REFRESH, default=False): cv.boolean,
|
||||
cv.Optional(df.CONF_DRAW_ROUNDING, default=2): cv.positive_int,
|
||||
cv.Optional(CONF_BUFFER_SIZE, default="100%"): cv.percentage,
|
||||
cv.Optional(df.CONF_LOG_LEVEL, default="WARN"): cv.one_of(
|
||||
*df.LV_LOG_LEVELS, upper=True
|
||||
),
|
||||
cv.Optional(df.CONF_BYTE_ORDER, default="big_endian"): cv.one_of(
|
||||
"big_endian", "little_endian"
|
||||
),
|
||||
cv.Optional(df.CONF_STYLE_DEFINITIONS): cv.ensure_list(
|
||||
cv.Schema({cv.Required(CONF_ID): cv.declare_id(lv_style_t)})
|
||||
.extend(STYLE_SCHEMA)
|
||||
.extend(
|
||||
{
|
||||
cv.Optional(df.CONF_GRID_CELL_X_ALIGN): grid_alignments,
|
||||
cv.Optional(df.CONF_GRID_CELL_Y_ALIGN): grid_alignments,
|
||||
cv.Optional(df.CONF_PAD_ROW): lvalid.pixels,
|
||||
cv.Optional(df.CONF_PAD_COLUMN): lvalid.pixels,
|
||||
}
|
||||
)
|
||||
),
|
||||
cv.Optional(CONF_ON_IDLE): validate_automation(
|
||||
{
|
||||
cv.Optional(df.CONF_GRID_CELL_X_ALIGN): grid_alignments,
|
||||
cv.Optional(df.CONF_GRID_CELL_Y_ALIGN): grid_alignments,
|
||||
cv.Optional(df.CONF_PAD_ROW): lvalid.pixels,
|
||||
cv.Optional(df.CONF_PAD_COLUMN): lvalid.pixels,
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(IdleTrigger),
|
||||
cv.Required(CONF_TIMEOUT): cv.templatable(
|
||||
cv.positive_time_period_milliseconds
|
||||
),
|
||||
}
|
||||
)
|
||||
),
|
||||
cv.Optional(CONF_ON_IDLE): validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(IdleTrigger),
|
||||
cv.Required(CONF_TIMEOUT): cv.templatable(
|
||||
cv.positive_time_period_milliseconds
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(df.CONF_ON_PAUSE): validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PauseTrigger),
|
||||
}
|
||||
),
|
||||
cv.Optional(df.CONF_ON_RESUME): validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PauseTrigger),
|
||||
}
|
||||
),
|
||||
cv.Exclusive(df.CONF_WIDGETS, CONF_PAGES): cv.ensure_list(WIDGET_SCHEMA),
|
||||
cv.Exclusive(CONF_PAGES, CONF_PAGES): cv.ensure_list(
|
||||
container_schema(page_spec)
|
||||
),
|
||||
cv.Optional(df.CONF_MSGBOXES): cv.ensure_list(MSGBOX_SCHEMA),
|
||||
cv.Optional(df.CONF_PAGE_WRAP, default=True): lv_bool,
|
||||
cv.Optional(df.CONF_TOP_LAYER): container_schema(obj_spec),
|
||||
cv.Optional(df.CONF_TRANSPARENCY_KEY, default=0x000400): lvalid.lv_color,
|
||||
cv.Optional(df.CONF_THEME): cv.Schema(
|
||||
{cv.Optional(name): obj_schema(w) for name, w in WIDGET_TYPES.items()}
|
||||
),
|
||||
cv.Optional(df.CONF_GRADIENTS): GRADIENT_SCHEMA,
|
||||
cv.Optional(df.CONF_TOUCHSCREENS, default=None): touchscreen_schema,
|
||||
cv.Optional(df.CONF_ENCODERS, default=None): ENCODERS_CONFIG,
|
||||
cv.Optional(df.CONF_KEYPADS, default=None): KEYPADS_CONFIG,
|
||||
cv.GenerateID(df.CONF_DEFAULT_GROUP): cv.declare_id(lv_group_t),
|
||||
cv.Optional(df.CONF_RESUME_ON_INPUT, default=True): cv.boolean,
|
||||
}
|
||||
)
|
||||
.extend(DISP_BG_SCHEMA)
|
||||
.add_extra(add_hello_world)
|
||||
),
|
||||
cv.Optional(df.CONF_ON_PAUSE): validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PauseTrigger),
|
||||
}
|
||||
),
|
||||
cv.Optional(df.CONF_ON_RESUME): validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PauseTrigger),
|
||||
}
|
||||
),
|
||||
cv.Exclusive(df.CONF_WIDGETS, CONF_PAGES): cv.ensure_list(
|
||||
WIDGET_SCHEMA
|
||||
),
|
||||
cv.Exclusive(CONF_PAGES, CONF_PAGES): cv.ensure_list(
|
||||
container_schema(page_spec)
|
||||
),
|
||||
cv.Optional(df.CONF_MSGBOXES): cv.ensure_list(MSGBOX_SCHEMA),
|
||||
cv.Optional(df.CONF_PAGE_WRAP, default=True): lv_bool,
|
||||
cv.Optional(df.CONF_TOP_LAYER): container_schema(obj_spec),
|
||||
cv.Optional(
|
||||
df.CONF_TRANSPARENCY_KEY, default=0x000400
|
||||
): lvalid.lv_color,
|
||||
cv.Optional(df.CONF_THEME): cv.Schema(
|
||||
{
|
||||
cv.Optional(name): obj_schema(w)
|
||||
for name, w in WIDGET_TYPES.items()
|
||||
}
|
||||
),
|
||||
cv.Optional(df.CONF_GRADIENTS): GRADIENT_SCHEMA,
|
||||
cv.Optional(df.CONF_TOUCHSCREENS, default=None): touchscreen_schema,
|
||||
cv.Optional(df.CONF_ENCODERS, default=None): ENCODERS_CONFIG,
|
||||
cv.Optional(df.CONF_KEYPADS, default=None): KEYPADS_CONFIG,
|
||||
cv.GenerateID(df.CONF_DEFAULT_GROUP): cv.declare_id(lv_group_t),
|
||||
cv.Optional(df.CONF_RESUME_ON_INPUT, default=True): cv.boolean,
|
||||
}
|
||||
)
|
||||
.extend(DISP_BG_SCHEMA),
|
||||
),
|
||||
cv.has_at_most_one_key(CONF_PAGES, df.CONF_LAYOUT),
|
||||
add_hello_world,
|
||||
)
|
||||
|
||||
|
||||
|
@ -146,6 +146,8 @@ TYPE_FLEX = "flex"
|
||||
TYPE_GRID = "grid"
|
||||
TYPE_NONE = "none"
|
||||
|
||||
DIRECTIONS = LvConstant("LV_DIR_", "LEFT", "RIGHT", "BOTTOM", "TOP")
|
||||
|
||||
LV_FONTS = list(f"montserrat_{s}" for s in range(8, 50, 2)) + [
|
||||
"dejavu_16_persian_hebrew",
|
||||
"simsun_16_cjk",
|
||||
@ -169,9 +171,13 @@ LV_EVENT_MAP = {
|
||||
"CANCEL": "CANCEL",
|
||||
"ALL_EVENTS": "ALL",
|
||||
"CHANGE": "VALUE_CHANGED",
|
||||
"GESTURE": "GESTURE",
|
||||
}
|
||||
|
||||
LV_EVENT_TRIGGERS = tuple(f"on_{x.lower()}" for x in LV_EVENT_MAP)
|
||||
SWIPE_TRIGGERS = tuple(
|
||||
f"on_swipe_{x.lower()}" for x in DIRECTIONS.choices + ("up", "down")
|
||||
)
|
||||
|
||||
|
||||
LV_ANIM = LvConstant(
|
||||
@ -250,7 +256,6 @@ KEYBOARD_MODES = LvConstant(
|
||||
"NUMBER",
|
||||
)
|
||||
ROLLER_MODES = LvConstant("LV_ROLLER_MODE_", "NORMAL", "INFINITE")
|
||||
DIRECTIONS = LvConstant("LV_DIR_", "LEFT", "RIGHT", "BOTTOM", "TOP")
|
||||
TILE_DIRECTIONS = DIRECTIONS.extend("HOR", "VER", "ALL")
|
||||
CHILD_ALIGNMENTS = LvConstant(
|
||||
"LV_ALIGN_",
|
||||
|
@ -211,10 +211,9 @@ def part_schema(parts):
|
||||
|
||||
|
||||
def automation_schema(typ: LvType):
|
||||
events = df.LV_EVENT_TRIGGERS + df.SWIPE_TRIGGERS
|
||||
if typ.has_on_value:
|
||||
events = df.LV_EVENT_TRIGGERS + (CONF_ON_VALUE,)
|
||||
else:
|
||||
events = df.LV_EVENT_TRIGGERS
|
||||
events = events + (CONF_ON_VALUE,)
|
||||
args = typ.get_arg_type() if isinstance(typ, LvType) else []
|
||||
args.append(lv_event_t_ptr)
|
||||
return {
|
||||
|
@ -7,8 +7,10 @@ from .defines import (
|
||||
CONF_ALIGN_TO,
|
||||
CONF_X,
|
||||
CONF_Y,
|
||||
DIRECTIONS,
|
||||
LV_EVENT_MAP,
|
||||
LV_EVENT_TRIGGERS,
|
||||
SWIPE_TRIGGERS,
|
||||
literal,
|
||||
)
|
||||
from .lvcode import (
|
||||
@ -23,7 +25,7 @@ from .lvcode import (
|
||||
lvgl_static,
|
||||
)
|
||||
from .types import LV_EVENT
|
||||
from .widgets import widget_map
|
||||
from .widgets import LvScrActType, get_scr_act, widget_map
|
||||
|
||||
|
||||
async def generate_triggers():
|
||||
@ -33,6 +35,9 @@ async def generate_triggers():
|
||||
"""
|
||||
|
||||
for w in widget_map.values():
|
||||
if isinstance(w.type, LvScrActType):
|
||||
w = get_scr_act(w.var)
|
||||
|
||||
if w.config:
|
||||
for event, conf in {
|
||||
event: conf
|
||||
@ -43,6 +48,24 @@ async def generate_triggers():
|
||||
w.add_flag("LV_OBJ_FLAG_CLICKABLE")
|
||||
event = literal("LV_EVENT_" + LV_EVENT_MAP[event[3:].upper()])
|
||||
await add_trigger(conf, w, event)
|
||||
|
||||
for event, conf in {
|
||||
event: conf
|
||||
for event, conf in w.config.items()
|
||||
if event in SWIPE_TRIGGERS
|
||||
}.items():
|
||||
conf = conf[0]
|
||||
dir = event[9:].upper()
|
||||
dir = {"UP": "TOP", "DOWN": "BOTTOM"}.get(dir, dir)
|
||||
dir = DIRECTIONS.mapper(dir)
|
||||
w.clear_flag("LV_OBJ_FLAG_SCROLLABLE")
|
||||
selected = literal(
|
||||
f"lv_indev_get_gesture_dir(lv_indev_get_act()) == {dir}"
|
||||
)
|
||||
await add_trigger(
|
||||
conf, w, literal("LV_EVENT_GESTURE"), is_selected=selected
|
||||
)
|
||||
|
||||
for conf in w.config.get(CONF_ON_VALUE, ()):
|
||||
await add_trigger(
|
||||
conf,
|
||||
@ -61,13 +84,14 @@ async def generate_triggers():
|
||||
lv.obj_align_to(w.obj, target, align, x, y)
|
||||
|
||||
|
||||
async def add_trigger(conf, w, *events):
|
||||
async def add_trigger(conf, w, *events, is_selected=None):
|
||||
is_selected = is_selected or w.is_selected()
|
||||
tid = conf[CONF_TRIGGER_ID]
|
||||
trigger = cg.new_Pvariable(tid)
|
||||
args = w.get_args() + [(lv_event_t_ptr, "event")]
|
||||
value = w.get_values()
|
||||
await automation.build_automation(trigger, args, conf)
|
||||
async with LambdaContext(EVENT_ARG, where=tid) as context:
|
||||
with LvConditional(w.is_selected()):
|
||||
with LvConditional(is_selected):
|
||||
lv_add(trigger.trigger(*value, literal("event")))
|
||||
lv_add(lvgl_static.add_event_cb(w.obj, await context.get_lambda(), *events))
|
||||
|
@ -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
|
||||
|
@ -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> {
|
||||
|
@ -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";
|
||||
}
|
||||
|
@ -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_();
|
||||
|
0
esphome/components/mixer/__init__.py
Normal file
0
esphome/components/mixer/__init__.py
Normal file
172
esphome/components/mixer/speaker/__init__.py
Normal file
172
esphome/components/mixer/speaker/__init__.py
Normal file
@ -0,0 +1,172 @@
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import audio, esp32, speaker
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_BITS_PER_SAMPLE,
|
||||
CONF_BUFFER_DURATION,
|
||||
CONF_DURATION,
|
||||
CONF_ID,
|
||||
CONF_NEVER,
|
||||
CONF_NUM_CHANNELS,
|
||||
CONF_OUTPUT_SPEAKER,
|
||||
CONF_SAMPLE_RATE,
|
||||
CONF_TASK_STACK_IN_PSRAM,
|
||||
CONF_TIMEOUT,
|
||||
PLATFORM_ESP32,
|
||||
)
|
||||
from esphome.core.entity_helpers import inherit_property_from
|
||||
import esphome.final_validate as fv
|
||||
|
||||
AUTO_LOAD = ["audio"]
|
||||
CODEOWNERS = ["@kahrendt"]
|
||||
|
||||
mixer_speaker_ns = cg.esphome_ns.namespace("mixer_speaker")
|
||||
MixerSpeaker = mixer_speaker_ns.class_("MixerSpeaker", cg.Component)
|
||||
SourceSpeaker = mixer_speaker_ns.class_("SourceSpeaker", cg.Component, speaker.Speaker)
|
||||
|
||||
CONF_DECIBEL_REDUCTION = "decibel_reduction"
|
||||
CONF_QUEUE_MODE = "queue_mode"
|
||||
CONF_SOURCE_SPEAKERS = "source_speakers"
|
||||
|
||||
DuckingApplyAction = mixer_speaker_ns.class_(
|
||||
"DuckingApplyAction", automation.Action, cg.Parented.template(SourceSpeaker)
|
||||
)
|
||||
|
||||
|
||||
SOURCE_SPEAKER_SCHEMA = speaker.SPEAKER_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(SourceSpeaker),
|
||||
cv.Optional(
|
||||
CONF_BUFFER_DURATION, default="100ms"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(CONF_TIMEOUT, default="500ms"): cv.Any(
|
||||
cv.positive_time_period_milliseconds,
|
||||
cv.one_of(CONF_NEVER, lower=True),
|
||||
),
|
||||
cv.Optional(CONF_BITS_PER_SAMPLE, default=16): cv.int_range(16, 16),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _set_stream_limits(config):
|
||||
audio.set_stream_limits(
|
||||
min_bits_per_sample=16,
|
||||
max_bits_per_sample=16,
|
||||
)(config)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def _validate_source_speaker(config):
|
||||
fconf = fv.full_config.get()
|
||||
|
||||
# Get ID for the output speaker and add it to the source speakrs config to easily inherit properties
|
||||
path = fconf.get_path_for_id(config[CONF_ID])[:-3]
|
||||
path.append(CONF_OUTPUT_SPEAKER)
|
||||
output_speaker_id = fconf.get_config_for_path(path)
|
||||
config[CONF_OUTPUT_SPEAKER] = output_speaker_id
|
||||
|
||||
inherit_property_from(CONF_NUM_CHANNELS, CONF_OUTPUT_SPEAKER)(config)
|
||||
inherit_property_from(CONF_SAMPLE_RATE, CONF_OUTPUT_SPEAKER)(config)
|
||||
|
||||
audio.final_validate_audio_schema(
|
||||
"mixer",
|
||||
audio_device=CONF_OUTPUT_SPEAKER,
|
||||
bits_per_sample=config.get(CONF_BITS_PER_SAMPLE),
|
||||
channels=config.get(CONF_NUM_CHANNELS),
|
||||
sample_rate=config.get(CONF_SAMPLE_RATE),
|
||||
)(config)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(MixerSpeaker),
|
||||
cv.Required(CONF_OUTPUT_SPEAKER): cv.use_id(speaker.Speaker),
|
||||
cv.Required(CONF_SOURCE_SPEAKERS): cv.All(
|
||||
cv.ensure_list(SOURCE_SPEAKER_SCHEMA),
|
||||
cv.Length(min=2, max=8),
|
||||
[_set_stream_limits],
|
||||
),
|
||||
cv.Optional(CONF_NUM_CHANNELS): cv.int_range(min=1, max=2),
|
||||
cv.Optional(CONF_QUEUE_MODE, default=False): cv.boolean,
|
||||
cv.SplitDefault(CONF_TASK_STACK_IN_PSRAM, esp32_idf=False): cv.All(
|
||||
cv.boolean, cv.only_with_esp_idf
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.only_on([PLATFORM_ESP32]),
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_SOURCE_SPEAKERS): [_validate_source_speaker],
|
||||
},
|
||||
extra=cv.ALLOW_EXTRA,
|
||||
),
|
||||
inherit_property_from(CONF_NUM_CHANNELS, CONF_OUTPUT_SPEAKER),
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
|
||||
spkr = await cg.get_variable(config[CONF_OUTPUT_SPEAKER])
|
||||
|
||||
cg.add(var.set_output_channels(config[CONF_NUM_CHANNELS]))
|
||||
cg.add(var.set_output_speaker(spkr))
|
||||
cg.add(var.set_queue_mode(config[CONF_QUEUE_MODE]))
|
||||
|
||||
if task_stack_in_psram := config.get(CONF_TASK_STACK_IN_PSRAM):
|
||||
cg.add(var.set_task_stack_in_psram(task_stack_in_psram))
|
||||
if task_stack_in_psram:
|
||||
if config[CONF_TASK_STACK_IN_PSRAM]:
|
||||
esp32.add_idf_sdkconfig_option(
|
||||
"CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY", True
|
||||
)
|
||||
|
||||
for speaker_config in config[CONF_SOURCE_SPEAKERS]:
|
||||
source_speaker = cg.new_Pvariable(speaker_config[CONF_ID])
|
||||
|
||||
cg.add(source_speaker.set_buffer_duration(speaker_config[CONF_BUFFER_DURATION]))
|
||||
|
||||
if speaker_config[CONF_TIMEOUT] != CONF_NEVER:
|
||||
cg.add(source_speaker.set_timeout(speaker_config[CONF_TIMEOUT]))
|
||||
|
||||
await cg.register_component(source_speaker, speaker_config)
|
||||
await cg.register_parented(source_speaker, config[CONF_ID])
|
||||
await speaker.register_speaker(source_speaker, speaker_config)
|
||||
|
||||
cg.add(var.add_source_speaker(source_speaker))
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"mixer_speaker.apply_ducking",
|
||||
DuckingApplyAction,
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(SourceSpeaker),
|
||||
cv.Required(CONF_DECIBEL_REDUCTION): cv.templatable(
|
||||
cv.int_range(min=0, max=51)
|
||||
),
|
||||
cv.Optional(CONF_DURATION, default="0.0s"): cv.templatable(
|
||||
cv.positive_time_period_milliseconds
|
||||
),
|
||||
}
|
||||
),
|
||||
)
|
||||
async def ducking_set_to_code(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
decibel_reduction = await cg.templatable(
|
||||
config[CONF_DECIBEL_REDUCTION], args, cg.uint8
|
||||
)
|
||||
cg.add(var.set_decibel_reduction(decibel_reduction))
|
||||
duration = await cg.templatable(config[CONF_DURATION], args, cg.uint32)
|
||||
cg.add(var.set_duration(duration))
|
||||
return var
|
19
esphome/components/mixer/speaker/automation.h
Normal file
19
esphome/components/mixer/speaker/automation.h
Normal file
@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include "mixer_speaker.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
namespace esphome {
|
||||
namespace mixer_speaker {
|
||||
template<typename... Ts> class DuckingApplyAction : public Action<Ts...>, public Parented<SourceSpeaker> {
|
||||
TEMPLATABLE_VALUE(uint8_t, decibel_reduction)
|
||||
TEMPLATABLE_VALUE(uint32_t, duration)
|
||||
void play(Ts... x) override {
|
||||
this->parent_->apply_ducking(this->decibel_reduction_.value(x...), this->duration_.value(x...));
|
||||
}
|
||||
};
|
||||
} // namespace mixer_speaker
|
||||
} // namespace esphome
|
||||
|
||||
#endif
|
624
esphome/components/mixer/speaker/mixer_speaker.cpp
Normal file
624
esphome/components/mixer/speaker/mixer_speaker.cpp
Normal file
@ -0,0 +1,624 @@
|
||||
#include "mixer_speaker.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
namespace esphome {
|
||||
namespace mixer_speaker {
|
||||
|
||||
static const UBaseType_t MIXER_TASK_PRIORITY = 10;
|
||||
|
||||
static const uint32_t TRANSFER_BUFFER_DURATION_MS = 50;
|
||||
static const uint32_t TASK_DELAY_MS = 25;
|
||||
|
||||
static const size_t TASK_STACK_SIZE = 4096;
|
||||
|
||||
static const int16_t MAX_AUDIO_SAMPLE_VALUE = INT16_MAX;
|
||||
static const int16_t MIN_AUDIO_SAMPLE_VALUE = INT16_MIN;
|
||||
|
||||
static const char *const TAG = "speaker_mixer";
|
||||
|
||||
// Gives the Q15 fixed point scaling factor to reduce by 0 dB, 1dB, ..., 50 dB
|
||||
// dB to PCM scaling factor formula: floating_point_scale_factor = 2^(-db/6.014)
|
||||
// float to Q15 fixed point formula: q15_scale_factor = floating_point_scale_factor * 2^(15)
|
||||
static const std::vector<int16_t> DECIBEL_REDUCTION_TABLE = {
|
||||
32767, 29201, 26022, 23189, 20665, 18415, 16410, 14624, 13032, 11613, 10349, 9222, 8218, 7324, 6527, 5816, 5183,
|
||||
4619, 4116, 3668, 3269, 2913, 2596, 2313, 2061, 1837, 1637, 1459, 1300, 1158, 1032, 920, 820, 731,
|
||||
651, 580, 517, 461, 411, 366, 326, 291, 259, 231, 206, 183, 163, 146, 130, 116, 103};
|
||||
|
||||
enum MixerEventGroupBits : uint32_t {
|
||||
COMMAND_STOP = (1 << 0), // stops the mixer task
|
||||
STATE_STARTING = (1 << 10),
|
||||
STATE_RUNNING = (1 << 11),
|
||||
STATE_STOPPING = (1 << 12),
|
||||
STATE_STOPPED = (1 << 13),
|
||||
ERR_ESP_NO_MEM = (1 << 19),
|
||||
ALL_BITS = 0x00FFFFFF, // All valid FreeRTOS event group bits
|
||||
};
|
||||
|
||||
void SourceSpeaker::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "Mixer Source Speaker");
|
||||
ESP_LOGCONFIG(TAG, " Buffer Duration: %" PRIu32 " ms", this->buffer_duration_ms_);
|
||||
if (this->timeout_ms_.has_value()) {
|
||||
ESP_LOGCONFIG(TAG, " Timeout: %" PRIu32 " ms", this->timeout_ms_.value());
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, " Timeout: never");
|
||||
}
|
||||
}
|
||||
|
||||
void SourceSpeaker::setup() {
|
||||
this->parent_->get_output_speaker()->add_audio_output_callback(
|
||||
[this](uint32_t new_playback_ms, uint32_t remainder_us, uint32_t pending_ms, uint32_t write_timestamp) {
|
||||
uint32_t personal_playback_ms = std::min(new_playback_ms, this->pending_playback_ms_);
|
||||
if (personal_playback_ms > 0) {
|
||||
this->pending_playback_ms_ -= personal_playback_ms;
|
||||
this->audio_output_callback_(personal_playback_ms, remainder_us, this->pending_playback_ms_, write_timestamp);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void SourceSpeaker::loop() {
|
||||
switch (this->state_) {
|
||||
case speaker::STATE_STARTING: {
|
||||
esp_err_t err = this->start_();
|
||||
if (err == ESP_OK) {
|
||||
this->state_ = speaker::STATE_RUNNING;
|
||||
this->stop_gracefully_ = false;
|
||||
this->last_seen_data_ms_ = millis();
|
||||
this->status_clear_error();
|
||||
} else {
|
||||
switch (err) {
|
||||
case ESP_ERR_NO_MEM:
|
||||
this->status_set_error("Failed to start mixer: not enough memory");
|
||||
break;
|
||||
case ESP_ERR_NOT_SUPPORTED:
|
||||
this->status_set_error("Failed to start mixer: unsupported bits per sample");
|
||||
break;
|
||||
case ESP_ERR_INVALID_ARG:
|
||||
this->status_set_error("Failed to start mixer: audio stream isn't compatible with the other audio stream.");
|
||||
break;
|
||||
case ESP_ERR_INVALID_STATE:
|
||||
this->status_set_error("Failed to start mixer: mixer task failed to start");
|
||||
break;
|
||||
default:
|
||||
this->status_set_error("Failed to start mixer");
|
||||
break;
|
||||
}
|
||||
|
||||
this->state_ = speaker::STATE_STOPPING;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case speaker::STATE_RUNNING:
|
||||
if (!this->transfer_buffer_->has_buffered_data()) {
|
||||
if ((this->timeout_ms_.has_value() && ((millis() - this->last_seen_data_ms_) > this->timeout_ms_.value())) ||
|
||||
this->stop_gracefully_) {
|
||||
this->state_ = speaker::STATE_STOPPING;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case speaker::STATE_STOPPING:
|
||||
this->stop_();
|
||||
this->stop_gracefully_ = false;
|
||||
this->state_ = speaker::STATE_STOPPED;
|
||||
break;
|
||||
case speaker::STATE_STOPPED:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
size_t SourceSpeaker::play(const uint8_t *data, size_t length, TickType_t ticks_to_wait) {
|
||||
if (this->is_stopped()) {
|
||||
this->start();
|
||||
}
|
||||
size_t bytes_written = 0;
|
||||
if (this->ring_buffer_.use_count() == 1) {
|
||||
std::shared_ptr<RingBuffer> temp_ring_buffer = this->ring_buffer_.lock();
|
||||
bytes_written = temp_ring_buffer->write_without_replacement(data, length, ticks_to_wait);
|
||||
if (bytes_written > 0) {
|
||||
this->last_seen_data_ms_ = millis();
|
||||
}
|
||||
}
|
||||
return bytes_written;
|
||||
}
|
||||
|
||||
void SourceSpeaker::start() { this->state_ = speaker::STATE_STARTING; }
|
||||
|
||||
esp_err_t SourceSpeaker::start_() {
|
||||
const size_t ring_buffer_size = this->audio_stream_info_.ms_to_bytes(this->buffer_duration_ms_);
|
||||
if (this->transfer_buffer_.use_count() == 0) {
|
||||
this->transfer_buffer_ =
|
||||
audio::AudioSourceTransferBuffer::create(this->audio_stream_info_.ms_to_bytes(TRANSFER_BUFFER_DURATION_MS));
|
||||
|
||||
if (this->transfer_buffer_ == nullptr) {
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
std::shared_ptr<RingBuffer> temp_ring_buffer;
|
||||
|
||||
if (!this->ring_buffer_.use_count()) {
|
||||
temp_ring_buffer = RingBuffer::create(ring_buffer_size);
|
||||
this->ring_buffer_ = temp_ring_buffer;
|
||||
}
|
||||
|
||||
if (!this->ring_buffer_.use_count()) {
|
||||
return ESP_ERR_NO_MEM;
|
||||
} else {
|
||||
this->transfer_buffer_->set_source(temp_ring_buffer);
|
||||
}
|
||||
}
|
||||
|
||||
return this->parent_->start(this->audio_stream_info_);
|
||||
}
|
||||
|
||||
void SourceSpeaker::stop() {
|
||||
if (this->state_ != speaker::STATE_STOPPED) {
|
||||
this->state_ = speaker::STATE_STOPPING;
|
||||
}
|
||||
}
|
||||
|
||||
void SourceSpeaker::stop_() {
|
||||
this->transfer_buffer_.reset(); // deallocates the transfer buffer
|
||||
}
|
||||
|
||||
void SourceSpeaker::finish() { this->stop_gracefully_ = true; }
|
||||
|
||||
bool SourceSpeaker::has_buffered_data() const {
|
||||
return ((this->transfer_buffer_.use_count() > 0) && this->transfer_buffer_->has_buffered_data());
|
||||
}
|
||||
|
||||
void SourceSpeaker::set_mute_state(bool mute_state) {
|
||||
this->mute_state_ = mute_state;
|
||||
this->parent_->get_output_speaker()->set_mute_state(mute_state);
|
||||
}
|
||||
|
||||
void SourceSpeaker::set_volume(float volume) {
|
||||
this->volume_ = volume;
|
||||
this->parent_->get_output_speaker()->set_volume(volume);
|
||||
}
|
||||
|
||||
size_t SourceSpeaker::process_data_from_source(TickType_t ticks_to_wait) {
|
||||
if (!this->transfer_buffer_.use_count()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Store current offset, as these samples are already ducked
|
||||
const size_t current_length = this->transfer_buffer_->available();
|
||||
|
||||
size_t bytes_read = this->transfer_buffer_->transfer_data_from_source(ticks_to_wait);
|
||||
|
||||
uint32_t samples_to_duck = this->audio_stream_info_.bytes_to_samples(bytes_read);
|
||||
if (samples_to_duck > 0) {
|
||||
int16_t *current_buffer = reinterpret_cast<int16_t *>(this->transfer_buffer_->get_buffer_start() + current_length);
|
||||
|
||||
duck_samples(current_buffer, samples_to_duck, &this->current_ducking_db_reduction_,
|
||||
&this->ducking_transition_samples_remaining_, this->samples_per_ducking_step_,
|
||||
this->db_change_per_ducking_step_);
|
||||
}
|
||||
|
||||
return bytes_read;
|
||||
}
|
||||
|
||||
void SourceSpeaker::apply_ducking(uint8_t decibel_reduction, uint32_t duration) {
|
||||
if (this->target_ducking_db_reduction_ != decibel_reduction) {
|
||||
this->current_ducking_db_reduction_ = this->target_ducking_db_reduction_;
|
||||
|
||||
this->target_ducking_db_reduction_ = decibel_reduction;
|
||||
|
||||
uint8_t total_ducking_steps = 0;
|
||||
if (this->target_ducking_db_reduction_ > this->current_ducking_db_reduction_) {
|
||||
// The dB reduction level is increasing (which results in quieter audio)
|
||||
total_ducking_steps = this->target_ducking_db_reduction_ - this->current_ducking_db_reduction_ - 1;
|
||||
this->db_change_per_ducking_step_ = 1;
|
||||
} else {
|
||||
// The dB reduction level is decreasing (which results in louder audio)
|
||||
total_ducking_steps = this->current_ducking_db_reduction_ - this->target_ducking_db_reduction_ - 1;
|
||||
this->db_change_per_ducking_step_ = -1;
|
||||
}
|
||||
if ((duration > 0) && (total_ducking_steps > 0)) {
|
||||
this->ducking_transition_samples_remaining_ = this->audio_stream_info_.ms_to_samples(duration);
|
||||
|
||||
this->samples_per_ducking_step_ = this->ducking_transition_samples_remaining_ / total_ducking_steps;
|
||||
this->ducking_transition_samples_remaining_ =
|
||||
this->samples_per_ducking_step_ * total_ducking_steps; // Adjust for integer division rounding
|
||||
|
||||
this->current_ducking_db_reduction_ += this->db_change_per_ducking_step_;
|
||||
} else {
|
||||
this->ducking_transition_samples_remaining_ = 0;
|
||||
this->current_ducking_db_reduction_ = this->target_ducking_db_reduction_;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SourceSpeaker::duck_samples(int16_t *input_buffer, uint32_t input_samples_to_duck,
|
||||
int8_t *current_ducking_db_reduction, uint32_t *ducking_transition_samples_remaining,
|
||||
uint32_t samples_per_ducking_step, int8_t db_change_per_ducking_step) {
|
||||
if (*ducking_transition_samples_remaining > 0) {
|
||||
// Ducking level is still transitioning
|
||||
|
||||
// Takes the ceiling of input_samples_to_duck/samples_per_ducking_step
|
||||
uint32_t ducking_steps_in_batch =
|
||||
input_samples_to_duck / samples_per_ducking_step + (input_samples_to_duck % samples_per_ducking_step != 0);
|
||||
|
||||
for (uint32_t i = 0; i < ducking_steps_in_batch; ++i) {
|
||||
uint32_t samples_left_in_step = *ducking_transition_samples_remaining % samples_per_ducking_step;
|
||||
|
||||
if (samples_left_in_step == 0) {
|
||||
samples_left_in_step = samples_per_ducking_step;
|
||||
}
|
||||
|
||||
uint32_t samples_to_duck = std::min(input_samples_to_duck, samples_left_in_step);
|
||||
samples_to_duck = std::min(samples_to_duck, *ducking_transition_samples_remaining);
|
||||
|
||||
// Ensure we only point to valid index in the Q15 scaling factor table
|
||||
uint8_t safe_db_reduction_index =
|
||||
clamp<uint8_t>(*current_ducking_db_reduction, 0, DECIBEL_REDUCTION_TABLE.size() - 1);
|
||||
int16_t q15_scale_factor = DECIBEL_REDUCTION_TABLE[safe_db_reduction_index];
|
||||
|
||||
audio::scale_audio_samples(input_buffer, input_buffer, q15_scale_factor, samples_to_duck);
|
||||
|
||||
if (samples_left_in_step - samples_to_duck == 0) {
|
||||
// After scaling the current samples, we are ready to transition to the next step
|
||||
*current_ducking_db_reduction += db_change_per_ducking_step;
|
||||
}
|
||||
|
||||
input_buffer += samples_to_duck;
|
||||
*ducking_transition_samples_remaining -= samples_to_duck;
|
||||
input_samples_to_duck -= samples_to_duck;
|
||||
}
|
||||
}
|
||||
|
||||
if ((*current_ducking_db_reduction > 0) && (input_samples_to_duck > 0)) {
|
||||
// Audio is ducked, but its not in the middle of a transition step
|
||||
|
||||
uint8_t safe_db_reduction_index =
|
||||
clamp<uint8_t>(*current_ducking_db_reduction, 0, DECIBEL_REDUCTION_TABLE.size() - 1);
|
||||
int16_t q15_scale_factor = DECIBEL_REDUCTION_TABLE[safe_db_reduction_index];
|
||||
|
||||
audio::scale_audio_samples(input_buffer, input_buffer, q15_scale_factor, input_samples_to_duck);
|
||||
}
|
||||
}
|
||||
|
||||
void MixerSpeaker::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "Speaker Mixer:");
|
||||
ESP_LOGCONFIG(TAG, " Number of output channels: %u", this->output_channels_);
|
||||
}
|
||||
|
||||
void MixerSpeaker::setup() {
|
||||
this->event_group_ = xEventGroupCreate();
|
||||
|
||||
if (this->event_group_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to create event group");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void MixerSpeaker::loop() {
|
||||
uint32_t event_group_bits = xEventGroupGetBits(this->event_group_);
|
||||
|
||||
if (event_group_bits & MixerEventGroupBits::STATE_STARTING) {
|
||||
ESP_LOGD(TAG, "Starting speaker mixer");
|
||||
xEventGroupClearBits(this->event_group_, MixerEventGroupBits::STATE_STARTING);
|
||||
}
|
||||
if (event_group_bits & MixerEventGroupBits::ERR_ESP_NO_MEM) {
|
||||
this->status_set_error("Failed to allocate the mixer's internal buffer");
|
||||
xEventGroupClearBits(this->event_group_, MixerEventGroupBits::ERR_ESP_NO_MEM);
|
||||
}
|
||||
if (event_group_bits & MixerEventGroupBits::STATE_RUNNING) {
|
||||
ESP_LOGD(TAG, "Started speaker mixer");
|
||||
this->status_clear_error();
|
||||
xEventGroupClearBits(this->event_group_, MixerEventGroupBits::STATE_RUNNING);
|
||||
}
|
||||
if (event_group_bits & MixerEventGroupBits::STATE_STOPPING) {
|
||||
ESP_LOGD(TAG, "Stopping speaker mixer");
|
||||
xEventGroupClearBits(this->event_group_, MixerEventGroupBits::STATE_STOPPING);
|
||||
}
|
||||
if (event_group_bits & MixerEventGroupBits::STATE_STOPPED) {
|
||||
if (this->delete_task_() == ESP_OK) {
|
||||
xEventGroupClearBits(this->event_group_, MixerEventGroupBits::ALL_BITS);
|
||||
}
|
||||
}
|
||||
|
||||
if (this->task_handle_ != nullptr) {
|
||||
bool all_stopped = true;
|
||||
|
||||
for (auto &speaker : this->source_speakers_) {
|
||||
all_stopped &= speaker->is_stopped();
|
||||
}
|
||||
|
||||
if (all_stopped) {
|
||||
this->stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t MixerSpeaker::start(audio::AudioStreamInfo &stream_info) {
|
||||
if (!this->audio_stream_info_.has_value()) {
|
||||
if (stream_info.get_bits_per_sample() != 16) {
|
||||
// Audio streams that don't have 16 bits per sample are not supported
|
||||
return ESP_ERR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
this->audio_stream_info_ = audio::AudioStreamInfo(stream_info.get_bits_per_sample(), this->output_channels_,
|
||||
stream_info.get_sample_rate());
|
||||
this->output_speaker_->set_audio_stream_info(this->audio_stream_info_.value());
|
||||
} else {
|
||||
if (!this->queue_mode_ && (stream_info.get_sample_rate() != this->audio_stream_info_.value().get_sample_rate())) {
|
||||
// The two audio streams must have the same sample rate to mix properly if not in queue mode
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
}
|
||||
|
||||
return this->start_task_();
|
||||
}
|
||||
|
||||
esp_err_t MixerSpeaker::start_task_() {
|
||||
if (this->task_stack_buffer_ == nullptr) {
|
||||
if (this->task_stack_in_psram_) {
|
||||
RAMAllocator<StackType_t> stack_allocator(RAMAllocator<StackType_t>::ALLOC_EXTERNAL);
|
||||
this->task_stack_buffer_ = stack_allocator.allocate(TASK_STACK_SIZE);
|
||||
} else {
|
||||
RAMAllocator<StackType_t> stack_allocator(RAMAllocator<StackType_t>::ALLOC_INTERNAL);
|
||||
this->task_stack_buffer_ = stack_allocator.allocate(TASK_STACK_SIZE);
|
||||
}
|
||||
}
|
||||
|
||||
if (this->task_stack_buffer_ == nullptr) {
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
if (this->task_handle_ == nullptr) {
|
||||
this->task_handle_ = xTaskCreateStatic(audio_mixer_task, "mixer", TASK_STACK_SIZE, (void *) this,
|
||||
MIXER_TASK_PRIORITY, this->task_stack_buffer_, &this->task_stack_);
|
||||
}
|
||||
|
||||
if (this->task_handle_ == nullptr) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t MixerSpeaker::delete_task_() {
|
||||
if (!this->task_created_) {
|
||||
this->task_handle_ = nullptr;
|
||||
|
||||
if (this->task_stack_buffer_ != nullptr) {
|
||||
if (this->task_stack_in_psram_) {
|
||||
RAMAllocator<StackType_t> stack_allocator(RAMAllocator<StackType_t>::ALLOC_EXTERNAL);
|
||||
stack_allocator.deallocate(this->task_stack_buffer_, TASK_STACK_SIZE);
|
||||
} else {
|
||||
RAMAllocator<StackType_t> stack_allocator(RAMAllocator<StackType_t>::ALLOC_INTERNAL);
|
||||
stack_allocator.deallocate(this->task_stack_buffer_, TASK_STACK_SIZE);
|
||||
}
|
||||
|
||||
this->task_stack_buffer_ = nullptr;
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
void MixerSpeaker::stop() { xEventGroupSetBits(this->event_group_, MixerEventGroupBits::COMMAND_STOP); }
|
||||
|
||||
void MixerSpeaker::copy_frames(const int16_t *input_buffer, audio::AudioStreamInfo input_stream_info,
|
||||
int16_t *output_buffer, audio::AudioStreamInfo output_stream_info,
|
||||
uint32_t frames_to_transfer) {
|
||||
uint8_t input_channels = input_stream_info.get_channels();
|
||||
uint8_t output_channels = output_stream_info.get_channels();
|
||||
const uint8_t max_input_channel_index = input_channels - 1;
|
||||
|
||||
if (input_channels == output_channels) {
|
||||
size_t bytes_to_copy = input_stream_info.frames_to_bytes(frames_to_transfer);
|
||||
memcpy(output_buffer, input_buffer, bytes_to_copy);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
for (uint32_t frame_index = 0; frame_index < frames_to_transfer; ++frame_index) {
|
||||
for (uint8_t output_channel_index = 0; output_channel_index < output_channels; ++output_channel_index) {
|
||||
uint8_t input_channel_index = std::min(output_channel_index, max_input_channel_index);
|
||||
output_buffer[output_channels * frame_index + output_channel_index] =
|
||||
input_buffer[input_channels * frame_index + input_channel_index];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MixerSpeaker::mix_audio_samples(const int16_t *primary_buffer, audio::AudioStreamInfo primary_stream_info,
|
||||
const int16_t *secondary_buffer, audio::AudioStreamInfo secondary_stream_info,
|
||||
int16_t *output_buffer, audio::AudioStreamInfo output_stream_info,
|
||||
uint32_t frames_to_mix) {
|
||||
const uint8_t primary_channels = primary_stream_info.get_channels();
|
||||
const uint8_t secondary_channels = secondary_stream_info.get_channels();
|
||||
const uint8_t output_channels = output_stream_info.get_channels();
|
||||
|
||||
const uint8_t max_primary_channel_index = primary_channels - 1;
|
||||
const uint8_t max_secondary_channel_index = secondary_channels - 1;
|
||||
|
||||
for (uint32_t frames_index = 0; frames_index < frames_to_mix; ++frames_index) {
|
||||
for (uint8_t output_channel_index = 0; output_channel_index < output_channels; ++output_channel_index) {
|
||||
const uint32_t secondary_channel_index = std::min(output_channel_index, max_secondary_channel_index);
|
||||
const int32_t secondary_sample = secondary_buffer[frames_index * secondary_channels + secondary_channel_index];
|
||||
|
||||
const uint32_t primary_channel_index = std::min(output_channel_index, max_primary_channel_index);
|
||||
const int32_t primary_sample =
|
||||
static_cast<int32_t>(primary_buffer[frames_index * primary_channels + primary_channel_index]);
|
||||
|
||||
const int32_t added_sample = secondary_sample + primary_sample;
|
||||
|
||||
output_buffer[frames_index * output_channels + output_channel_index] =
|
||||
static_cast<int16_t>(clamp<int32_t>(added_sample, MIN_AUDIO_SAMPLE_VALUE, MAX_AUDIO_SAMPLE_VALUE));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MixerSpeaker::audio_mixer_task(void *params) {
|
||||
MixerSpeaker *this_mixer = (MixerSpeaker *) params;
|
||||
|
||||
xEventGroupSetBits(this_mixer->event_group_, MixerEventGroupBits::STATE_STARTING);
|
||||
|
||||
this_mixer->task_created_ = true;
|
||||
|
||||
std::unique_ptr<audio::AudioSinkTransferBuffer> output_transfer_buffer = audio::AudioSinkTransferBuffer::create(
|
||||
this_mixer->audio_stream_info_.value().ms_to_bytes(TRANSFER_BUFFER_DURATION_MS));
|
||||
|
||||
if (output_transfer_buffer == nullptr) {
|
||||
xEventGroupSetBits(this_mixer->event_group_,
|
||||
MixerEventGroupBits::STATE_STOPPED | MixerEventGroupBits::ERR_ESP_NO_MEM);
|
||||
|
||||
this_mixer->task_created_ = false;
|
||||
vTaskDelete(nullptr);
|
||||
}
|
||||
|
||||
output_transfer_buffer->set_sink(this_mixer->output_speaker_);
|
||||
|
||||
xEventGroupSetBits(this_mixer->event_group_, MixerEventGroupBits::STATE_RUNNING);
|
||||
|
||||
bool sent_finished = false;
|
||||
|
||||
while (true) {
|
||||
uint32_t event_group_bits = xEventGroupGetBits(this_mixer->event_group_);
|
||||
if (event_group_bits & MixerEventGroupBits::COMMAND_STOP) {
|
||||
break;
|
||||
}
|
||||
|
||||
output_transfer_buffer->transfer_data_to_sink(pdMS_TO_TICKS(TASK_DELAY_MS));
|
||||
|
||||
const uint32_t output_frames_free =
|
||||
this_mixer->audio_stream_info_.value().bytes_to_frames(output_transfer_buffer->free());
|
||||
|
||||
std::vector<SourceSpeaker *> speakers_with_data;
|
||||
std::vector<std::shared_ptr<audio::AudioSourceTransferBuffer>> transfer_buffers_with_data;
|
||||
|
||||
for (auto &speaker : this_mixer->source_speakers_) {
|
||||
if (speaker->get_transfer_buffer().use_count() > 0) {
|
||||
std::shared_ptr<audio::AudioSourceTransferBuffer> transfer_buffer = speaker->get_transfer_buffer().lock();
|
||||
speaker->process_data_from_source(0); // Transfers and ducks audio from source ring buffers
|
||||
|
||||
if ((transfer_buffer->available() > 0) && !speaker->get_pause_state()) {
|
||||
// Store the locked transfer buffers in their own vector to avoid releasing ownership until after the loop
|
||||
transfer_buffers_with_data.push_back(transfer_buffer);
|
||||
speakers_with_data.push_back(speaker);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (transfer_buffers_with_data.empty()) {
|
||||
// No audio available for transferring, block task temporarily
|
||||
delay(TASK_DELAY_MS);
|
||||
continue;
|
||||
}
|
||||
|
||||
uint32_t frames_to_mix = output_frames_free;
|
||||
|
||||
if ((transfer_buffers_with_data.size() == 1) || this_mixer->queue_mode_) {
|
||||
// Only one speaker has audio data, just copy samples over
|
||||
|
||||
audio::AudioStreamInfo active_stream_info = speakers_with_data[0]->get_audio_stream_info();
|
||||
|
||||
if (active_stream_info.get_sample_rate() ==
|
||||
this_mixer->output_speaker_->get_audio_stream_info().get_sample_rate()) {
|
||||
// Speaker's sample rate matches the output speaker's, copy directly
|
||||
|
||||
const uint32_t frames_available_in_buffer =
|
||||
active_stream_info.bytes_to_frames(transfer_buffers_with_data[0]->available());
|
||||
frames_to_mix = std::min(frames_to_mix, frames_available_in_buffer);
|
||||
copy_frames(reinterpret_cast<int16_t *>(transfer_buffers_with_data[0]->get_buffer_start()), active_stream_info,
|
||||
reinterpret_cast<int16_t *>(output_transfer_buffer->get_buffer_end()),
|
||||
this_mixer->audio_stream_info_.value(), frames_to_mix);
|
||||
|
||||
// Update source speaker buffer length
|
||||
transfer_buffers_with_data[0]->decrease_buffer_length(active_stream_info.frames_to_bytes(frames_to_mix));
|
||||
speakers_with_data[0]->accumulated_frames_read_ += frames_to_mix;
|
||||
|
||||
// Add new audio duration to the source speaker pending playback
|
||||
speakers_with_data[0]->pending_playback_ms_ +=
|
||||
active_stream_info.frames_to_milliseconds_with_remainder(&speakers_with_data[0]->accumulated_frames_read_);
|
||||
|
||||
// Update output transfer buffer length
|
||||
output_transfer_buffer->increase_buffer_length(
|
||||
this_mixer->audio_stream_info_.value().frames_to_bytes(frames_to_mix));
|
||||
} else {
|
||||
// Speaker's stream info doesn't match the output speaker's, so it's a new source speaker
|
||||
if (!this_mixer->output_speaker_->is_stopped()) {
|
||||
if (!sent_finished) {
|
||||
this_mixer->output_speaker_->finish();
|
||||
sent_finished = true; // Avoid repeatedly sending the finish command
|
||||
}
|
||||
} else {
|
||||
// Speaker has finished writing the current audio, update the stream information and restart the speaker
|
||||
this_mixer->audio_stream_info_ =
|
||||
audio::AudioStreamInfo(active_stream_info.get_bits_per_sample(), this_mixer->output_channels_,
|
||||
active_stream_info.get_sample_rate());
|
||||
this_mixer->output_speaker_->set_audio_stream_info(this_mixer->audio_stream_info_.value());
|
||||
this_mixer->output_speaker_->start();
|
||||
sent_finished = false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Determine how many frames to mix
|
||||
for (int i = 0; i < transfer_buffers_with_data.size(); ++i) {
|
||||
const uint32_t frames_available_in_buffer =
|
||||
speakers_with_data[i]->get_audio_stream_info().bytes_to_frames(transfer_buffers_with_data[i]->available());
|
||||
frames_to_mix = std::min(frames_to_mix, frames_available_in_buffer);
|
||||
}
|
||||
int16_t *primary_buffer = reinterpret_cast<int16_t *>(transfer_buffers_with_data[0]->get_buffer_start());
|
||||
audio::AudioStreamInfo primary_stream_info = speakers_with_data[0]->get_audio_stream_info();
|
||||
|
||||
// Mix two streams together
|
||||
for (int i = 1; i < transfer_buffers_with_data.size(); ++i) {
|
||||
mix_audio_samples(primary_buffer, primary_stream_info,
|
||||
reinterpret_cast<int16_t *>(transfer_buffers_with_data[i]->get_buffer_start()),
|
||||
speakers_with_data[i]->get_audio_stream_info(),
|
||||
reinterpret_cast<int16_t *>(output_transfer_buffer->get_buffer_end()),
|
||||
this_mixer->audio_stream_info_.value(), frames_to_mix);
|
||||
|
||||
speakers_with_data[i]->pending_playback_ms_ +=
|
||||
speakers_with_data[i]->get_audio_stream_info().frames_to_milliseconds_with_remainder(
|
||||
&speakers_with_data[i]->accumulated_frames_read_);
|
||||
|
||||
if (i != transfer_buffers_with_data.size() - 1) {
|
||||
// Need to mix more streams together, point primary buffer and stream info to the already mixed output
|
||||
primary_buffer = reinterpret_cast<int16_t *>(output_transfer_buffer->get_buffer_end());
|
||||
primary_stream_info = this_mixer->audio_stream_info_.value();
|
||||
}
|
||||
}
|
||||
|
||||
// Update source transfer buffer lengths and add new audio durations to the source speaker pending playbacks
|
||||
for (int i = 0; i < transfer_buffers_with_data.size(); ++i) {
|
||||
transfer_buffers_with_data[i]->decrease_buffer_length(
|
||||
speakers_with_data[i]->get_audio_stream_info().frames_to_bytes(frames_to_mix));
|
||||
speakers_with_data[i]->accumulated_frames_read_ += frames_to_mix;
|
||||
|
||||
speakers_with_data[i]->pending_playback_ms_ +=
|
||||
speakers_with_data[i]->get_audio_stream_info().frames_to_milliseconds_with_remainder(
|
||||
&speakers_with_data[i]->accumulated_frames_read_);
|
||||
}
|
||||
|
||||
// Update output transfer buffer length
|
||||
output_transfer_buffer->increase_buffer_length(
|
||||
this_mixer->audio_stream_info_.value().frames_to_bytes(frames_to_mix));
|
||||
}
|
||||
}
|
||||
|
||||
xEventGroupSetBits(this_mixer->event_group_, MixerEventGroupBits::STATE_STOPPING);
|
||||
|
||||
output_transfer_buffer.reset();
|
||||
|
||||
xEventGroupSetBits(this_mixer->event_group_, MixerEventGroupBits::STATE_STOPPED);
|
||||
this_mixer->task_created_ = false;
|
||||
vTaskDelete(nullptr);
|
||||
}
|
||||
|
||||
} // namespace mixer_speaker
|
||||
} // namespace esphome
|
||||
|
||||
#endif
|
207
esphome/components/mixer/speaker/mixer_speaker.h
Normal file
207
esphome/components/mixer/speaker/mixer_speaker.h
Normal file
@ -0,0 +1,207 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include "esphome/components/audio/audio.h"
|
||||
#include "esphome/components/audio/audio_transfer_buffer.h"
|
||||
#include "esphome/components/speaker/speaker.h"
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
#include <freertos/event_groups.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
|
||||
namespace esphome {
|
||||
namespace mixer_speaker {
|
||||
|
||||
/* Classes for mixing several source speaker audio streams and writing it to another speaker component.
|
||||
* - Volume controls are passed through to the output speaker
|
||||
* - Directly handles pausing at the SourceSpeaker level; pause state is not passed through to the output speaker.
|
||||
* - Audio sent to the SourceSpeaker's must have 16 bits per sample.
|
||||
* - Audio sent to the SourceSpeaker can have any number of channels. They are duplicated or ignored as needed to match
|
||||
* the number of channels required for the output speaker.
|
||||
* - In queue mode, the audio sent to the SoureSpeakers can have different sample rates.
|
||||
* - In non-queue mode, the audio sent to the SourceSpeakers must have the same sample rates.
|
||||
* - SourceSpeaker has an internal ring buffer. It also allocates a shared_ptr for an AudioTranserBuffer object.
|
||||
* - Audio Data Flow:
|
||||
* - Audio data played on a SourceSpeaker first writes to its internal ring buffer.
|
||||
* - MixerSpeaker task temporarily takes shared ownership of each SourceSpeaker's AudioTransferBuffer.
|
||||
* - MixerSpeaker calls SourceSpeaker's `process_data_from_source`, which tranfers audio from the SourceSpeaker's
|
||||
* ring buffer to its AudioTransferBuffer. Audio ducking is applied at this step.
|
||||
* - In queue mode, MixerSpeaker prioritizes the earliest configured SourceSpeaker with audio data. Audio data is
|
||||
* sent to the output speaker.
|
||||
* - In non-queue mode, MixerSpeaker adds all the audio data in each SourceSpeaker into one stream that is written
|
||||
* to the output speaker.
|
||||
*/
|
||||
|
||||
class MixerSpeaker;
|
||||
|
||||
class SourceSpeaker : public speaker::Speaker, public Component {
|
||||
public:
|
||||
void dump_config() override;
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
|
||||
size_t play(const uint8_t *data, size_t length, TickType_t ticks_to_wait) override;
|
||||
size_t play(const uint8_t *data, size_t length) override { return this->play(data, length, 0); }
|
||||
|
||||
void start() override;
|
||||
void stop() override;
|
||||
void finish() override;
|
||||
|
||||
bool has_buffered_data() const override;
|
||||
|
||||
/// @brief Mute state changes are passed to the parent's output speaker
|
||||
void set_mute_state(bool mute_state) override;
|
||||
|
||||
/// @brief Volume state changes are passed to the parent's output speaker
|
||||
void set_volume(float volume) override;
|
||||
|
||||
void set_pause_state(bool pause_state) override { this->pause_state_ = pause_state; }
|
||||
bool get_pause_state() const override { return this->pause_state_; }
|
||||
|
||||
/// @brief Transfers audio from the ring buffer into the transfer buffer. Ducks audio while transferring.
|
||||
/// @param ticks_to_wait FreeRTOS ticks to wait while waiting to read from the ring buffer.
|
||||
/// @return Number of bytes transferred from the ring buffer.
|
||||
size_t process_data_from_source(TickType_t ticks_to_wait);
|
||||
|
||||
/// @brief Sets the ducking level for the source speaker.
|
||||
/// @param decibel_reduction (uint8_t) The dB reduction level. For example, 0 is no change, 10 is a reduction by 10 dB
|
||||
/// @param duration (uint32_t) The number of milliseconds to transition from the current level to the new level
|
||||
void apply_ducking(uint8_t decibel_reduction, uint32_t duration);
|
||||
|
||||
void set_buffer_duration(uint32_t buffer_duration_ms) { this->buffer_duration_ms_ = buffer_duration_ms; }
|
||||
void set_parent(MixerSpeaker *parent) { this->parent_ = parent; }
|
||||
void set_timeout(uint32_t ms) { this->timeout_ms_ = ms; }
|
||||
|
||||
std::weak_ptr<audio::AudioSourceTransferBuffer> get_transfer_buffer() { return this->transfer_buffer_; }
|
||||
|
||||
protected:
|
||||
friend class MixerSpeaker;
|
||||
esp_err_t start_();
|
||||
void stop_();
|
||||
|
||||
/// @brief Ducks audio samples by a specified amount. When changing the ducking amount, it can transition gradually
|
||||
/// over a specified amount of samples.
|
||||
/// @param input_buffer buffer with audio samples to be ducked in place
|
||||
/// @param input_samples_to_duck number of samples to process in ``input_buffer``
|
||||
/// @param current_ducking_db_reduction pointer to the current dB reduction
|
||||
/// @param ducking_transition_samples_remaining pointer to the total number of samples left before the the
|
||||
/// transition is finished
|
||||
/// @param samples_per_ducking_step total number of samples per ducking step for the transition
|
||||
/// @param db_change_per_ducking_step the change in dB reduction per step
|
||||
static void duck_samples(int16_t *input_buffer, uint32_t input_samples_to_duck, int8_t *current_ducking_db_reduction,
|
||||
uint32_t *ducking_transition_samples_remaining, uint32_t samples_per_ducking_step,
|
||||
int8_t db_change_per_ducking_step);
|
||||
|
||||
MixerSpeaker *parent_;
|
||||
|
||||
std::shared_ptr<audio::AudioSourceTransferBuffer> transfer_buffer_;
|
||||
std::weak_ptr<RingBuffer> ring_buffer_;
|
||||
|
||||
uint32_t buffer_duration_ms_;
|
||||
uint32_t last_seen_data_ms_{0};
|
||||
optional<uint32_t> timeout_ms_;
|
||||
bool stop_gracefully_{false};
|
||||
|
||||
bool pause_state_{false};
|
||||
|
||||
int8_t target_ducking_db_reduction_{0};
|
||||
int8_t current_ducking_db_reduction_{0};
|
||||
int8_t db_change_per_ducking_step_{1};
|
||||
uint32_t ducking_transition_samples_remaining_{0};
|
||||
uint32_t samples_per_ducking_step_{0};
|
||||
|
||||
uint32_t accumulated_frames_read_{0};
|
||||
|
||||
uint32_t pending_playback_ms_{0};
|
||||
};
|
||||
|
||||
class MixerSpeaker : public Component {
|
||||
public:
|
||||
void dump_config() override;
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
|
||||
void add_source_speaker(SourceSpeaker *source_speaker) { this->source_speakers_.push_back(source_speaker); }
|
||||
|
||||
/// @brief Starts the mixer task. Called by a source speaker giving the current audio stream information
|
||||
/// @param stream_info The calling source speakers audio stream information
|
||||
/// @return ESP_ERR_NOT_SUPPORTED if the incoming stream is incompatible due to unsupported bits per sample
|
||||
/// ESP_ERR_INVALID_ARG if the incoming stream is incompatible to be mixed with the other input audio stream
|
||||
/// ESP_ERR_NO_MEM if there isn't enough memory for the task's stack
|
||||
/// ESP_ERR_INVALID_STATE if the task fails to start
|
||||
/// ESP_OK if the incoming stream is compatible and the mixer task starts
|
||||
esp_err_t start(audio::AudioStreamInfo &stream_info);
|
||||
|
||||
void stop();
|
||||
|
||||
void set_output_channels(uint8_t output_channels) { this->output_channels_ = output_channels; }
|
||||
void set_output_speaker(speaker::Speaker *speaker) { this->output_speaker_ = speaker; }
|
||||
void set_queue_mode(bool queue_mode) { this->queue_mode_ = queue_mode; }
|
||||
void set_task_stack_in_psram(bool task_stack_in_psram) { this->task_stack_in_psram_ = task_stack_in_psram; }
|
||||
|
||||
speaker::Speaker *get_output_speaker() const { return this->output_speaker_; }
|
||||
|
||||
protected:
|
||||
/// @brief Copies audio frames from the input buffer to the output buffer taking into account the number of channels
|
||||
/// in each stream. If the output stream has more channels, the input samples are duplicated. If the output stream has
|
||||
/// less channels, the extra channel input samples are dropped.
|
||||
/// @param input_buffer
|
||||
/// @param input_stream_info
|
||||
/// @param output_buffer
|
||||
/// @param output_stream_info
|
||||
/// @param frames_to_transfer number of frames (consisting of a sample for each channel) to copy from the input buffer
|
||||
static void copy_frames(const int16_t *input_buffer, audio::AudioStreamInfo input_stream_info, int16_t *output_buffer,
|
||||
audio::AudioStreamInfo output_stream_info, uint32_t frames_to_transfer);
|
||||
|
||||
/// @brief Mixes the primary and secondary streams taking into account the number of channels in each stream. Primary
|
||||
/// and secondary samples are duplicated or dropped as necessary to ensure the output stream has the configured number
|
||||
/// of channels. Output samples are clamped to the corresponding int16 min or max values if the mixed sample
|
||||
/// overflows.
|
||||
/// @param primary_buffer (int16_t *) samples buffer for the primary stream
|
||||
/// @param primary_stream_info stream info for the primary stream
|
||||
/// @param secondary_buffer (int16_t *) samples buffer for secondary stream
|
||||
/// @param secondary_stream_info stream info for the secondary stream
|
||||
/// @param output_buffer (int16_t *) buffer for the mixed samples
|
||||
/// @param output_stream_info stream info for the output buffer
|
||||
/// @param frames_to_mix number of frames in the primary and secondary buffers to mix together
|
||||
static void mix_audio_samples(const int16_t *primary_buffer, audio::AudioStreamInfo primary_stream_info,
|
||||
const int16_t *secondary_buffer, audio::AudioStreamInfo secondary_stream_info,
|
||||
int16_t *output_buffer, audio::AudioStreamInfo output_stream_info,
|
||||
uint32_t frames_to_mix);
|
||||
|
||||
static void audio_mixer_task(void *params);
|
||||
|
||||
/// @brief Starts the mixer task after allocating memory for the task stack.
|
||||
/// @return ESP_ERR_NO_MEM if there isn't enough memory for the task's stack
|
||||
/// ESP_ERR_INVALID_STATE if the task didn't start
|
||||
/// ESP_OK if successful
|
||||
esp_err_t start_task_();
|
||||
|
||||
/// @brief If the task is stopped, it sets the task handle to the nullptr and deallocates its stack
|
||||
/// @return ESP_OK if the task was stopped, ESP_ERR_INVALID_STATE otherwise.
|
||||
esp_err_t delete_task_();
|
||||
|
||||
EventGroupHandle_t event_group_{nullptr};
|
||||
|
||||
std::vector<SourceSpeaker *> source_speakers_;
|
||||
speaker::Speaker *output_speaker_{nullptr};
|
||||
|
||||
uint8_t output_channels_;
|
||||
bool queue_mode_;
|
||||
bool task_stack_in_psram_{false};
|
||||
|
||||
bool task_created_{false};
|
||||
|
||||
TaskHandle_t task_handle_{nullptr};
|
||||
StaticTask_t task_stack_;
|
||||
StackType_t *task_stack_buffer_{nullptr};
|
||||
|
||||
optional<audio::AudioStreamInfo> audio_stream_info_;
|
||||
};
|
||||
|
||||
} // namespace mixer_speaker
|
||||
} // namespace esphome
|
||||
|
||||
#endif
|
@ -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
|
||||
|
@ -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")
|
||||
|
@ -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)
|
||||
|
@ -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):
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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_); }
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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:
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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) {
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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)
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user