1
0
mirror of https://github.com/esphome/esphome.git synced 2025-03-21 18:18:13 +00:00

Merge pull request #8438 from esphome/bump-2025.3.0

2025.3.0
This commit is contained in:
Keith Burzinski 2025-03-19 23:37:13 -05:00 committed by GitHub
commit c4de9e87e4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
201 changed files with 7430 additions and 1261 deletions

View File

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

View File

@ -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.15.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.15.0
env:
DOCKER_BUILD_SUMMARY: false
DOCKER_BUILD_RECORD_UPLOAD: false

View File

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

View File

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

View File

@ -47,7 +47,7 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore Python virtual environment
id: cache-venv
uses: actions/cache@v4.2.0
uses: actions/cache@v4.2.2
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.2
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.2
with:
path: ~/.platformio
key: platformio-${{ matrix.pio_cache_key }}
@ -482,7 +482,7 @@ jobs:
runs-on: ubuntu-24.04
needs:
- common
- black
- ruff
- ci-custom
- clang-format
- flake8

View File

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

View File

@ -89,10 +89,10 @@ jobs:
python-version: "3.9"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.9.0
uses: docker/setup-buildx-action@v3.10.0
- name: Set up QEMU
if: matrix.platform != 'linux/amd64'
uses: docker/setup-qemu-action@v3.4.0
uses: docker/setup-qemu-action@v3.6.0
- name: Log in to docker hub
uses: docker/login-action@v3.3.0
@ -140,7 +140,7 @@ jobs:
echo name=$(cat /tmp/platform) >> $GITHUB_OUTPUT
- name: Upload digests
uses: actions/upload-artifact@v4.6.0
uses: actions/upload-artifact@v4.6.1
with:
name: digests-${{ steps.sanitize.outputs.name }}
path: /tmp/digests
@ -176,14 +176,14 @@ jobs:
- uses: actions/checkout@v4.1.7
- name: Download digests
uses: actions/download-artifact@v4.1.8
uses: actions/download-artifact@v4.1.9
with:
pattern: digests-*
path: /tmp/digests
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.9.0
uses: docker/setup-buildx-action@v3.10.0
- name: Log in to docker hub
if: matrix.registry == 'dockerhub'

View File

@ -36,7 +36,7 @@ jobs:
python ./script/sync-device_class.py
- name: Commit changes
uses: peter-evans/create-pull-request@v7.0.6
uses: peter-evans/create-pull-request@v7.0.7
with:
commit-message: "Synchronise Device Classes from Home Assistant"
committer: esphomebot <esphome@nabucasa.com>

View File

@ -4,7 +4,7 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.5.4
rev: v0.9.2
hooks:
# Run the linter.
- id: ruff

View File

@ -93,6 +93,7 @@ esphome/components/captive_portal/* @OttoWinter
esphome/components/ccs811/* @habbie
esphome/components/cd74hc4067/* @asoehlke
esphome/components/ch422g/* @clydebarrow @jesterret
esphome/components/chsc6x/* @kkosik20
esphome/components/climate/* @esphome/core
esphome/components/climate_ir/* @glmnet
esphome/components/color_temperature/* @jesserockz
@ -234,6 +235,7 @@ esphome/components/kuntze/* @ssieb
esphome/components/lcd_menu/* @numo68
esphome/components/ld2410/* @regevbr @sebcaps
esphome/components/ld2420/* @descipher
esphome/components/ld2450/* @hareeshmu
esphome/components/ledc/* @OttoWinter
esphome/components/libretiny/* @kuba2k2
esphome/components/libretiny_pwm/* @kuba2k2
@ -297,6 +299,7 @@ esphome/components/mopeka_std_check/* @Fabian-Schmidt
esphome/components/mpl3115a2/* @kbickar
esphome/components/mpu6886/* @fabaff
esphome/components/ms8607/* @e28eta
esphome/components/msa3xx/* @latonita
esphome/components/nau7802/* @cujomalainey
esphome/components/network/* @esphome/core
esphome/components/nextion/* @edwardtfn @senexcrenshaw
@ -445,6 +448,7 @@ esphome/components/tmp102/* @timsavage
esphome/components/tmp1075/* @sybrenstuvel
esphome/components/tmp117/* @Azimath
esphome/components/tof10120/* @wstrzalka
esphome/components/tormatic/* @ti-mo
esphome/components/toshiba/* @kbx81
esphome/components/touchscreen/* @jesserockz @nielsnl68
esphome/components/tsl2591/* @wjcarpenter

View File

@ -33,9 +33,9 @@ RUN \
python3-venv=3.11.2-1+b1 \
python3-wheel=0.38.4-2 \
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+deb12u4 \
git=1:2.39.5-0+deb12u2 \
curl=7.88.1-10+deb12u12 \
openssh-client=1:9.2p1-2+deb12u5 \
python3-cffi=1.15.1-5 \
libcairo2=1.16.0-7 \
libmagic1=1:5.44-3 \
@ -76,7 +76,7 @@ BUILD_DEPS="
python3-dev=3.11.2-1+b1
zlib1g-dev=1:1.2.13.dfsg-1
libjpeg-dev=1:2.1.5-2
libfreetype-dev=2.12.1+dfsg-5+deb12u3
libfreetype-dev=2.12.1+dfsg-5+deb12u4
libssl-dev=3.0.15-1~deb12u1
libffi-dev=3.4.4-1
cargo=0.66.0+ds1-1
@ -84,7 +84,7 @@ BUILD_DEPS="
"
LIB_DEPS="
libtiff6=4.5.0-6+deb12u1
libopenjp2-7=2.5.0-2
libopenjp2-7=2.5.0-2+deb12u1
"
if [ "$TARGETARCH$TARGETVARIANT" = "arm64" ]
then
@ -160,7 +160,7 @@ RUN \
apt-get update \
# Use pinned versions so that we get updates with build caching
&& apt-get install -y --no-install-recommends \
nginx-light=1.22.1-9 \
nginx-light=1.22.1-9+deb12u1 \
&& rm -rf \
/tmp/* \
/var/{cache,log}/* \

View File

@ -23,10 +23,6 @@ if bashio::config.true 'streamer_mode'; then
export ESPHOME_STREAMER_MODE=true
fi
if bashio::config.true 'status_use_ping'; then
export ESPHOME_DASHBOARD_USE_PING=true
fi
if bashio::config.has_value 'relative_url'; then
export ESPHOME_DASHBOARD_RELATIVE_URL=$(bashio::config 'relative_url')
fi

View File

@ -2,6 +2,7 @@
import argparse
from datetime import datetime
import functools
import importlib
import logging
import os
import re
@ -66,7 +67,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}")
@ -336,6 +337,13 @@ def check_permissions(port):
def upload_program(config, args, host):
try:
module = importlib.import_module("esphome.components." + CORE.target_platform)
if getattr(module, "upload_program")(config, args, host):
return 0
except AttributeError:
pass
if get_port_type(host) == "SERIAL":
check_permissions(host)
if CORE.target_platform in (PLATFORM_ESP32, PLATFORM_ESP8266):

View File

@ -227,6 +227,9 @@ message DeviceInfoResponse {
uint32 voice_assistant_feature_flags = 17;
string suggested_area = 16;
// The Bluetooth mac address of the device. For example "AC:BC:32:89:0E:AA"
string bluetooth_mac_address = 18;
}
message ListEntitiesRequest {
@ -1564,6 +1567,8 @@ message VoiceAssistantAnnounceRequest {
string media_id = 1;
string text = 2;
string preannounce_media_id = 3;
bool start_conversation = 4;
}
message VoiceAssistantAnnounceFinished {

View File

@ -28,8 +28,38 @@ namespace api {
static const char *const TAG = "api.connection";
static const int ESP32_CAMERA_STOP_STREAM = 5000;
// helper for allowing only unique entries in the queue
void DeferredMessageQueue::dmq_push_back_with_dedup_(void *source, send_message_t *send_message) {
DeferredMessage item(source, send_message);
auto iter = std::find_if(this->deferred_queue_.begin(), this->deferred_queue_.end(),
[&item](const DeferredMessage &test) -> bool { return test == item; });
if (iter != this->deferred_queue_.end()) {
(*iter) = item;
} else {
this->deferred_queue_.push_back(item);
}
}
void DeferredMessageQueue::process_queue() {
while (!deferred_queue_.empty()) {
DeferredMessage &de = deferred_queue_.front();
if (de.send_message_(this->api_connection_, de.source_)) {
// O(n) but memory efficiency is more important than speed here which is why std::vector was chosen
deferred_queue_.erase(deferred_queue_.begin());
} else {
break;
}
}
}
void DeferredMessageQueue::defer(void *source, send_message_t *send_message) {
this->dmq_push_back_with_dedup_(source, send_message);
}
APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *parent)
: parent_(parent), initial_state_iterator_(this), list_entities_iterator_(this) {
: parent_(parent), deferred_message_queue_(this), initial_state_iterator_(this), list_entities_iterator_(this) {
this->proto_write_buffer_.reserve(64);
#if defined(USE_API_PLAINTEXT)
@ -116,7 +146,11 @@ void APIConnection::loop() {
return;
}
this->deferred_message_queue_.process_queue();
if (!this->list_entities_iterator_.completed())
this->list_entities_iterator_.advance();
if (!this->initial_state_iterator_.completed() && this->list_entities_iterator_.completed())
this->initial_state_iterator_.advance();
static uint32_t keepalive = 60000;
@ -210,13 +244,31 @@ bool APIConnection::send_binary_sensor_state(binary_sensor::BinarySensor *binary
if (!this->state_subscription_)
return false;
if (!APIConnection::try_send_binary_sensor_state(this, binary_sensor, state)) {
this->deferred_message_queue_.defer(binary_sensor, try_send_binary_sensor_state);
}
return true;
}
void APIConnection::send_binary_sensor_info(binary_sensor::BinarySensor *binary_sensor) {
if (!APIConnection::try_send_binary_sensor_info(this, binary_sensor)) {
this->deferred_message_queue_.defer(binary_sensor, try_send_binary_sensor_info);
}
}
bool APIConnection::try_send_binary_sensor_state(APIConnection *api, void *v_binary_sensor) {
binary_sensor::BinarySensor *binary_sensor = reinterpret_cast<binary_sensor::BinarySensor *>(v_binary_sensor);
return APIConnection::try_send_binary_sensor_state(api, binary_sensor, binary_sensor->state);
}
bool APIConnection::try_send_binary_sensor_state(APIConnection *api, binary_sensor::BinarySensor *binary_sensor,
bool state) {
BinarySensorStateResponse resp;
resp.key = binary_sensor->get_object_id_hash();
resp.state = state;
resp.missing_state = !binary_sensor->has_state();
return this->send_binary_sensor_state_response(resp);
return api->send_binary_sensor_state_response(resp);
}
bool APIConnection::send_binary_sensor_info(binary_sensor::BinarySensor *binary_sensor) {
bool APIConnection::try_send_binary_sensor_info(APIConnection *api, void *v_binary_sensor) {
binary_sensor::BinarySensor *binary_sensor = reinterpret_cast<binary_sensor::BinarySensor *>(v_binary_sensor);
ListEntitiesBinarySensorResponse msg;
msg.object_id = binary_sensor->get_object_id();
msg.key = binary_sensor->get_object_id_hash();
@ -228,7 +280,7 @@ bool APIConnection::send_binary_sensor_info(binary_sensor::BinarySensor *binary_
msg.disabled_by_default = binary_sensor->is_disabled_by_default();
msg.icon = binary_sensor->get_icon();
msg.entity_category = static_cast<enums::EntityCategory>(binary_sensor->get_entity_category());
return this->send_list_entities_binary_sensor_response(msg);
return api->send_list_entities_binary_sensor_response(msg);
}
#endif
@ -237,6 +289,19 @@ bool APIConnection::send_cover_state(cover::Cover *cover) {
if (!this->state_subscription_)
return false;
if (!APIConnection::try_send_cover_state(this, cover)) {
this->deferred_message_queue_.defer(cover, try_send_cover_state);
}
return true;
}
void APIConnection::send_cover_info(cover::Cover *cover) {
if (!APIConnection::try_send_cover_info(this, cover)) {
this->deferred_message_queue_.defer(cover, try_send_cover_info);
}
}
bool APIConnection::try_send_cover_state(APIConnection *api, void *v_cover) {
cover::Cover *cover = reinterpret_cast<cover::Cover *>(v_cover);
auto traits = cover->get_traits();
CoverStateResponse resp{};
resp.key = cover->get_object_id_hash();
@ -246,9 +311,10 @@ bool APIConnection::send_cover_state(cover::Cover *cover) {
if (traits.get_supports_tilt())
resp.tilt = cover->tilt;
resp.current_operation = static_cast<enums::CoverOperation>(cover->current_operation);
return this->send_cover_state_response(resp);
return api->send_cover_state_response(resp);
}
bool APIConnection::send_cover_info(cover::Cover *cover) {
bool APIConnection::try_send_cover_info(APIConnection *api, void *v_cover) {
cover::Cover *cover = reinterpret_cast<cover::Cover *>(v_cover);
auto traits = cover->get_traits();
ListEntitiesCoverResponse msg;
msg.key = cover->get_object_id_hash();
@ -264,7 +330,7 @@ bool APIConnection::send_cover_info(cover::Cover *cover) {
msg.disabled_by_default = cover->is_disabled_by_default();
msg.icon = cover->get_icon();
msg.entity_category = static_cast<enums::EntityCategory>(cover->get_entity_category());
return this->send_list_entities_cover_response(msg);
return api->send_list_entities_cover_response(msg);
}
void APIConnection::cover_command(const CoverCommandRequest &msg) {
cover::Cover *cover = App.get_cover_by_key(msg.key);
@ -300,6 +366,19 @@ bool APIConnection::send_fan_state(fan::Fan *fan) {
if (!this->state_subscription_)
return false;
if (!APIConnection::try_send_fan_state(this, fan)) {
this->deferred_message_queue_.defer(fan, try_send_fan_state);
}
return true;
}
void APIConnection::send_fan_info(fan::Fan *fan) {
if (!APIConnection::try_send_fan_info(this, fan)) {
this->deferred_message_queue_.defer(fan, try_send_fan_info);
}
}
bool APIConnection::try_send_fan_state(APIConnection *api, void *v_fan) {
fan::Fan *fan = reinterpret_cast<fan::Fan *>(v_fan);
auto traits = fan->get_traits();
FanStateResponse resp{};
resp.key = fan->get_object_id_hash();
@ -313,9 +392,10 @@ bool APIConnection::send_fan_state(fan::Fan *fan) {
resp.direction = static_cast<enums::FanDirection>(fan->direction);
if (traits.supports_preset_modes())
resp.preset_mode = fan->preset_mode;
return this->send_fan_state_response(resp);
return api->send_fan_state_response(resp);
}
bool APIConnection::send_fan_info(fan::Fan *fan) {
bool APIConnection::try_send_fan_info(APIConnection *api, void *v_fan) {
fan::Fan *fan = reinterpret_cast<fan::Fan *>(v_fan);
auto traits = fan->get_traits();
ListEntitiesFanResponse msg;
msg.key = fan->get_object_id_hash();
@ -332,7 +412,7 @@ bool APIConnection::send_fan_info(fan::Fan *fan) {
msg.disabled_by_default = fan->is_disabled_by_default();
msg.icon = fan->get_icon();
msg.entity_category = static_cast<enums::EntityCategory>(fan->get_entity_category());
return this->send_list_entities_fan_response(msg);
return api->send_list_entities_fan_response(msg);
}
void APIConnection::fan_command(const FanCommandRequest &msg) {
fan::Fan *fan = App.get_fan_by_key(msg.key);
@ -361,6 +441,19 @@ bool APIConnection::send_light_state(light::LightState *light) {
if (!this->state_subscription_)
return false;
if (!APIConnection::try_send_light_state(this, light)) {
this->deferred_message_queue_.defer(light, try_send_light_state);
}
return true;
}
void APIConnection::send_light_info(light::LightState *light) {
if (!APIConnection::try_send_light_info(this, light)) {
this->deferred_message_queue_.defer(light, try_send_light_info);
}
}
bool APIConnection::try_send_light_state(APIConnection *api, void *v_light) {
light::LightState *light = reinterpret_cast<light::LightState *>(v_light);
auto traits = light->get_traits();
auto values = light->remote_values;
auto color_mode = values.get_color_mode();
@ -380,9 +473,10 @@ bool APIConnection::send_light_state(light::LightState *light) {
resp.warm_white = values.get_warm_white();
if (light->supports_effects())
resp.effect = light->get_effect_name();
return this->send_light_state_response(resp);
return api->send_light_state_response(resp);
}
bool APIConnection::send_light_info(light::LightState *light) {
bool APIConnection::try_send_light_info(APIConnection *api, void *v_light) {
light::LightState *light = reinterpret_cast<light::LightState *>(v_light);
auto traits = light->get_traits();
ListEntitiesLightResponse msg;
msg.key = light->get_object_id_hash();
@ -415,7 +509,7 @@ bool APIConnection::send_light_info(light::LightState *light) {
for (auto *effect : light->get_effects())
msg.effects.push_back(effect->get_name());
}
return this->send_list_entities_light_response(msg);
return api->send_list_entities_light_response(msg);
}
void APIConnection::light_command(const LightCommandRequest &msg) {
light::LightState *light = App.get_light_by_key(msg.key);
@ -459,13 +553,30 @@ bool APIConnection::send_sensor_state(sensor::Sensor *sensor, float state) {
if (!this->state_subscription_)
return false;
if (!APIConnection::try_send_sensor_state(this, sensor, state)) {
this->deferred_message_queue_.defer(sensor, try_send_sensor_state);
}
return true;
}
void APIConnection::send_sensor_info(sensor::Sensor *sensor) {
if (!APIConnection::try_send_sensor_info(this, sensor)) {
this->deferred_message_queue_.defer(sensor, try_send_sensor_info);
}
}
bool APIConnection::try_send_sensor_state(APIConnection *api, void *v_sensor) {
sensor::Sensor *sensor = reinterpret_cast<sensor::Sensor *>(v_sensor);
return APIConnection::try_send_sensor_state(api, sensor, sensor->state);
}
bool APIConnection::try_send_sensor_state(APIConnection *api, sensor::Sensor *sensor, float state) {
SensorStateResponse resp{};
resp.key = sensor->get_object_id_hash();
resp.state = state;
resp.missing_state = !sensor->has_state();
return this->send_sensor_state_response(resp);
return api->send_sensor_state_response(resp);
}
bool APIConnection::send_sensor_info(sensor::Sensor *sensor) {
bool APIConnection::try_send_sensor_info(APIConnection *api, void *v_sensor) {
sensor::Sensor *sensor = reinterpret_cast<sensor::Sensor *>(v_sensor);
ListEntitiesSensorResponse msg;
msg.key = sensor->get_object_id_hash();
msg.object_id = sensor->get_object_id();
@ -482,7 +593,7 @@ bool APIConnection::send_sensor_info(sensor::Sensor *sensor) {
msg.state_class = static_cast<enums::SensorStateClass>(sensor->get_state_class());
msg.disabled_by_default = sensor->is_disabled_by_default();
msg.entity_category = static_cast<enums::EntityCategory>(sensor->get_entity_category());
return this->send_list_entities_sensor_response(msg);
return api->send_list_entities_sensor_response(msg);
}
#endif
@ -491,12 +602,29 @@ bool APIConnection::send_switch_state(switch_::Switch *a_switch, bool state) {
if (!this->state_subscription_)
return false;
if (!APIConnection::try_send_switch_state(this, a_switch, state)) {
this->deferred_message_queue_.defer(a_switch, try_send_switch_state);
}
return true;
}
void APIConnection::send_switch_info(switch_::Switch *a_switch) {
if (!APIConnection::try_send_switch_info(this, a_switch)) {
this->deferred_message_queue_.defer(a_switch, try_send_switch_info);
}
}
bool APIConnection::try_send_switch_state(APIConnection *api, void *v_a_switch) {
switch_::Switch *a_switch = reinterpret_cast<switch_::Switch *>(v_a_switch);
return APIConnection::try_send_switch_state(api, a_switch, a_switch->state);
}
bool APIConnection::try_send_switch_state(APIConnection *api, switch_::Switch *a_switch, bool state) {
SwitchStateResponse resp{};
resp.key = a_switch->get_object_id_hash();
resp.state = state;
return this->send_switch_state_response(resp);
return api->send_switch_state_response(resp);
}
bool APIConnection::send_switch_info(switch_::Switch *a_switch) {
bool APIConnection::try_send_switch_info(APIConnection *api, void *v_a_switch) {
switch_::Switch *a_switch = reinterpret_cast<switch_::Switch *>(v_a_switch);
ListEntitiesSwitchResponse msg;
msg.key = a_switch->get_object_id_hash();
msg.object_id = a_switch->get_object_id();
@ -508,7 +636,7 @@ bool APIConnection::send_switch_info(switch_::Switch *a_switch) {
msg.disabled_by_default = a_switch->is_disabled_by_default();
msg.entity_category = static_cast<enums::EntityCategory>(a_switch->get_entity_category());
msg.device_class = a_switch->get_device_class();
return this->send_list_entities_switch_response(msg);
return api->send_list_entities_switch_response(msg);
}
void APIConnection::switch_command(const SwitchCommandRequest &msg) {
switch_::Switch *a_switch = App.get_switch_by_key(msg.key);
@ -528,13 +656,31 @@ bool APIConnection::send_text_sensor_state(text_sensor::TextSensor *text_sensor,
if (!this->state_subscription_)
return false;
if (!APIConnection::try_send_text_sensor_state(this, text_sensor, std::move(state))) {
this->deferred_message_queue_.defer(text_sensor, try_send_text_sensor_state);
}
return true;
}
void APIConnection::send_text_sensor_info(text_sensor::TextSensor *text_sensor) {
if (!APIConnection::try_send_text_sensor_info(this, text_sensor)) {
this->deferred_message_queue_.defer(text_sensor, try_send_text_sensor_info);
}
}
bool APIConnection::try_send_text_sensor_state(APIConnection *api, void *v_text_sensor) {
text_sensor::TextSensor *text_sensor = reinterpret_cast<text_sensor::TextSensor *>(v_text_sensor);
return APIConnection::try_send_text_sensor_state(api, text_sensor, text_sensor->state);
}
bool APIConnection::try_send_text_sensor_state(APIConnection *api, text_sensor::TextSensor *text_sensor,
std::string state) {
TextSensorStateResponse resp{};
resp.key = text_sensor->get_object_id_hash();
resp.state = std::move(state);
resp.missing_state = !text_sensor->has_state();
return this->send_text_sensor_state_response(resp);
return api->send_text_sensor_state_response(resp);
}
bool APIConnection::send_text_sensor_info(text_sensor::TextSensor *text_sensor) {
bool APIConnection::try_send_text_sensor_info(APIConnection *api, void *v_text_sensor) {
text_sensor::TextSensor *text_sensor = reinterpret_cast<text_sensor::TextSensor *>(v_text_sensor);
ListEntitiesTextSensorResponse msg;
msg.key = text_sensor->get_object_id_hash();
msg.object_id = text_sensor->get_object_id();
@ -546,7 +692,7 @@ bool APIConnection::send_text_sensor_info(text_sensor::TextSensor *text_sensor)
msg.disabled_by_default = text_sensor->is_disabled_by_default();
msg.entity_category = static_cast<enums::EntityCategory>(text_sensor->get_entity_category());
msg.device_class = text_sensor->get_device_class();
return this->send_list_entities_text_sensor_response(msg);
return api->send_list_entities_text_sensor_response(msg);
}
#endif
@ -555,6 +701,19 @@ bool APIConnection::send_climate_state(climate::Climate *climate) {
if (!this->state_subscription_)
return false;
if (!APIConnection::try_send_climate_state(this, climate)) {
this->deferred_message_queue_.defer(climate, try_send_climate_state);
}
return true;
}
void APIConnection::send_climate_info(climate::Climate *climate) {
if (!APIConnection::try_send_climate_info(this, climate)) {
this->deferred_message_queue_.defer(climate, try_send_climate_info);
}
}
bool APIConnection::try_send_climate_state(APIConnection *api, void *v_climate) {
climate::Climate *climate = reinterpret_cast<climate::Climate *>(v_climate);
auto traits = climate->get_traits();
ClimateStateResponse resp{};
resp.key = climate->get_object_id_hash();
@ -583,9 +742,10 @@ bool APIConnection::send_climate_state(climate::Climate *climate) {
resp.current_humidity = climate->current_humidity;
if (traits.get_supports_target_humidity())
resp.target_humidity = climate->target_humidity;
return this->send_climate_state_response(resp);
return api->send_climate_state_response(resp);
}
bool APIConnection::send_climate_info(climate::Climate *climate) {
bool APIConnection::try_send_climate_info(APIConnection *api, void *v_climate) {
climate::Climate *climate = reinterpret_cast<climate::Climate *>(v_climate);
auto traits = climate->get_traits();
ListEntitiesClimateResponse msg;
msg.key = climate->get_object_id_hash();
@ -626,7 +786,7 @@ bool APIConnection::send_climate_info(climate::Climate *climate) {
msg.supported_custom_presets.push_back(custom_preset);
for (auto swing_mode : traits.get_supported_swing_modes())
msg.supported_swing_modes.push_back(static_cast<enums::ClimateSwingMode>(swing_mode));
return this->send_list_entities_climate_response(msg);
return api->send_list_entities_climate_response(msg);
}
void APIConnection::climate_command(const ClimateCommandRequest &msg) {
climate::Climate *climate = App.get_climate_by_key(msg.key);
@ -663,13 +823,30 @@ bool APIConnection::send_number_state(number::Number *number, float state) {
if (!this->state_subscription_)
return false;
if (!APIConnection::try_send_number_state(this, number, state)) {
this->deferred_message_queue_.defer(number, try_send_number_state);
}
return true;
}
void APIConnection::send_number_info(number::Number *number) {
if (!APIConnection::try_send_number_info(this, number)) {
this->deferred_message_queue_.defer(number, try_send_number_info);
}
}
bool APIConnection::try_send_number_state(APIConnection *api, void *v_number) {
number::Number *number = reinterpret_cast<number::Number *>(v_number);
return APIConnection::try_send_number_state(api, number, number->state);
}
bool APIConnection::try_send_number_state(APIConnection *api, number::Number *number, float state) {
NumberStateResponse resp{};
resp.key = number->get_object_id_hash();
resp.state = state;
resp.missing_state = !number->has_state();
return this->send_number_state_response(resp);
return api->send_number_state_response(resp);
}
bool APIConnection::send_number_info(number::Number *number) {
bool APIConnection::try_send_number_info(APIConnection *api, void *v_number) {
number::Number *number = reinterpret_cast<number::Number *>(v_number);
ListEntitiesNumberResponse msg;
msg.key = number->get_object_id_hash();
msg.object_id = number->get_object_id();
@ -687,7 +864,7 @@ bool APIConnection::send_number_info(number::Number *number) {
msg.max_value = number->traits.get_max_value();
msg.step = number->traits.get_step();
return this->send_list_entities_number_response(msg);
return api->send_list_entities_number_response(msg);
}
void APIConnection::number_command(const NumberCommandRequest &msg) {
number::Number *number = App.get_number_by_key(msg.key);
@ -705,15 +882,29 @@ bool APIConnection::send_date_state(datetime::DateEntity *date) {
if (!this->state_subscription_)
return false;
if (!APIConnection::try_send_date_state(this, date)) {
this->deferred_message_queue_.defer(date, try_send_date_state);
}
return true;
}
void APIConnection::send_date_info(datetime::DateEntity *date) {
if (!APIConnection::try_send_date_info(this, date)) {
this->deferred_message_queue_.defer(date, try_send_date_info);
}
}
bool APIConnection::try_send_date_state(APIConnection *api, void *v_date) {
datetime::DateEntity *date = reinterpret_cast<datetime::DateEntity *>(v_date);
DateStateResponse resp{};
resp.key = date->get_object_id_hash();
resp.missing_state = !date->has_state();
resp.year = date->year;
resp.month = date->month;
resp.day = date->day;
return this->send_date_state_response(resp);
return api->send_date_state_response(resp);
}
bool APIConnection::send_date_info(datetime::DateEntity *date) {
bool APIConnection::try_send_date_info(APIConnection *api, void *v_date) {
datetime::DateEntity *date = reinterpret_cast<datetime::DateEntity *>(v_date);
ListEntitiesDateResponse msg;
msg.key = date->get_object_id_hash();
msg.object_id = date->get_object_id();
@ -724,7 +915,7 @@ bool APIConnection::send_date_info(datetime::DateEntity *date) {
msg.disabled_by_default = date->is_disabled_by_default();
msg.entity_category = static_cast<enums::EntityCategory>(date->get_entity_category());
return this->send_list_entities_date_response(msg);
return api->send_list_entities_date_response(msg);
}
void APIConnection::date_command(const DateCommandRequest &msg) {
datetime::DateEntity *date = App.get_date_by_key(msg.key);
@ -742,15 +933,29 @@ bool APIConnection::send_time_state(datetime::TimeEntity *time) {
if (!this->state_subscription_)
return false;
if (!APIConnection::try_send_time_state(this, time)) {
this->deferred_message_queue_.defer(time, try_send_time_state);
}
return true;
}
void APIConnection::send_time_info(datetime::TimeEntity *time) {
if (!APIConnection::try_send_time_info(this, time)) {
this->deferred_message_queue_.defer(time, try_send_time_info);
}
}
bool APIConnection::try_send_time_state(APIConnection *api, void *v_time) {
datetime::TimeEntity *time = reinterpret_cast<datetime::TimeEntity *>(v_time);
TimeStateResponse resp{};
resp.key = time->get_object_id_hash();
resp.missing_state = !time->has_state();
resp.hour = time->hour;
resp.minute = time->minute;
resp.second = time->second;
return this->send_time_state_response(resp);
return api->send_time_state_response(resp);
}
bool APIConnection::send_time_info(datetime::TimeEntity *time) {
bool APIConnection::try_send_time_info(APIConnection *api, void *v_time) {
datetime::TimeEntity *time = reinterpret_cast<datetime::TimeEntity *>(v_time);
ListEntitiesTimeResponse msg;
msg.key = time->get_object_id_hash();
msg.object_id = time->get_object_id();
@ -761,7 +966,7 @@ bool APIConnection::send_time_info(datetime::TimeEntity *time) {
msg.disabled_by_default = time->is_disabled_by_default();
msg.entity_category = static_cast<enums::EntityCategory>(time->get_entity_category());
return this->send_list_entities_time_response(msg);
return api->send_list_entities_time_response(msg);
}
void APIConnection::time_command(const TimeCommandRequest &msg) {
datetime::TimeEntity *time = App.get_time_by_key(msg.key);
@ -779,6 +984,19 @@ bool APIConnection::send_datetime_state(datetime::DateTimeEntity *datetime) {
if (!this->state_subscription_)
return false;
if (!APIConnection::try_send_datetime_state(this, datetime)) {
this->deferred_message_queue_.defer(datetime, try_send_datetime_state);
}
return true;
}
void APIConnection::send_datetime_info(datetime::DateTimeEntity *datetime) {
if (!APIConnection::try_send_datetime_info(this, datetime)) {
this->deferred_message_queue_.defer(datetime, try_send_datetime_info);
}
}
bool APIConnection::try_send_datetime_state(APIConnection *api, void *v_datetime) {
datetime::DateTimeEntity *datetime = reinterpret_cast<datetime::DateTimeEntity *>(v_datetime);
DateTimeStateResponse resp{};
resp.key = datetime->get_object_id_hash();
resp.missing_state = !datetime->has_state();
@ -786,9 +1004,10 @@ bool APIConnection::send_datetime_state(datetime::DateTimeEntity *datetime) {
ESPTime state = datetime->state_as_esptime();
resp.epoch_seconds = state.timestamp;
}
return this->send_date_time_state_response(resp);
return api->send_date_time_state_response(resp);
}
bool APIConnection::send_datetime_info(datetime::DateTimeEntity *datetime) {
bool APIConnection::try_send_datetime_info(APIConnection *api, void *v_datetime) {
datetime::DateTimeEntity *datetime = reinterpret_cast<datetime::DateTimeEntity *>(v_datetime);
ListEntitiesDateTimeResponse msg;
msg.key = datetime->get_object_id_hash();
msg.object_id = datetime->get_object_id();
@ -799,7 +1018,7 @@ bool APIConnection::send_datetime_info(datetime::DateTimeEntity *datetime) {
msg.disabled_by_default = datetime->is_disabled_by_default();
msg.entity_category = static_cast<enums::EntityCategory>(datetime->get_entity_category());
return this->send_list_entities_date_time_response(msg);
return api->send_list_entities_date_time_response(msg);
}
void APIConnection::datetime_command(const DateTimeCommandRequest &msg) {
datetime::DateTimeEntity *datetime = App.get_datetime_by_key(msg.key);
@ -817,13 +1036,30 @@ bool APIConnection::send_text_state(text::Text *text, std::string state) {
if (!this->state_subscription_)
return false;
if (!APIConnection::try_send_text_state(this, text, std::move(state))) {
this->deferred_message_queue_.defer(text, try_send_text_state);
}
return true;
}
void APIConnection::send_text_info(text::Text *text) {
if (!APIConnection::try_send_text_info(this, text)) {
this->deferred_message_queue_.defer(text, try_send_text_info);
}
}
bool APIConnection::try_send_text_state(APIConnection *api, void *v_text) {
text::Text *text = reinterpret_cast<text::Text *>(v_text);
return APIConnection::try_send_text_state(api, text, text->state);
}
bool APIConnection::try_send_text_state(APIConnection *api, text::Text *text, std::string state) {
TextStateResponse resp{};
resp.key = text->get_object_id_hash();
resp.state = std::move(state);
resp.missing_state = !text->has_state();
return this->send_text_state_response(resp);
return api->send_text_state_response(resp);
}
bool APIConnection::send_text_info(text::Text *text) {
bool APIConnection::try_send_text_info(APIConnection *api, void *v_text) {
text::Text *text = reinterpret_cast<text::Text *>(v_text);
ListEntitiesTextResponse msg;
msg.key = text->get_object_id_hash();
msg.object_id = text->get_object_id();
@ -837,7 +1073,7 @@ bool APIConnection::send_text_info(text::Text *text) {
msg.max_length = text->traits.get_max_length();
msg.pattern = text->traits.get_pattern();
return this->send_list_entities_text_response(msg);
return api->send_list_entities_text_response(msg);
}
void APIConnection::text_command(const TextCommandRequest &msg) {
text::Text *text = App.get_text_by_key(msg.key);
@ -855,13 +1091,30 @@ bool APIConnection::send_select_state(select::Select *select, std::string state)
if (!this->state_subscription_)
return false;
if (!APIConnection::try_send_select_state(this, select, std::move(state))) {
this->deferred_message_queue_.defer(select, try_send_select_state);
}
return true;
}
void APIConnection::send_select_info(select::Select *select) {
if (!APIConnection::try_send_select_info(this, select)) {
this->deferred_message_queue_.defer(select, try_send_select_info);
}
}
bool APIConnection::try_send_select_state(APIConnection *api, void *v_select) {
select::Select *select = reinterpret_cast<select::Select *>(v_select);
return APIConnection::try_send_select_state(api, select, select->state);
}
bool APIConnection::try_send_select_state(APIConnection *api, select::Select *select, std::string state) {
SelectStateResponse resp{};
resp.key = select->get_object_id_hash();
resp.state = std::move(state);
resp.missing_state = !select->has_state();
return this->send_select_state_response(resp);
return api->send_select_state_response(resp);
}
bool APIConnection::send_select_info(select::Select *select) {
bool APIConnection::try_send_select_info(APIConnection *api, void *v_select) {
select::Select *select = reinterpret_cast<select::Select *>(v_select);
ListEntitiesSelectResponse msg;
msg.key = select->get_object_id_hash();
msg.object_id = select->get_object_id();
@ -875,7 +1128,7 @@ bool APIConnection::send_select_info(select::Select *select) {
for (const auto &option : select->traits.get_options())
msg.options.push_back(option);
return this->send_list_entities_select_response(msg);
return api->send_list_entities_select_response(msg);
}
void APIConnection::select_command(const SelectCommandRequest &msg) {
select::Select *select = App.get_select_by_key(msg.key);
@ -889,7 +1142,13 @@ void APIConnection::select_command(const SelectCommandRequest &msg) {
#endif
#ifdef USE_BUTTON
bool APIConnection::send_button_info(button::Button *button) {
void APIConnection::send_button_info(button::Button *button) {
if (!APIConnection::try_send_button_info(this, button)) {
this->deferred_message_queue_.defer(button, try_send_button_info);
}
}
bool APIConnection::try_send_button_info(APIConnection *api, void *v_button) {
button::Button *button = reinterpret_cast<button::Button *>(v_button);
ListEntitiesButtonResponse msg;
msg.key = button->get_object_id_hash();
msg.object_id = button->get_object_id();
@ -900,7 +1159,7 @@ bool APIConnection::send_button_info(button::Button *button) {
msg.disabled_by_default = button->is_disabled_by_default();
msg.entity_category = static_cast<enums::EntityCategory>(button->get_entity_category());
msg.device_class = button->get_device_class();
return this->send_list_entities_button_response(msg);
return api->send_list_entities_button_response(msg);
}
void APIConnection::button_command(const ButtonCommandRequest &msg) {
button::Button *button = App.get_button_by_key(msg.key);
@ -916,12 +1175,29 @@ bool APIConnection::send_lock_state(lock::Lock *a_lock, lock::LockState state) {
if (!this->state_subscription_)
return false;
if (!APIConnection::try_send_lock_state(this, a_lock, state)) {
this->deferred_message_queue_.defer(a_lock, try_send_lock_state);
}
return true;
}
void APIConnection::send_lock_info(lock::Lock *a_lock) {
if (!APIConnection::try_send_lock_info(this, a_lock)) {
this->deferred_message_queue_.defer(a_lock, try_send_lock_info);
}
}
bool APIConnection::try_send_lock_state(APIConnection *api, void *v_a_lock) {
lock::Lock *a_lock = reinterpret_cast<lock::Lock *>(v_a_lock);
return APIConnection::try_send_lock_state(api, a_lock, a_lock->state);
}
bool APIConnection::try_send_lock_state(APIConnection *api, lock::Lock *a_lock, lock::LockState state) {
LockStateResponse resp{};
resp.key = a_lock->get_object_id_hash();
resp.state = static_cast<enums::LockState>(state);
return this->send_lock_state_response(resp);
return api->send_lock_state_response(resp);
}
bool APIConnection::send_lock_info(lock::Lock *a_lock) {
bool APIConnection::try_send_lock_info(APIConnection *api, void *v_a_lock) {
lock::Lock *a_lock = reinterpret_cast<lock::Lock *>(v_a_lock);
ListEntitiesLockResponse msg;
msg.key = a_lock->get_object_id_hash();
msg.object_id = a_lock->get_object_id();
@ -934,7 +1210,7 @@ bool APIConnection::send_lock_info(lock::Lock *a_lock) {
msg.entity_category = static_cast<enums::EntityCategory>(a_lock->get_entity_category());
msg.supports_open = a_lock->traits.get_supports_open();
msg.requires_code = a_lock->traits.get_requires_code();
return this->send_list_entities_lock_response(msg);
return api->send_list_entities_lock_response(msg);
}
void APIConnection::lock_command(const LockCommandRequest &msg) {
lock::Lock *a_lock = App.get_lock_by_key(msg.key);
@ -960,13 +1236,27 @@ bool APIConnection::send_valve_state(valve::Valve *valve) {
if (!this->state_subscription_)
return false;
if (!APIConnection::try_send_valve_state(this, valve)) {
this->deferred_message_queue_.defer(valve, try_send_valve_state);
}
return true;
}
void APIConnection::send_valve_info(valve::Valve *valve) {
if (!APIConnection::try_send_valve_info(this, valve)) {
this->deferred_message_queue_.defer(valve, try_send_valve_info);
}
}
bool APIConnection::try_send_valve_state(APIConnection *api, void *v_valve) {
valve::Valve *valve = reinterpret_cast<valve::Valve *>(v_valve);
ValveStateResponse resp{};
resp.key = valve->get_object_id_hash();
resp.position = valve->position;
resp.current_operation = static_cast<enums::ValveOperation>(valve->current_operation);
return this->send_valve_state_response(resp);
return api->send_valve_state_response(resp);
}
bool APIConnection::send_valve_info(valve::Valve *valve) {
bool APIConnection::try_send_valve_info(APIConnection *api, void *v_valve) {
valve::Valve *valve = reinterpret_cast<valve::Valve *>(v_valve);
auto traits = valve->get_traits();
ListEntitiesValveResponse msg;
msg.key = valve->get_object_id_hash();
@ -981,7 +1271,7 @@ bool APIConnection::send_valve_info(valve::Valve *valve) {
msg.assumed_state = traits.get_is_assumed_state();
msg.supports_position = traits.get_supports_position();
msg.supports_stop = traits.get_supports_stop();
return this->send_list_entities_valve_response(msg);
return api->send_list_entities_valve_response(msg);
}
void APIConnection::valve_command(const ValveCommandRequest &msg) {
valve::Valve *valve = App.get_valve_by_key(msg.key);
@ -1002,6 +1292,19 @@ bool APIConnection::send_media_player_state(media_player::MediaPlayer *media_pla
if (!this->state_subscription_)
return false;
if (!APIConnection::try_send_media_player_state(this, media_player)) {
this->deferred_message_queue_.defer(media_player, try_send_media_player_state);
}
return true;
}
void APIConnection::send_media_player_info(media_player::MediaPlayer *media_player) {
if (!APIConnection::try_send_media_player_info(this, media_player)) {
this->deferred_message_queue_.defer(media_player, try_send_media_player_info);
}
}
bool APIConnection::try_send_media_player_state(APIConnection *api, void *v_media_player) {
media_player::MediaPlayer *media_player = reinterpret_cast<media_player::MediaPlayer *>(v_media_player);
MediaPlayerStateResponse resp{};
resp.key = media_player->get_object_id_hash();
@ -1011,9 +1314,10 @@ bool APIConnection::send_media_player_state(media_player::MediaPlayer *media_pla
resp.state = static_cast<enums::MediaPlayerState>(report_state);
resp.volume = media_player->volume;
resp.muted = media_player->is_muted();
return this->send_media_player_state_response(resp);
return api->send_media_player_state_response(resp);
}
bool APIConnection::send_media_player_info(media_player::MediaPlayer *media_player) {
bool APIConnection::try_send_media_player_info(APIConnection *api, void *v_media_player) {
media_player::MediaPlayer *media_player = reinterpret_cast<media_player::MediaPlayer *>(v_media_player);
ListEntitiesMediaPlayerResponse msg;
msg.key = media_player->get_object_id_hash();
msg.object_id = media_player->get_object_id();
@ -1037,7 +1341,7 @@ bool APIConnection::send_media_player_info(media_player::MediaPlayer *media_play
msg.supported_formats.push_back(media_format);
}
return this->send_list_entities_media_player_response(msg);
return api->send_list_entities_media_player_response(msg);
}
void APIConnection::media_player_command(const MediaPlayerCommandRequest &msg) {
media_player::MediaPlayer *media_player = App.get_media_player_by_key(msg.key);
@ -1062,7 +1366,7 @@ void APIConnection::media_player_command(const MediaPlayerCommandRequest &msg) {
#endif
#ifdef USE_ESP32_CAMERA
void APIConnection::send_camera_state(std::shared_ptr<esp32_camera::CameraImage> image) {
void APIConnection::set_camera_state(std::shared_ptr<esp32_camera::CameraImage> image) {
if (!this->state_subscription_)
return;
if (this->image_reader_.available())
@ -1071,7 +1375,13 @@ void APIConnection::send_camera_state(std::shared_ptr<esp32_camera::CameraImage>
image->was_requested_by(esphome::esp32_camera::IDLE))
this->image_reader_.set_image(std::move(image));
}
bool APIConnection::send_camera_info(esp32_camera::ESP32Camera *camera) {
void APIConnection::send_camera_info(esp32_camera::ESP32Camera *camera) {
if (!APIConnection::try_send_camera_info(this, camera)) {
this->deferred_message_queue_.defer(camera, try_send_camera_info);
}
}
bool APIConnection::try_send_camera_info(APIConnection *api, void *v_camera) {
esp32_camera::ESP32Camera *camera = reinterpret_cast<esp32_camera::ESP32Camera *>(v_camera);
ListEntitiesCameraResponse msg;
msg.key = camera->get_object_id_hash();
msg.object_id = camera->get_object_id();
@ -1081,7 +1391,7 @@ bool APIConnection::send_camera_info(esp32_camera::ESP32Camera *camera) {
msg.disabled_by_default = camera->is_disabled_by_default();
msg.icon = camera->get_icon();
msg.entity_category = static_cast<enums::EntityCategory>(camera->get_entity_category());
return this->send_list_entities_camera_response(msg);
return api->send_list_entities_camera_response(msg);
}
void APIConnection::camera_image(const CameraImageRequest &msg) {
if (esp32_camera::global_esp32_camera == nullptr)
@ -1268,12 +1578,28 @@ bool APIConnection::send_alarm_control_panel_state(alarm_control_panel::AlarmCon
if (!this->state_subscription_)
return false;
if (!APIConnection::try_send_alarm_control_panel_state(this, a_alarm_control_panel)) {
this->deferred_message_queue_.defer(a_alarm_control_panel, try_send_alarm_control_panel_state);
}
return true;
}
void APIConnection::send_alarm_control_panel_info(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) {
if (!APIConnection::try_send_alarm_control_panel_info(this, a_alarm_control_panel)) {
this->deferred_message_queue_.defer(a_alarm_control_panel, try_send_alarm_control_panel_info);
}
}
bool APIConnection::try_send_alarm_control_panel_state(APIConnection *api, void *v_a_alarm_control_panel) {
alarm_control_panel::AlarmControlPanel *a_alarm_control_panel =
reinterpret_cast<alarm_control_panel::AlarmControlPanel *>(v_a_alarm_control_panel);
AlarmControlPanelStateResponse resp{};
resp.key = a_alarm_control_panel->get_object_id_hash();
resp.state = static_cast<enums::AlarmControlPanelState>(a_alarm_control_panel->get_state());
return this->send_alarm_control_panel_state_response(resp);
return api->send_alarm_control_panel_state_response(resp);
}
bool APIConnection::send_alarm_control_panel_info(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) {
bool APIConnection::try_send_alarm_control_panel_info(APIConnection *api, void *v_a_alarm_control_panel) {
alarm_control_panel::AlarmControlPanel *a_alarm_control_panel =
reinterpret_cast<alarm_control_panel::AlarmControlPanel *>(v_a_alarm_control_panel);
ListEntitiesAlarmControlPanelResponse msg;
msg.key = a_alarm_control_panel->get_object_id_hash();
msg.object_id = a_alarm_control_panel->get_object_id();
@ -1285,7 +1611,7 @@ bool APIConnection::send_alarm_control_panel_info(alarm_control_panel::AlarmCont
msg.supported_features = a_alarm_control_panel->get_supported_features();
msg.requires_code = a_alarm_control_panel->get_requires_code();
msg.requires_code_to_arm = a_alarm_control_panel->get_requires_code_to_arm();
return this->send_list_entities_alarm_control_panel_response(msg);
return api->send_list_entities_alarm_control_panel_response(msg);
}
void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) {
alarm_control_panel::AlarmControlPanel *a_alarm_control_panel = App.get_alarm_control_panel_by_key(msg.key);
@ -1322,13 +1648,28 @@ void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRe
#endif
#ifdef USE_EVENT
bool APIConnection::send_event(event::Event *event, std::string event_type) {
void APIConnection::send_event(event::Event *event, std::string event_type) {
if (!APIConnection::try_send_event(this, event, std::move(event_type))) {
this->deferred_message_queue_.defer(event, try_send_event);
}
}
void APIConnection::send_event_info(event::Event *event) {
if (!APIConnection::try_send_event_info(this, event)) {
this->deferred_message_queue_.defer(event, try_send_event_info);
}
}
bool APIConnection::try_send_event(APIConnection *api, void *v_event) {
event::Event *event = reinterpret_cast<event::Event *>(v_event);
return APIConnection::try_send_event(api, event, *(event->last_event_type));
}
bool APIConnection::try_send_event(APIConnection *api, event::Event *event, std::string event_type) {
EventResponse resp{};
resp.key = event->get_object_id_hash();
resp.event_type = std::move(event_type);
return this->send_event_response(resp);
return api->send_event_response(resp);
}
bool APIConnection::send_event_info(event::Event *event) {
bool APIConnection::try_send_event_info(APIConnection *api, void *v_event) {
event::Event *event = reinterpret_cast<event::Event *>(v_event);
ListEntitiesEventResponse msg;
msg.key = event->get_object_id_hash();
msg.object_id = event->get_object_id();
@ -1341,7 +1682,7 @@ bool APIConnection::send_event_info(event::Event *event) {
msg.device_class = event->get_device_class();
for (const auto &event_type : event->get_event_types())
msg.event_types.push_back(event_type);
return this->send_list_entities_event_response(msg);
return api->send_list_entities_event_response(msg);
}
#endif
@ -1350,6 +1691,19 @@ bool APIConnection::send_update_state(update::UpdateEntity *update) {
if (!this->state_subscription_)
return false;
if (!APIConnection::try_send_update_state(this, update)) {
this->deferred_message_queue_.defer(update, try_send_update_state);
}
return true;
}
void APIConnection::send_update_info(update::UpdateEntity *update) {
if (!APIConnection::try_send_update_info(this, update)) {
this->deferred_message_queue_.defer(update, try_send_update_info);
}
}
bool APIConnection::try_send_update_state(APIConnection *api, void *v_update) {
update::UpdateEntity *update = reinterpret_cast<update::UpdateEntity *>(v_update);
UpdateStateResponse resp{};
resp.key = update->get_object_id_hash();
resp.missing_state = !update->has_state();
@ -1366,9 +1720,10 @@ bool APIConnection::send_update_state(update::UpdateEntity *update) {
resp.release_url = update->update_info.release_url;
}
return this->send_update_state_response(resp);
return api->send_update_state_response(resp);
}
bool APIConnection::send_update_info(update::UpdateEntity *update) {
bool APIConnection::try_send_update_info(APIConnection *api, void *v_update) {
update::UpdateEntity *update = reinterpret_cast<update::UpdateEntity *>(v_update);
ListEntitiesUpdateResponse msg;
msg.key = update->get_object_id_hash();
msg.object_id = update->get_object_id();
@ -1379,7 +1734,7 @@ bool APIConnection::send_update_info(update::UpdateEntity *update) {
msg.disabled_by_default = update->is_disabled_by_default();
msg.entity_category = static_cast<enums::EntityCategory>(update->get_entity_category());
msg.device_class = update->get_device_class();
return this->send_list_entities_update_response(msg);
return api->send_list_entities_update_response(msg);
}
void APIConnection::update_command(const UpdateCommandRequest &msg) {
update::UpdateEntity *update = App.get_update_by_key(msg.key);
@ -1403,7 +1758,7 @@ void APIConnection::update_command(const UpdateCommandRequest &msg) {
}
#endif
bool APIConnection::send_log_message(int level, const char *tag, const char *line) {
bool APIConnection::try_send_log_message(int level, const char *tag, const char *line) {
if (this->log_subscription_ < level)
return false;
@ -1488,6 +1843,7 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) {
#ifdef USE_BLUETOOTH_PROXY
resp.legacy_bluetooth_proxy_version = bluetooth_proxy::global_bluetooth_proxy->get_legacy_version();
resp.bluetooth_proxy_feature_flags = bluetooth_proxy::global_bluetooth_proxy->get_feature_flags();
resp.bluetooth_mac_address = bluetooth_proxy::global_bluetooth_proxy->get_bluetooth_mac_address_pretty();
#endif
#ifdef USE_VOICE_ASSISTANT
resp.legacy_voice_assistant_version = voice_assistant::global_voice_assistant->get_legacy_version();

View File

@ -14,6 +14,46 @@
namespace esphome {
namespace api {
using send_message_t = bool(APIConnection *, void *);
/*
This class holds a pointer to the source component that wants to publish a message, and a pointer to a function that
will lazily publish that message. The two pointers allow dedup in the deferred queue if multiple publishes for the
same component are backed up, and take up only 8 bytes of memory. The entry in the deferred queue (a std::vector) is
the DeferredMessage instance itself (not a pointer to one elsewhere in heap) so still only 8 bytes per entry. Even
100 backed up messages (you'd have to have at least 100 sensors publishing because of dedup) would take up only 0.8
kB.
*/
class DeferredMessageQueue {
struct DeferredMessage {
friend class DeferredMessageQueue;
protected:
void *source_;
send_message_t *send_message_;
public:
DeferredMessage(void *source, send_message_t *send_message) : source_(source), send_message_(send_message) {}
bool operator==(const DeferredMessage &test) const {
return (source_ == test.source_ && send_message_ == test.send_message_);
}
} __attribute__((packed));
protected:
// vector is used very specifically for its zero memory overhead even though items are popped from the front (memory
// footprint is more important than speed here)
std::vector<DeferredMessage> deferred_queue_;
APIConnection *api_connection_;
// helper for allowing only unique entries in the queue
void dmq_push_back_with_dedup_(void *source, send_message_t *send_message);
public:
DeferredMessageQueue(APIConnection *api_connection) : api_connection_(api_connection) {}
void process_queue();
void defer(void *source, send_message_t *send_message);
};
class APIConnection : public APIServerConnection {
public:
APIConnection(std::unique_ptr<socket::Socket> socket, APIServer *parent);
@ -28,96 +68,140 @@ class APIConnection : public APIServerConnection {
}
#ifdef USE_BINARY_SENSOR
bool send_binary_sensor_state(binary_sensor::BinarySensor *binary_sensor, bool state);
bool send_binary_sensor_info(binary_sensor::BinarySensor *binary_sensor);
void send_binary_sensor_info(binary_sensor::BinarySensor *binary_sensor);
static bool try_send_binary_sensor_state(APIConnection *api, void *v_binary_sensor);
static bool try_send_binary_sensor_state(APIConnection *api, binary_sensor::BinarySensor *binary_sensor, bool state);
static bool try_send_binary_sensor_info(APIConnection *api, void *v_binary_sensor);
#endif
#ifdef USE_COVER
bool send_cover_state(cover::Cover *cover);
bool send_cover_info(cover::Cover *cover);
void send_cover_info(cover::Cover *cover);
static bool try_send_cover_state(APIConnection *api, void *v_cover);
static bool try_send_cover_info(APIConnection *api, void *v_cover);
void cover_command(const CoverCommandRequest &msg) override;
#endif
#ifdef USE_FAN
bool send_fan_state(fan::Fan *fan);
bool send_fan_info(fan::Fan *fan);
void send_fan_info(fan::Fan *fan);
static bool try_send_fan_state(APIConnection *api, void *v_fan);
static bool try_send_fan_info(APIConnection *api, void *v_fan);
void fan_command(const FanCommandRequest &msg) override;
#endif
#ifdef USE_LIGHT
bool send_light_state(light::LightState *light);
bool send_light_info(light::LightState *light);
void send_light_info(light::LightState *light);
static bool try_send_light_state(APIConnection *api, void *v_light);
static bool try_send_light_info(APIConnection *api, void *v_light);
void light_command(const LightCommandRequest &msg) override;
#endif
#ifdef USE_SENSOR
bool send_sensor_state(sensor::Sensor *sensor, float state);
bool send_sensor_info(sensor::Sensor *sensor);
void send_sensor_info(sensor::Sensor *sensor);
static bool try_send_sensor_state(APIConnection *api, void *v_sensor);
static bool try_send_sensor_state(APIConnection *api, sensor::Sensor *sensor, float state);
static bool try_send_sensor_info(APIConnection *api, void *v_sensor);
#endif
#ifdef USE_SWITCH
bool send_switch_state(switch_::Switch *a_switch, bool state);
bool send_switch_info(switch_::Switch *a_switch);
void send_switch_info(switch_::Switch *a_switch);
static bool try_send_switch_state(APIConnection *api, void *v_a_switch);
static bool try_send_switch_state(APIConnection *api, switch_::Switch *a_switch, bool state);
static bool try_send_switch_info(APIConnection *api, void *v_a_switch);
void switch_command(const SwitchCommandRequest &msg) override;
#endif
#ifdef USE_TEXT_SENSOR
bool send_text_sensor_state(text_sensor::TextSensor *text_sensor, std::string state);
bool send_text_sensor_info(text_sensor::TextSensor *text_sensor);
void send_text_sensor_info(text_sensor::TextSensor *text_sensor);
static bool try_send_text_sensor_state(APIConnection *api, void *v_text_sensor);
static bool try_send_text_sensor_state(APIConnection *api, text_sensor::TextSensor *text_sensor, std::string state);
static bool try_send_text_sensor_info(APIConnection *api, void *v_text_sensor);
#endif
#ifdef USE_ESP32_CAMERA
void send_camera_state(std::shared_ptr<esp32_camera::CameraImage> image);
bool send_camera_info(esp32_camera::ESP32Camera *camera);
void set_camera_state(std::shared_ptr<esp32_camera::CameraImage> image);
void send_camera_info(esp32_camera::ESP32Camera *camera);
static bool try_send_camera_info(APIConnection *api, void *v_camera);
void camera_image(const CameraImageRequest &msg) override;
#endif
#ifdef USE_CLIMATE
bool send_climate_state(climate::Climate *climate);
bool send_climate_info(climate::Climate *climate);
void send_climate_info(climate::Climate *climate);
static bool try_send_climate_state(APIConnection *api, void *v_climate);
static bool try_send_climate_info(APIConnection *api, void *v_climate);
void climate_command(const ClimateCommandRequest &msg) override;
#endif
#ifdef USE_NUMBER
bool send_number_state(number::Number *number, float state);
bool send_number_info(number::Number *number);
void send_number_info(number::Number *number);
static bool try_send_number_state(APIConnection *api, void *v_number);
static bool try_send_number_state(APIConnection *api, number::Number *number, float state);
static bool try_send_number_info(APIConnection *api, void *v_number);
void number_command(const NumberCommandRequest &msg) override;
#endif
#ifdef USE_DATETIME_DATE
bool send_date_state(datetime::DateEntity *date);
bool send_date_info(datetime::DateEntity *date);
void send_date_info(datetime::DateEntity *date);
static bool try_send_date_state(APIConnection *api, void *v_date);
static bool try_send_date_info(APIConnection *api, void *v_date);
void date_command(const DateCommandRequest &msg) override;
#endif
#ifdef USE_DATETIME_TIME
bool send_time_state(datetime::TimeEntity *time);
bool send_time_info(datetime::TimeEntity *time);
void send_time_info(datetime::TimeEntity *time);
static bool try_send_time_state(APIConnection *api, void *v_time);
static bool try_send_time_info(APIConnection *api, void *v_time);
void time_command(const TimeCommandRequest &msg) override;
#endif
#ifdef USE_DATETIME_DATETIME
bool send_datetime_state(datetime::DateTimeEntity *datetime);
bool send_datetime_info(datetime::DateTimeEntity *datetime);
void send_datetime_info(datetime::DateTimeEntity *datetime);
static bool try_send_datetime_state(APIConnection *api, void *v_datetime);
static bool try_send_datetime_info(APIConnection *api, void *v_datetime);
void datetime_command(const DateTimeCommandRequest &msg) override;
#endif
#ifdef USE_TEXT
bool send_text_state(text::Text *text, std::string state);
bool send_text_info(text::Text *text);
void send_text_info(text::Text *text);
static bool try_send_text_state(APIConnection *api, void *v_text);
static bool try_send_text_state(APIConnection *api, text::Text *text, std::string state);
static bool try_send_text_info(APIConnection *api, void *v_text);
void text_command(const TextCommandRequest &msg) override;
#endif
#ifdef USE_SELECT
bool send_select_state(select::Select *select, std::string state);
bool send_select_info(select::Select *select);
void send_select_info(select::Select *select);
static bool try_send_select_state(APIConnection *api, void *v_select);
static bool try_send_select_state(APIConnection *api, select::Select *select, std::string state);
static bool try_send_select_info(APIConnection *api, void *v_select);
void select_command(const SelectCommandRequest &msg) override;
#endif
#ifdef USE_BUTTON
bool send_button_info(button::Button *button);
void send_button_info(button::Button *button);
static bool try_send_button_info(APIConnection *api, void *v_button);
void button_command(const ButtonCommandRequest &msg) override;
#endif
#ifdef USE_LOCK
bool send_lock_state(lock::Lock *a_lock, lock::LockState state);
bool send_lock_info(lock::Lock *a_lock);
void send_lock_info(lock::Lock *a_lock);
static bool try_send_lock_state(APIConnection *api, void *v_a_lock);
static bool try_send_lock_state(APIConnection *api, lock::Lock *a_lock, lock::LockState state);
static bool try_send_lock_info(APIConnection *api, void *v_a_lock);
void lock_command(const LockCommandRequest &msg) override;
#endif
#ifdef USE_VALVE
bool send_valve_state(valve::Valve *valve);
bool send_valve_info(valve::Valve *valve);
void send_valve_info(valve::Valve *valve);
static bool try_send_valve_state(APIConnection *api, void *v_valve);
static bool try_send_valve_info(APIConnection *api, void *v_valve);
void valve_command(const ValveCommandRequest &msg) override;
#endif
#ifdef USE_MEDIA_PLAYER
bool send_media_player_state(media_player::MediaPlayer *media_player);
bool send_media_player_info(media_player::MediaPlayer *media_player);
void send_media_player_info(media_player::MediaPlayer *media_player);
static bool try_send_media_player_state(APIConnection *api, void *v_media_player);
static bool try_send_media_player_info(APIConnection *api, void *v_media_player);
void media_player_command(const MediaPlayerCommandRequest &msg) override;
#endif
bool send_log_message(int level, const char *tag, const char *line);
bool try_send_log_message(int level, const char *tag, const char *line);
void send_homeassistant_service_call(const HomeassistantServiceResponse &call) {
if (!this->service_call_subscription_)
return;
@ -160,18 +244,25 @@ class APIConnection : public APIServerConnection {
#ifdef USE_ALARM_CONTROL_PANEL
bool send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel);
bool send_alarm_control_panel_info(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel);
void send_alarm_control_panel_info(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel);
static bool try_send_alarm_control_panel_state(APIConnection *api, void *v_a_alarm_control_panel);
static bool try_send_alarm_control_panel_info(APIConnection *api, void *v_a_alarm_control_panel);
void alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) override;
#endif
#ifdef USE_EVENT
bool send_event(event::Event *event, std::string event_type);
bool send_event_info(event::Event *event);
void send_event(event::Event *event, std::string event_type);
void send_event_info(event::Event *event);
static bool try_send_event(APIConnection *api, void *v_event);
static bool try_send_event(APIConnection *api, event::Event *event, std::string event_type);
static bool try_send_event_info(APIConnection *api, void *v_event);
#endif
#ifdef USE_UPDATE
bool send_update_state(update::UpdateEntity *update);
bool send_update_info(update::UpdateEntity *update);
void send_update_info(update::UpdateEntity *update);
static bool try_send_update_state(APIConnection *api, void *v_update);
static bool try_send_update_info(APIConnection *api, void *v_update);
void update_command(const UpdateCommandRequest &msg) override;
#endif
@ -262,6 +353,7 @@ class APIConnection : public APIServerConnection {
bool service_call_subscription_{false};
bool next_close_ = false;
APIServer *parent_;
DeferredMessageQueue deferred_message_queue_;
InitialStateIterator initial_state_iterator_;
ListEntitiesIterator list_entities_iterator_;
int state_subs_at_ = -1;

View File

@ -838,6 +838,10 @@ bool DeviceInfoResponse::decode_length(uint32_t field_id, ProtoLengthDelimited v
this->suggested_area = value.as_string();
return true;
}
case 18: {
this->bluetooth_mac_address = value.as_string();
return true;
}
default:
return false;
}
@ -860,6 +864,7 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_uint32(14, this->legacy_voice_assistant_version);
buffer.encode_uint32(17, this->voice_assistant_feature_flags);
buffer.encode_string(16, this->suggested_area);
buffer.encode_string(18, this->bluetooth_mac_address);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void DeviceInfoResponse::dump_to(std::string &out) const {
@ -937,6 +942,10 @@ void DeviceInfoResponse::dump_to(std::string &out) const {
out.append(" suggested_area: ");
out.append("'").append(this->suggested_area).append("'");
out.append("\n");
out.append(" bluetooth_mac_address: ");
out.append("'").append(this->bluetooth_mac_address).append("'");
out.append("\n");
out.append("}");
}
#endif
@ -7085,6 +7094,16 @@ void VoiceAssistantTimerEventResponse::dump_to(std::string &out) const {
out.append("}");
}
#endif
bool VoiceAssistantAnnounceRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
case 4: {
this->start_conversation = value.as_bool();
return true;
}
default:
return false;
}
}
bool VoiceAssistantAnnounceRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 1: {
@ -7095,6 +7114,10 @@ bool VoiceAssistantAnnounceRequest::decode_length(uint32_t field_id, ProtoLength
this->text = value.as_string();
return true;
}
case 3: {
this->preannounce_media_id = value.as_string();
return true;
}
default:
return false;
}
@ -7102,6 +7125,8 @@ bool VoiceAssistantAnnounceRequest::decode_length(uint32_t field_id, ProtoLength
void VoiceAssistantAnnounceRequest::encode(ProtoWriteBuffer buffer) const {
buffer.encode_string(1, this->media_id);
buffer.encode_string(2, this->text);
buffer.encode_string(3, this->preannounce_media_id);
buffer.encode_bool(4, this->start_conversation);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void VoiceAssistantAnnounceRequest::dump_to(std::string &out) const {
@ -7114,6 +7139,14 @@ void VoiceAssistantAnnounceRequest::dump_to(std::string &out) const {
out.append(" text: ");
out.append("'").append(this->text).append("'");
out.append("\n");
out.append(" preannounce_media_id: ");
out.append("'").append(this->preannounce_media_id).append("'");
out.append("\n");
out.append(" start_conversation: ");
out.append(YESNO(this->start_conversation));
out.append("\n");
out.append("}");
}
#endif

View File

@ -354,6 +354,7 @@ class DeviceInfoResponse : public ProtoMessage {
uint32_t legacy_voice_assistant_version{0};
uint32_t voice_assistant_feature_flags{0};
std::string suggested_area{};
std::string bluetooth_mac_address{};
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
@ -1831,6 +1832,8 @@ class VoiceAssistantAnnounceRequest : public ProtoMessage {
public:
std::string media_id{};
std::string text{};
std::string preannounce_media_id{};
bool start_conversation{false};
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
@ -1838,6 +1841,7 @@ class VoiceAssistantAnnounceRequest : public ProtoMessage {
protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class VoiceAssistantAnnounceFinished : public ProtoMessage {
public:

View File

@ -72,7 +72,7 @@ void APIServer::setup() {
logger::global_logger->add_on_log_callback([this](int level, const char *tag, const char *message) {
for (auto &c : this->clients_) {
if (!c->remove_)
c->send_log_message(level, tag, message);
c->try_send_log_message(level, tag, message);
}
});
}
@ -86,7 +86,7 @@ void APIServer::setup() {
[this](const std::shared_ptr<esp32_camera::CameraImage> &image) {
for (auto &c : this->clients_) {
if (!c->remove_)
c->send_camera_state(image);
c->set_camera_state(image);
}
});
}

View File

@ -10,37 +10,63 @@ namespace api {
#ifdef USE_BINARY_SENSOR
bool ListEntitiesIterator::on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) {
return this->client_->send_binary_sensor_info(binary_sensor);
this->client_->send_binary_sensor_info(binary_sensor);
return true;
}
#endif
#ifdef USE_COVER
bool ListEntitiesIterator::on_cover(cover::Cover *cover) { return this->client_->send_cover_info(cover); }
bool ListEntitiesIterator::on_cover(cover::Cover *cover) {
this->client_->send_cover_info(cover);
return true;
}
#endif
#ifdef USE_FAN
bool ListEntitiesIterator::on_fan(fan::Fan *fan) { return this->client_->send_fan_info(fan); }
bool ListEntitiesIterator::on_fan(fan::Fan *fan) {
this->client_->send_fan_info(fan);
return true;
}
#endif
#ifdef USE_LIGHT
bool ListEntitiesIterator::on_light(light::LightState *light) { return this->client_->send_light_info(light); }
bool ListEntitiesIterator::on_light(light::LightState *light) {
this->client_->send_light_info(light);
return true;
}
#endif
#ifdef USE_SENSOR
bool ListEntitiesIterator::on_sensor(sensor::Sensor *sensor) { return this->client_->send_sensor_info(sensor); }
bool ListEntitiesIterator::on_sensor(sensor::Sensor *sensor) {
this->client_->send_sensor_info(sensor);
return true;
}
#endif
#ifdef USE_SWITCH
bool ListEntitiesIterator::on_switch(switch_::Switch *a_switch) { return this->client_->send_switch_info(a_switch); }
bool ListEntitiesIterator::on_switch(switch_::Switch *a_switch) {
this->client_->send_switch_info(a_switch);
return true;
}
#endif
#ifdef USE_BUTTON
bool ListEntitiesIterator::on_button(button::Button *button) { return this->client_->send_button_info(button); }
bool ListEntitiesIterator::on_button(button::Button *button) {
this->client_->send_button_info(button);
return true;
}
#endif
#ifdef USE_TEXT_SENSOR
bool ListEntitiesIterator::on_text_sensor(text_sensor::TextSensor *text_sensor) {
return this->client_->send_text_sensor_info(text_sensor);
this->client_->send_text_sensor_info(text_sensor);
return true;
}
#endif
#ifdef USE_LOCK
bool ListEntitiesIterator::on_lock(lock::Lock *a_lock) { return this->client_->send_lock_info(a_lock); }
bool ListEntitiesIterator::on_lock(lock::Lock *a_lock) {
this->client_->send_lock_info(a_lock);
return true;
}
#endif
#ifdef USE_VALVE
bool ListEntitiesIterator::on_valve(valve::Valve *valve) { return this->client_->send_valve_info(valve); }
bool ListEntitiesIterator::on_valve(valve::Valve *valve) {
this->client_->send_valve_info(valve);
return true;
}
#endif
bool ListEntitiesIterator::on_end() { return this->client_->send_list_info_done(); }
@ -52,55 +78,83 @@ bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) {
#ifdef USE_ESP32_CAMERA
bool ListEntitiesIterator::on_camera(esp32_camera::ESP32Camera *camera) {
return this->client_->send_camera_info(camera);
this->client_->send_camera_info(camera);
return true;
}
#endif
#ifdef USE_CLIMATE
bool ListEntitiesIterator::on_climate(climate::Climate *climate) { return this->client_->send_climate_info(climate); }
bool ListEntitiesIterator::on_climate(climate::Climate *climate) {
this->client_->send_climate_info(climate);
return true;
}
#endif
#ifdef USE_NUMBER
bool ListEntitiesIterator::on_number(number::Number *number) { return this->client_->send_number_info(number); }
bool ListEntitiesIterator::on_number(number::Number *number) {
this->client_->send_number_info(number);
return true;
}
#endif
#ifdef USE_DATETIME_DATE
bool ListEntitiesIterator::on_date(datetime::DateEntity *date) { return this->client_->send_date_info(date); }
bool ListEntitiesIterator::on_date(datetime::DateEntity *date) {
this->client_->send_date_info(date);
return true;
}
#endif
#ifdef USE_DATETIME_TIME
bool ListEntitiesIterator::on_time(datetime::TimeEntity *time) { return this->client_->send_time_info(time); }
bool ListEntitiesIterator::on_time(datetime::TimeEntity *time) {
this->client_->send_time_info(time);
return true;
}
#endif
#ifdef USE_DATETIME_DATETIME
bool ListEntitiesIterator::on_datetime(datetime::DateTimeEntity *datetime) {
return this->client_->send_datetime_info(datetime);
this->client_->send_datetime_info(datetime);
return true;
}
#endif
#ifdef USE_TEXT
bool ListEntitiesIterator::on_text(text::Text *text) { return this->client_->send_text_info(text); }
bool ListEntitiesIterator::on_text(text::Text *text) {
this->client_->send_text_info(text);
return true;
}
#endif
#ifdef USE_SELECT
bool ListEntitiesIterator::on_select(select::Select *select) { return this->client_->send_select_info(select); }
bool ListEntitiesIterator::on_select(select::Select *select) {
this->client_->send_select_info(select);
return true;
}
#endif
#ifdef USE_MEDIA_PLAYER
bool ListEntitiesIterator::on_media_player(media_player::MediaPlayer *media_player) {
return this->client_->send_media_player_info(media_player);
this->client_->send_media_player_info(media_player);
return true;
}
#endif
#ifdef USE_ALARM_CONTROL_PANEL
bool ListEntitiesIterator::on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) {
return this->client_->send_alarm_control_panel_info(a_alarm_control_panel);
this->client_->send_alarm_control_panel_info(a_alarm_control_panel);
return true;
}
#endif
#ifdef USE_EVENT
bool ListEntitiesIterator::on_event(event::Event *event) { return this->client_->send_event_info(event); }
bool ListEntitiesIterator::on_event(event::Event *event) {
this->client_->send_event_info(event);
return true;
}
#endif
#ifdef USE_UPDATE
bool ListEntitiesIterator::on_update(update::UpdateEntity *update) { return this->client_->send_update_info(update); }
bool ListEntitiesIterator::on_update(update::UpdateEntity *update) {
this->client_->send_update_info(update);
return true;
}
#endif
} // namespace api

View File

@ -80,6 +80,7 @@ class ListEntitiesIterator : public ComponentIterator {
bool on_update(update::UpdateEntity *update) override;
#endif
bool on_end() override;
bool completed() { return this->state_ == IteratorState::NONE; }
protected:
APIConnection *client_;

View File

@ -76,6 +76,8 @@ class InitialStateIterator : public ComponentIterator {
#ifdef USE_UPDATE
bool on_update(update::UpdateEntity *update) override;
#endif
bool completed() { return this->state_ == IteratorState::NONE; }
protected:
APIConnection *client_;
};

View File

@ -118,4 +118,4 @@ def final_validate_audio_schema(
async def to_code(config):
cg.add_library("esphome/esp-audio-libs", "1.1.1")
cg.add_library("esphome/esp-audio-libs", "1.1.3")

View File

@ -66,19 +66,30 @@ esp_err_t AudioDecoder::start(AudioFileType audio_file_type) {
case AudioFileType::FLAC:
this->flac_decoder_ = make_unique<esp_audio_libs::flac::FLACDecoder>();
this->free_buffer_required_ =
this->output_transfer_buffer_->capacity(); // We'll revise this after reading the header
this->output_transfer_buffer_->capacity(); // Adjusted and reallocated after reading the header
break;
#endif
#ifdef USE_AUDIO_MP3_SUPPORT
case AudioFileType::MP3:
this->mp3_decoder_ = esp_audio_libs::helix_decoder::MP3InitDecoder();
// MP3 always has 1152 samples per chunk
this->free_buffer_required_ = 1152 * sizeof(int16_t) * 2; // samples * size per sample * channels
// Always reallocate the output transfer buffer to the smallest necessary size
this->output_transfer_buffer_->reallocate(this->free_buffer_required_);
break;
#endif
case AudioFileType::WAV:
this->wav_decoder_ = make_unique<esp_audio_libs::wav_decoder::WAVDecoder>();
this->wav_decoder_->reset();
// Processing WAVs doesn't actually require a specific amount of buffer size, as it is already in PCM format.
// Thus, we don't reallocate to a minimum size.
this->free_buffer_required_ = 1024;
if (this->output_transfer_buffer_->capacity() < this->free_buffer_required_) {
this->output_transfer_buffer_->reallocate(this->free_buffer_required_);
}
break;
case AudioFileType::NONE:
default:
@ -116,10 +127,18 @@ AudioDecoderState AudioDecoder::decode(bool stop_gracefully) {
uint32_t decoding_start = millis();
bool first_loop_iteration = true;
size_t bytes_processed = 0;
size_t bytes_available_before_processing = 0;
while (state == FileDecoderState::MORE_TO_PROCESS) {
// Transfer decoded out
if (!this->pause_output_) {
size_t bytes_written = this->output_transfer_buffer_->transfer_data_to_sink(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS));
// Never shift the data in the output transfer buffer to avoid unnecessary, slow data moves
size_t bytes_written =
this->output_transfer_buffer_->transfer_data_to_sink(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS), false);
if (this->audio_stream_info_.has_value()) {
this->accumulated_frames_written_ += this->audio_stream_info_.value().bytes_to_frames(bytes_written);
this->playback_ms_ +=
@ -138,12 +157,24 @@ AudioDecoderState AudioDecoder::decode(bool stop_gracefully) {
// Decode more audio
size_t bytes_read = this->input_transfer_buffer_->transfer_data_from_source(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS));
// Only shift data on the first loop iteration to avoid unnecessary, slow moves
size_t bytes_read = this->input_transfer_buffer_->transfer_data_from_source(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS),
first_loop_iteration);
if ((this->potentially_failed_count_ > 0) && (bytes_read == 0)) {
if (!first_loop_iteration && (this->input_transfer_buffer_->available() < bytes_processed)) {
// Less data is available than what was processed in last iteration, so don't attempt to decode.
// This attempts to avoid the decoder from consistently trying to decode an incomplete frame. The transfer buffer
// will shift the remaining data to the start and copy more from the source the next time the decode function is
// called
break;
}
bytes_available_before_processing = this->input_transfer_buffer_->available();
if ((this->potentially_failed_count_ > 10) && (bytes_read == 0)) {
// Failed to decode in last attempt and there is no new data
if (this->input_transfer_buffer_->free() == 0) {
if ((this->input_transfer_buffer_->free() == 0) && first_loop_iteration) {
// The input buffer is full. Since it previously failed on the exact same data, we can never recover
state = FileDecoderState::FAILED;
} else {
@ -175,6 +206,9 @@ AudioDecoderState AudioDecoder::decode(bool stop_gracefully) {
}
}
first_loop_iteration = false;
bytes_processed = bytes_available_before_processing - this->input_transfer_buffer_->available();
if (state == FileDecoderState::POTENTIALLY_FAILED) {
++this->potentially_failed_count_;
} else if (state == FileDecoderState::END_OF_FILE) {
@ -207,14 +241,12 @@ FileDecoderState AudioDecoder::decode_flac_() {
size_t bytes_consumed = this->flac_decoder_->get_bytes_index();
this->input_transfer_buffer_->decrease_buffer_length(bytes_consumed);
// Reallocate the output transfer buffer to the smallest necessary size
this->free_buffer_required_ = flac_decoder_->get_output_buffer_size_bytes();
if (this->output_transfer_buffer_->capacity() < this->free_buffer_required_) {
// Output buffer is not big enough
if (!this->output_transfer_buffer_->reallocate(this->free_buffer_required_)) {
// Couldn't reallocate output buffer
return FileDecoderState::FAILED;
}
}
this->audio_stream_info_ =
audio::AudioStreamInfo(this->flac_decoder_->get_sample_depth(), this->flac_decoder_->get_num_channels(),

View File

@ -259,14 +259,14 @@ AudioReaderState AudioReader::file_read_() {
}
AudioReaderState AudioReader::http_read_() {
this->output_transfer_buffer_->transfer_data_to_sink(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS));
this->output_transfer_buffer_->transfer_data_to_sink(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS), false);
if (esp_http_client_is_complete_data_received(this->client_)) {
if (this->output_transfer_buffer_->available() == 0) {
this->cleanup_connection_();
return AudioReaderState::FINISHED;
}
} else {
} else if (this->output_transfer_buffer_->free() > 0) {
size_t bytes_to_read = this->output_transfer_buffer_->free();
int received_len =
esp_http_client_read(this->client_, (char *) this->output_transfer_buffer_->get_buffer_end(), bytes_to_read);

View File

@ -93,8 +93,9 @@ AudioResamplerState AudioResampler::resample(bool stop_gracefully, int32_t *ms_d
}
if (!this->pause_output_) {
// Move audio data to the sink
this->output_transfer_buffer_->transfer_data_to_sink(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS));
// Move audio data to the sink without shifting the data in the output transfer buffer to avoid unnecessary, slow
// data moves
this->output_transfer_buffer_->transfer_data_to_sink(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS), false);
} else {
// If paused, block to avoid wasting CPU resources
delay(READ_WRITE_TIMEOUT_MS);
@ -115,6 +116,7 @@ AudioResamplerState AudioResampler::resample(bool stop_gracefully, int32_t *ms_d
if ((this->input_stream_info_.get_sample_rate() != this->output_stream_info_.get_sample_rate()) ||
(this->input_stream_info_.get_bits_per_sample() != this->output_stream_info_.get_bits_per_sample())) {
// Adjust gain by -3 dB to avoid clipping due to the resampling process
esp_audio_libs::resampler::ResamplerResults results =
this->resampler_->resample(this->input_transfer_buffer_->get_buffer_start(),
this->output_transfer_buffer_->get_buffer_end(), frames_available, frames_free, -3);

View File

@ -33,12 +33,17 @@ size_t AudioTransferBuffer::free() const {
if (this->buffer_size_ == 0) {
return 0;
}
return this->buffer_size_ - (this->buffer_length_ - (this->data_start_ - this->buffer_));
return this->buffer_size_ - (this->buffer_length_ + (this->data_start_ - this->buffer_));
}
void AudioTransferBuffer::decrease_buffer_length(size_t bytes) {
this->buffer_length_ -= bytes;
if (this->buffer_length_ > 0) {
this->data_start_ += bytes;
} else {
// All the data in the buffer has been consumed, reset the start pointer
this->data_start_ = this->buffer_;
}
}
void AudioTransferBuffer::increase_buffer_length(size_t bytes) { this->buffer_length_ += bytes; }
@ -71,7 +76,7 @@ bool AudioTransferBuffer::has_buffered_data() const {
bool AudioTransferBuffer::reallocate(size_t new_buffer_size) {
if (this->buffer_length_ > 0) {
// Already has data in the buffer, fail
// Buffer currently has data, so reallocation is impossible
return false;
}
this->deallocate_buffer_();
@ -106,12 +111,14 @@ void AudioTransferBuffer::deallocate_buffer_() {
this->buffer_length_ = 0;
}
size_t AudioSourceTransferBuffer::transfer_data_from_source(TickType_t ticks_to_wait) {
size_t AudioSourceTransferBuffer::transfer_data_from_source(TickType_t ticks_to_wait, bool pre_shift) {
if (pre_shift) {
// Shift data in buffer to start
if (this->buffer_length_ > 0) {
memmove(this->buffer_, this->data_start_, this->buffer_length_);
}
this->data_start_ = this->buffer_;
}
size_t bytes_to_read = this->free();
size_t bytes_read = 0;
@ -125,7 +132,7 @@ size_t AudioSourceTransferBuffer::transfer_data_from_source(TickType_t ticks_to_
return bytes_read;
}
size_t AudioSinkTransferBuffer::transfer_data_to_sink(TickType_t ticks_to_wait) {
size_t AudioSinkTransferBuffer::transfer_data_to_sink(TickType_t ticks_to_wait, bool post_shift) {
size_t bytes_written = 0;
if (this->available()) {
#ifdef USE_SPEAKER
@ -139,11 +146,14 @@ size_t AudioSinkTransferBuffer::transfer_data_to_sink(TickType_t ticks_to_wait)
}
this->decrease_buffer_length(bytes_written);
}
if (post_shift) {
// Shift unwritten data to the start of the buffer
memmove(this->buffer_, this->data_start_, this->buffer_length_);
this->data_start_ = this->buffer_;
}
return bytes_written;
}

View File

@ -60,6 +60,7 @@ class AudioTransferBuffer {
protected:
/// @brief Allocates the transfer buffer in external memory, if available.
/// @param buffer_size The number of bytes to allocate
/// @return True is successful, false otherwise.
bool allocate_buffer_(size_t buffer_size);
@ -89,8 +90,10 @@ class AudioSinkTransferBuffer : public AudioTransferBuffer {
/// @brief Writes any available data in the transfer buffer to the sink.
/// @param ticks_to_wait FreeRTOS ticks to block while waiting for the sink to have enough space
/// @param post_shift If true, all remaining data is moved to the start of the buffer after transferring to the sink.
/// Defaults to true.
/// @return Number of bytes written
size_t transfer_data_to_sink(TickType_t ticks_to_wait);
size_t transfer_data_to_sink(TickType_t ticks_to_wait, bool post_shift = true);
/// @brief Adds a ring buffer as the transfer buffer's sink.
/// @param ring_buffer weak_ptr to the allocated ring buffer
@ -125,8 +128,10 @@ class AudioSourceTransferBuffer : public AudioTransferBuffer {
/// @brief Reads any available data from the sink into the transfer buffer.
/// @param ticks_to_wait FreeRTOS ticks to block while waiting for the source to have enough data
/// @param pre_shift If true, any unwritten data is moved to the start of the buffer before transferring from the
/// source. Defaults to true.
/// @return Number of bytes read
size_t transfer_data_from_source(TickType_t ticks_to_wait);
size_t transfer_data_from_source(TickType_t ticks_to_wait, bool pre_shift = true);
/// @brief Adds a ring buffer as the transfer buffer's source.
/// @param ring_buffer weak_ptr to the allocated ring buffer

View File

@ -15,6 +15,9 @@
#include "bluetooth_connection.h"
#include <esp_bt.h>
#include <esp_bt_device.h>
namespace esphome {
namespace bluetooth_proxy {
@ -114,6 +117,11 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com
return flags;
}
std::string get_bluetooth_mac_address_pretty() {
const uint8_t *mac = esp_bt_dev_get_address();
return str_snprintf("%02X:%02X:%02X:%02X:%02X:%02X", 17, mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
}
protected:
void send_api_packet_(const esp32_ble_tracker::ESPBTDevice &device);

View File

@ -95,7 +95,7 @@ void BMP085Component::read_pressure_() {
return;
}
uint32_t value = (uint32_t(buffer[0]) << 16) | (uint32_t(buffer[1]) << 8) | uint32_t(buffer[0]);
uint32_t value = (uint32_t(buffer[0]) << 16) | (uint32_t(buffer[1]) << 8) | uint32_t(buffer[2]);
if ((value >> 5) == 0) {
ESP_LOGW(TAG, "Invalid pressure!");
this->status_set_warning();

View File

@ -0,0 +1,2 @@
CODEOWNERS = ["@kkosik20"]
DEPENDENCIES = ["i2c"]

View File

@ -0,0 +1,47 @@
#include "chsc6x_touchscreen.h"
namespace esphome {
namespace chsc6x {
void CHSC6XTouchscreen::setup() {
ESP_LOGCONFIG(TAG, "Setting up CHSC6X Touchscreen...");
if (this->interrupt_pin_ != nullptr) {
this->interrupt_pin_->setup();
this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE);
}
if (this->x_raw_max_ == this->x_raw_min_) {
this->x_raw_max_ = this->display_->get_native_width();
}
if (this->y_raw_max_ == this->y_raw_min_) {
this->y_raw_max_ = this->display_->get_native_height();
}
ESP_LOGCONFIG(TAG, "CHSC6X Touchscreen setup complete");
}
void CHSC6XTouchscreen::update_touches() {
uint8_t data[CHSC6X_REG_STATUS_LEN];
if (!this->read_bytes(CHSC6X_REG_STATUS, data, sizeof(data))) {
return;
}
uint8_t num_of_touches = data[CHSC6X_REG_STATUS_TOUCH];
if (num_of_touches == 1) {
uint16_t x = data[CHSC6X_REG_STATUS_X_COR];
uint16_t y = data[CHSC6X_REG_STATUS_Y_COR];
this->add_raw_touch_position_(0, x, y);
}
}
void CHSC6XTouchscreen::dump_config() {
ESP_LOGCONFIG(TAG, "CHSC6X Touchscreen:");
LOG_I2C_DEVICE(this);
LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_);
ESP_LOGCONFIG(TAG, " Touch timeout: %d", this->touch_timeout_);
ESP_LOGCONFIG(TAG, " x_raw_max_: %d", this->x_raw_max_);
ESP_LOGCONFIG(TAG, " y_raw_max_: %d", this->y_raw_max_);
}
} // namespace chsc6x
} // namespace esphome

View File

@ -0,0 +1,34 @@
#pragma once
#include "esphome/components/i2c/i2c.h"
#include "esphome/components/touchscreen/touchscreen.h"
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
namespace esphome {
namespace chsc6x {
static const char *const TAG = "chsc6x.touchscreen";
static const uint8_t CHSC6X_REG_STATUS = 0x00;
static const uint8_t CHSC6X_REG_STATUS_TOUCH = 0x00;
static const uint8_t CHSC6X_REG_STATUS_X_COR = 0x02;
static const uint8_t CHSC6X_REG_STATUS_Y_COR = 0x04;
static const uint8_t CHSC6X_REG_STATUS_LEN = 0x05;
static const uint8_t CHSC6X_CHIP_ID = 0x2e;
class CHSC6XTouchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice {
public:
void setup() override;
void update_touches() override;
void dump_config() override;
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
protected:
InternalGPIOPin *interrupt_pin_{};
};
} // namespace chsc6x
} // namespace esphome

View File

@ -0,0 +1,33 @@
from esphome import pins
import esphome.codegen as cg
from esphome.components import i2c, touchscreen
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_INTERRUPT_PIN
chsc6x_ns = cg.esphome_ns.namespace("chsc6x")
CHSC6XTouchscreen = chsc6x_ns.class_(
"CHSC6XTouchscreen",
touchscreen.Touchscreen,
i2c.I2CDevice,
)
CONFIG_SCHEMA = (
touchscreen.touchscreen_schema("100ms")
.extend(
{
cv.GenerateID(): cv.declare_id(CHSC6XTouchscreen),
cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema,
}
)
.extend(i2c.i2c_device_schema(0x2E))
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await touchscreen.register_touchscreen(var, config)
await i2c.register_i2c_device(var, config)
if interrupt_pin := config.get(CONF_INTERRUPT_PIN):
cg.add(var.set_interrupt_pin(await cg.gpio_pin_expression(interrupt_pin)))

View File

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

View File

@ -1,28 +1,5 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import binary_sensor
from .. import cst816_ns
from ..touchscreen import CST816Touchscreen, CST816ButtonListener
CONF_CST816_ID = "cst816_id"
CST816Button = cst816_ns.class_(
"CST816Button",
binary_sensor.BinarySensor,
cg.Component,
CST816ButtonListener,
cg.Parented.template(CST816Touchscreen),
CONFIG_SCHEMA = cv.invalid(
"The CST816 binary sensor has been removed. Instead use the touchscreen binary sensor with the 'use_raw' flag set."
)
CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(CST816Button).extend(
{
cv.GenerateID(CONF_CST816_ID): cv.use_id(CST816Touchscreen),
}
)
async def to_code(config):
var = await binary_sensor.new_binary_sensor(config)
await cg.register_component(var, config)
await cg.register_parented(var, config[CONF_CST816_ID])

View File

@ -1,27 +0,0 @@
#pragma once
#include "esphome/components/binary_sensor/binary_sensor.h"
#include "esphome/components/cst816/touchscreen/cst816_touchscreen.h"
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
namespace esphome {
namespace cst816 {
class CST816Button : public binary_sensor::BinarySensor,
public Component,
public CST816ButtonListener,
public Parented<CST816Touchscreen> {
public:
void setup() override {
this->parent_->register_button_listener(this);
this->publish_initial_state(false);
}
void dump_config() override { LOG_BINARY_SENSOR("", "CST816 Button", this); }
void update_button(bool state) override { this->publish_state(state); }
};
} // namespace cst816
} // namespace esphome

View File

@ -37,14 +37,6 @@ void CST816Touchscreen::continue_setup_() {
ESP_LOGCONFIG(TAG, "CST816 Touchscreen setup complete");
}
void CST816Touchscreen::update_button_state_(bool state) {
if (this->button_touched_ == state)
return;
this->button_touched_ = state;
for (auto *listener : this->button_listeners_)
listener->update_button(state);
}
void CST816Touchscreen::setup() {
ESP_LOGCONFIG(TAG, "Setting up CST816 Touchscreen...");
if (this->reset_pin_ != nullptr) {
@ -68,25 +60,22 @@ void CST816Touchscreen::update_touches() {
}
uint8_t num_of_touches = data[REG_TOUCH_NUM] & 3;
if (num_of_touches == 0) {
this->update_button_state_(false);
return;
}
uint16_t x = encode_uint16(data[REG_XPOS_HIGH] & 0xF, data[REG_XPOS_LOW]);
uint16_t y = encode_uint16(data[REG_YPOS_HIGH] & 0xF, data[REG_YPOS_LOW]);
ESP_LOGV(TAG, "Read touch %d/%d", x, y);
if (x >= this->x_raw_max_) {
this->update_button_state_(true);
} else {
this->add_raw_touch_position_(0, x, y);
}
}
void CST816Touchscreen::dump_config() {
ESP_LOGCONFIG(TAG, "CST816 Touchscreen:");
LOG_I2C_DEVICE(this);
LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_);
LOG_PIN(" Reset Pin: ", this->reset_pin_);
ESP_LOGCONFIG(TAG, " X Raw Min: %d, X Raw Max: %d", this->x_raw_min_, this->x_raw_max_);
ESP_LOGCONFIG(TAG, " Y Raw Min: %d, Y Raw Max: %d", this->y_raw_min_, this->y_raw_max_);
const char *name;
switch (this->chip_id_) {
case CST820_CHIP_ID:

View File

@ -40,7 +40,6 @@ class CST816Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice
public:
void setup() override;
void update_touches() override;
void register_button_listener(CST816ButtonListener *listener) { this->button_listeners_.push_back(listener); }
void dump_config() override;
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
@ -49,14 +48,11 @@ class CST816Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice
protected:
void continue_setup_();
void update_button_state_(bool state);
InternalGPIOPin *interrupt_pin_{};
GPIOPin *reset_pin_{};
uint8_t chip_id_{};
bool skip_probe_{}; // if set, do not expect to be able to probe the controller on the i2c bus.
std::vector<CST816ButtonListener *> button_listeners_;
bool button_touched_{};
};
} // namespace cst816

View File

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

View File

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

View File

@ -1,3 +1,4 @@
from collections.abc import MutableMapping
import functools
import hashlib
import logging
@ -6,10 +7,10 @@ from pathlib import Path
import re
import esphome_glyphsets as glyphsets
import freetype
from freetype import Face, ft_pixel_mode_grays, ft_pixel_mode_mono
import requests
from esphome import core, external_files
from esphome import external_files
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import (
@ -26,7 +27,7 @@ from esphome.const import (
CONF_WEIGHT,
)
from esphome.core import CORE, HexInt
from esphome.helpers import copy_file_if_changed, cpp_string_escape
from esphome.helpers import cpp_string_escape
_LOGGER = logging.getLogger(__name__)
@ -49,13 +50,42 @@ CONF_IGNORE_MISSING_GLYPHS = "ignore_missing_glyphs"
# Cache loaded freetype fonts
class FontCache(dict):
def __missing__(self, key):
try:
res = self[key] = freetype.Face(key)
return res
except freetype.FT_Exception as e:
raise cv.Invalid(f"Could not load Font file {key}: {e}") from e
class FontCache(MutableMapping):
@staticmethod
def get_name(value):
if CONF_FAMILY in value:
return (
f"{value[CONF_FAMILY]}:{int(value[CONF_ITALIC])}:{value[CONF_WEIGHT]}"
)
if CONF_URL in value:
return value[CONF_URL]
return value[CONF_PATH]
@staticmethod
def _keytransform(value):
if CONF_FAMILY in value:
return f"gfont:{value[CONF_FAMILY]}:{int(value[CONF_ITALIC])}:{value[CONF_WEIGHT]}"
if CONF_URL in value:
return f"url:{value[CONF_URL]}"
return f"file:{value[CONF_PATH]}"
def __init__(self):
self.store = {}
def __delitem__(self, key):
del self.store[self._keytransform(key)]
def __iter__(self):
return iter(self.store)
def __len__(self):
return len(self.store)
def __getitem__(self, item):
return self.store[self._keytransform(item)]
def __setitem__(self, key, value):
self.store[self._keytransform(key)] = Face(str(value))
FONT_CACHE = FontCache()
@ -109,14 +139,21 @@ def check_missing_glyphs(file, codepoints, warning: bool = False):
)
if count > 10:
missing_str += f"\n and {count - 10} more."
message = f"Font {Path(file).name} is missing {count} glyph{'s' if count != 1 else ''}:\n {missing_str}"
message = f"Font {FontCache.get_name(file)} is missing {count} glyph{'s' if count != 1 else ''}:\n {missing_str}"
if warning:
_LOGGER.warning(message)
else:
raise cv.Invalid(message)
def validate_glyphs(config):
def pt_to_px(pt):
"""
Convert a point size to pixels, rounding up to the nearest pixel
"""
return (pt + 63) // 64
def validate_font_config(config):
"""
Check for duplicate codepoints, then check that all requested codepoints actually
have glyphs defined in the appropriate font file.
@ -142,43 +179,51 @@ def validate_glyphs(config):
)
# Make setpoints and glyphspoints disjoint
setpoints.difference_update(glyphspoints)
if fileconf[CONF_TYPE] == TYPE_LOCAL_BITMAP:
# Pillow only allows 256 glyphs per bitmap font. Not sure if that is a Pillow limitation
# or a file format limitation
if any(x >= 256 for x in setpoints.copy().union(glyphspoints)):
raise cv.Invalid("Codepoints in bitmap fonts must be in the range 0-255")
else:
# for TT fonts, check that glyphs are actually present
# check that glyphs are actually present
# Check extras against their own font, exclude from parent font codepoints
for extra in config[CONF_EXTRAS]:
points = {ord(x) for x in flatten(extra[CONF_GLYPHS])}
glyphspoints.difference_update(points)
setpoints.difference_update(points)
check_missing_glyphs(extra[CONF_FILE][CONF_PATH], points)
check_missing_glyphs(extra[CONF_FILE], points)
# A named glyph that can't be provided is an error
check_missing_glyphs(fileconf[CONF_PATH], glyphspoints)
check_missing_glyphs(fileconf, glyphspoints)
# A missing glyph from a set is a warning.
if not config[CONF_IGNORE_MISSING_GLYPHS]:
check_missing_glyphs(fileconf[CONF_PATH], setpoints, warning=True)
check_missing_glyphs(fileconf, setpoints, warning=True)
# Populate the default after the above checks so that use of the default doesn't trigger errors
font = FONT_CACHE[fileconf]
if not config[CONF_GLYPHS] and not config[CONF_GLYPHSETS]:
if fileconf[CONF_TYPE] == TYPE_LOCAL_BITMAP:
config[CONF_GLYPHS] = [DEFAULT_GLYPHS]
else:
# set a default glyphset, intersected with what the font actually offers
font = FONT_CACHE[fileconf[CONF_PATH]]
config[CONF_GLYPHS] = [
chr(x)
for x in glyphsets.unicodes_per_glyphset(DEFAULT_GLYPHSET)
if font.get_char_index(x) != 0
]
if font.has_fixed_sizes:
sizes = [pt_to_px(x.size) for x in font.available_sizes]
if not sizes:
raise cv.Invalid(
f"Font {FontCache.get_name(fileconf)} has no available sizes"
)
if CONF_SIZE not in config:
config[CONF_SIZE] = sizes[0]
elif config[CONF_SIZE] not in sizes:
sizes = ", ".join(str(x) for x in sizes)
raise cv.Invalid(
f"Font {FontCache.get_name(fileconf)} only has size{'s' if len(sizes) != 1 else ''} {sizes} available"
)
elif CONF_SIZE not in config:
config[CONF_SIZE] = 20
return config
FONT_EXTENSIONS = (".ttf", ".woff", ".otf")
FONT_EXTENSIONS = (".ttf", ".woff", ".otf", "bdf", ".pcf")
def validate_truetype_file(value):
@ -187,24 +232,30 @@ def validate_truetype_file(value):
f"Please unzip the font archive '{value}' first and then use the .ttf files inside."
)
if not any(map(value.lower().endswith, FONT_EXTENSIONS)):
raise cv.Invalid(f"Only {FONT_EXTENSIONS} files are supported.")
raise cv.Invalid(f"Only {', '.join(FONT_EXTENSIONS)} files are supported.")
return CORE.relative_config_path(cv.file_(value))
def add_local_file(value):
if value in FONT_CACHE:
return value
path = value[CONF_PATH]
if not os.path.isfile(path):
raise cv.Invalid(f"File '{path}' not found.")
FONT_CACHE[value] = path
return value
TYPE_LOCAL = "local"
TYPE_LOCAL_BITMAP = "local_bitmap"
TYPE_GFONTS = "gfonts"
TYPE_WEB = "web"
LOCAL_SCHEMA = cv.Schema(
LOCAL_SCHEMA = cv.All(
cv.Schema(
{
cv.Required(CONF_PATH): validate_truetype_file,
}
)
LOCAL_BITMAP_SCHEMA = cv.Schema(
{
cv.Required(CONF_PATH): cv.file_,
}
),
add_local_file,
)
FULLPATH_SCHEMA = cv.maybe_simple_value(
@ -235,27 +286,23 @@ def _compute_local_font_path(value: dict) -> Path:
h.update(url.encode())
key = h.hexdigest()[:8]
base_dir = external_files.compute_local_file_dir(DOMAIN)
_LOGGER.debug("_compute_local_font_path: base_dir=%s", base_dir / key)
_LOGGER.debug("_compute_local_font_path: %s", base_dir / key)
return base_dir / key
def get_font_path(value, font_type) -> Path:
if font_type == TYPE_GFONTS:
name = f"{value[CONF_FAMILY]}@{value[CONF_WEIGHT]}@{value[CONF_ITALIC]}@v1"
return external_files.compute_local_file_dir(DOMAIN) / f"{name}.ttf"
if font_type == TYPE_WEB:
return _compute_local_font_path(value) / "font.ttf"
assert False
def download_gfont(value):
if value in FONT_CACHE:
return value
name = (
f"{value[CONF_FAMILY]}:ital,wght@{int(value[CONF_ITALIC])},{value[CONF_WEIGHT]}"
)
url = f"https://fonts.googleapis.com/css2?family={name}"
path = get_font_path(value, TYPE_GFONTS)
path = (
external_files.compute_local_file_dir(DOMAIN)
/ f"{value[CONF_FAMILY]}@{value[CONF_WEIGHT]}@{value[CONF_ITALIC]}@v1.ttf"
)
if not external_files.is_file_recent(str(path), value[CONF_REFRESH]):
_LOGGER.debug("download_gfont: path=%s", path)
try:
req = requests.get(url, timeout=external_files.NETWORK_TIMEOUT)
req.raise_for_status()
@ -275,16 +322,23 @@ def download_gfont(value):
_LOGGER.debug("download_gfont: ttf_url=%s", ttf_url)
external_files.download_content(ttf_url, path)
return FULLPATH_SCHEMA(path)
# In case the remote file is not modified, the download_content function will return the existing file,
# so update the modification time to now.
path.touch()
FONT_CACHE[value] = path
return value
def download_web_font(value):
if value in FONT_CACHE:
return value
url = value[CONF_URL]
path = get_font_path(value, TYPE_WEB)
path = _compute_local_font_path(value) / "font.ttf"
external_files.download_content(url, path)
_LOGGER.debug("download_web_font: path=%s", path)
return FULLPATH_SCHEMA(path)
FONT_CACHE[value] = path
return value
EXTERNAL_FONT_SCHEMA = cv.Schema(
@ -340,15 +394,6 @@ def validate_file_shorthand(value):
}
)
if value.endswith(".pcf") or value.endswith(".bdf"):
value = convert_bitmap_to_pillow_font(
CORE.relative_config_path(cv.file_(value))
)
return {
CONF_TYPE: TYPE_LOCAL_BITMAP,
CONF_PATH: value,
}
return font_file_schema(
{
CONF_TYPE: TYPE_LOCAL,
@ -361,7 +406,6 @@ TYPED_FILE_SCHEMA = cv.typed_schema(
{
TYPE_LOCAL: LOCAL_SCHEMA,
TYPE_GFONTS: GFONTS_SCHEMA,
TYPE_LOCAL_BITMAP: LOCAL_BITMAP_SCHEMA,
TYPE_WEB: WEB_FONT_SCHEMA,
}
)
@ -391,7 +435,7 @@ FONT_SCHEMA = cv.Schema(
cv.one_of(*glyphsets.defined_glyphsets())
),
cv.Optional(CONF_IGNORE_MISSING_GLYPHS, default=False): cv.boolean,
cv.Optional(CONF_SIZE, default=20): cv.int_range(min=1),
cv.Optional(CONF_SIZE): cv.int_range(min=1),
cv.Optional(CONF_BPP, default=1): cv.one_of(1, 2, 4, 8),
cv.Optional(CONF_EXTRAS, default=[]): cv.ensure_list(
cv.Schema(
@ -406,114 +450,19 @@ FONT_SCHEMA = cv.Schema(
},
)
CONFIG_SCHEMA = cv.All(FONT_SCHEMA, validate_glyphs)
# PIL doesn't provide a consistent interface for both TrueType and bitmap
# fonts. So, we use our own wrappers to give us the consistency that we need.
class TrueTypeFontWrapper:
def __init__(self, font):
self.font = font
def getoffset(self, glyph):
_, (offset_x, offset_y) = self.font.font.getsize(glyph)
return offset_x, offset_y
def getmask(self, glyph, **kwargs):
return self.font.getmask(str(glyph), **kwargs)
def getmetrics(self, glyphs):
return self.font.getmetrics()
class BitmapFontWrapper:
def __init__(self, font):
self.font = font
self.max_height = 0
def getoffset(self, glyph):
return 0, 0
def getmask(self, glyph, **kwargs):
return self.font.getmask(str(glyph), **kwargs)
def getmetrics(self, glyphs):
max_height = 0
for glyph in glyphs:
mask = self.getmask(glyph, mode="1")
_, height = mask.size
max_height = max(max_height, height)
return max_height, 0
CONFIG_SCHEMA = cv.All(FONT_SCHEMA, validate_font_config)
class EFont:
def __init__(self, file, size, codepoints):
def __init__(self, file, codepoints):
self.codepoints = codepoints
path = file[CONF_PATH]
self.name = Path(path).name
ftype = file[CONF_TYPE]
if ftype == TYPE_LOCAL_BITMAP:
self.font = load_bitmap_font(path)
else:
self.font = load_ttf_font(path, size)
self.ascent, self.descent = self.font.getmetrics(codepoints)
def convert_bitmap_to_pillow_font(filepath):
from PIL import BdfFontFile, PcfFontFile
local_bitmap_font_file = external_files.compute_local_file_dir(
DOMAIN,
) / os.path.basename(filepath)
copy_file_if_changed(filepath, local_bitmap_font_file)
local_pil_font_file = local_bitmap_font_file.with_suffix(".pil")
with open(local_bitmap_font_file, "rb") as fp:
try:
try:
p = PcfFontFile.PcfFontFile(fp)
except SyntaxError:
fp.seek(0)
p = BdfFontFile.BdfFontFile(fp)
# Convert to pillow-formatted fonts, which have a .pil and .pbm extension.
p.save(local_pil_font_file)
except (SyntaxError, OSError) as err:
raise core.EsphomeError(
f"Failed to parse as bitmap font: '{filepath}': {err}"
)
return str(local_pil_font_file)
def load_bitmap_font(filepath):
from PIL import ImageFont
try:
font = ImageFont.load(str(filepath))
except Exception as e:
raise core.EsphomeError(f"Failed to load bitmap font file: {filepath}: {e}")
return BitmapFontWrapper(font)
def load_ttf_font(path, size):
from PIL import ImageFont
try:
font = ImageFont.truetype(str(path), size)
except Exception as e:
raise core.EsphomeError(f"Could not load TrueType file {path}: {e}")
return TrueTypeFontWrapper(font)
self.font: Face = FONT_CACHE[file]
class GlyphInfo:
def __init__(self, data_len, offset_x, offset_y, width, height):
def __init__(self, data_len, advance, offset_x, offset_y, width, height):
self.data_len = data_len
self.advance = advance
self.offset_x = offset_x
self.offset_y = offset_y
self.width = width
@ -537,15 +486,14 @@ async def to_code(config):
}
# get the codepoints from the glyphs key, flatten to a list of chrs and combine with the points from glyphsets
point_set.update(flatten(config[CONF_GLYPHS]))
size = config[CONF_SIZE]
# Create the codepoint to font file map
base_font = EFont(config[CONF_FILE], size, point_set)
point_font_map: dict[str, EFont] = {c: base_font for c in point_set}
base_font = FONT_CACHE[config[CONF_FILE]]
point_font_map: dict[str, Face] = {c: base_font for c in point_set}
# process extras, updating the map and extending the codepoint list
for extra in config[CONF_EXTRAS]:
extra_points = flatten(extra[CONF_GLYPHS])
point_set.update(extra_points)
extra_font = EFont(extra[CONF_FILE], size, extra_points)
extra_font = FONT_CACHE[extra[CONF_FILE]]
point_font_map.update({c: extra_font for c in extra_points})
codepoints = list(point_set)
@ -553,28 +501,54 @@ async def to_code(config):
glyph_args = {}
data = []
bpp = config[CONF_BPP]
if bpp == 1:
mode = "1"
scale = 1
else:
mode = "L"
mode = ft_pixel_mode_grays
scale = 256 // (1 << bpp)
size = config[CONF_SIZE]
# create the data array for all glyphs
for codepoint in codepoints:
font = point_font_map[codepoint]
mask = font.font.getmask(codepoint, mode=mode)
offset_x, offset_y = font.font.getoffset(codepoint)
width, height = mask.size
format = font.get_format().decode("utf-8")
if format != "PCF":
font.set_pixel_sizes(size, 0)
font.load_char(codepoint)
font.glyph.render(mode)
width = font.glyph.bitmap.width
height = font.glyph.bitmap.rows
buffer = font.glyph.bitmap.buffer
pitch = font.glyph.bitmap.pitch
glyph_data = [0] * ((height * width * bpp + 7) // 8)
src_mode = font.glyph.bitmap.pixel_mode
pos = 0
for y in range(height):
for x in range(width):
pixel = mask.getpixel((x, y)) // scale
if src_mode == ft_pixel_mode_mono:
pixel = (
(1 << bpp) - 1
if buffer[y * pitch + x // 8] & (1 << (7 - x % 8))
else 0
)
else:
pixel = buffer[y * pitch + x] // scale
for bit_num in range(bpp):
if pixel & (1 << (bpp - bit_num - 1)):
glyph_data[pos // 8] |= 0x80 >> (pos % 8)
pos += 1
glyph_args[codepoint] = GlyphInfo(len(data), offset_x, offset_y, width, height)
ascender = pt_to_px(font.size.ascender)
if ascender == 0:
if font.has_fixed_sizes:
ascender = size
else:
_LOGGER.error(
"Unable to determine ascender of font %s", config[CONF_FILE]
)
glyph_args[codepoint] = GlyphInfo(
len(data),
pt_to_px(font.glyph.metrics.horiAdvance),
font.glyph.bitmap_left,
ascender - font.glyph.bitmap_top,
width,
height,
)
data += glyph_data
rhs = [HexInt(x) for x in data]
@ -598,6 +572,7 @@ async def to_code(config):
f"{str(prog_arr)} + {str(glyph_args[codepoint].data_len)}"
),
),
("advance", glyph_args[codepoint].advance),
("offset_x", glyph_args[codepoint].offset_x),
("offset_y", glyph_args[codepoint].offset_y),
("width", glyph_args[codepoint].width),
@ -607,11 +582,19 @@ async def to_code(config):
glyphs = cg.static_const_array(config[CONF_RAW_GLYPH_ID], glyph_initializer)
font_height = pt_to_px(base_font.size.height)
ascender = pt_to_px(base_font.size.ascender)
if font_height == 0:
if base_font.has_fixed_sizes:
font_height = size
ascender = font_height
else:
_LOGGER.error("Unable to determine height of font %s", config[CONF_FILE])
cg.new_Pvariable(
config[CONF_ID],
glyphs,
len(glyph_initializer),
base_font.ascent,
base_font.ascent + base_font.descent,
ascender,
font_height,
bpp,
)

View File

@ -81,7 +81,7 @@ void Font::measure(const char *str, int *width, int *x_offset, int *baseline, in
if (glyph_n < 0) {
// Unknown char, skip
if (!this->get_glyphs().empty())
x += this->get_glyphs()[0].glyph_data_->width;
x += this->get_glyphs()[0].glyph_data_->advance;
i++;
continue;
}
@ -92,7 +92,7 @@ void Font::measure(const char *str, int *width, int *x_offset, int *baseline, in
} else {
min_x = std::min(min_x, x + glyph.glyph_data_->offset_x);
}
x += glyph.glyph_data_->width + glyph.glyph_data_->offset_x;
x += glyph.glyph_data_->advance;
i += match_length;
has_char = true;
@ -111,7 +111,7 @@ void Font::print(int x_start, int y_start, display::Display *display, Color colo
// Unknown char, skip
ESP_LOGW(TAG, "Encountered character without representation in font: '%c'", text[i]);
if (!this->get_glyphs().empty()) {
uint8_t glyph_width = this->get_glyphs()[0].glyph_data_->width;
uint8_t glyph_width = this->get_glyphs()[0].glyph_data_->advance;
display->filled_rectangle(x_at, y_start, glyph_width, this->height_, color);
x_at += glyph_width;
}
@ -161,7 +161,7 @@ void Font::print(int x_start, int y_start, display::Display *display, Color colo
}
}
}
x_at += glyph.glyph_data_->width + glyph.glyph_data_->offset_x;
x_at += glyph.glyph_data_->advance;
i += match_length;
}

View File

@ -15,6 +15,7 @@ class Font;
struct GlyphData {
const uint8_t *a_char;
const uint8_t *data;
int advance;
int offset_x;
int offset_y;
int width;

View File

@ -132,6 +132,10 @@ void Graph::draw(Display *buff, uint16_t x_offset, uint16_t y_offset, Color colo
yrange = ymax - ymin;
}
// Store graph limts
this->graph_limit_max_ = ymax;
this->graph_limit_min_ = ymin;
/// Draw grid
if (!std::isnan(this->gridspacing_y_)) {
for (int y = yn; y <= ym; y++) {

View File

@ -161,11 +161,15 @@ class Graph : public Component {
uint32_t get_duration() { return duration_; }
uint32_t get_width() { return width_; }
uint32_t get_height() { return height_; }
float get_graph_limit_min() { return graph_limit_min_; }
float get_graph_limit_max() { return graph_limit_max_; }
protected:
uint32_t duration_; /// in seconds
uint32_t width_; /// in pixels
uint32_t height_; /// in pixels
float graph_limit_min_{NAN};
float graph_limit_max_{NAN};
float min_value_{NAN};
float max_value_{NAN};
float min_range_{1.0};

View File

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

View File

@ -12,7 +12,7 @@ float HBridgeSwitch::get_setup_priority() const { return setup_priority::HARDWAR
void HBridgeSwitch::setup() {
ESP_LOGCONFIG(TAG, "Setting up H-Bridge Switch '%s'...", this->name_.c_str());
optional<bool> initial_state = this->get_initial_state_with_restore_mode().value_or(false);
optional<bool> initial_state = this->get_initial_state_with_restore_mode();
// Like GPIOSwitch does, set the pin state both before and after pin setup()
this->on_pin_->digital_write(false);
@ -24,7 +24,7 @@ void HBridgeSwitch::setup() {
this->off_pin_->digital_write(false);
if (initial_state.has_value())
this->write_state(initial_state);
this->write_state(initial_state.value());
}
void HBridgeSwitch::dump_config() {

View File

@ -53,6 +53,7 @@ PROTOCOLS = {
"mitsubishi_sez": Protocol.PROTOCOL_MITSUBISHI_SEZ,
"panasonic_ckp": Protocol.PROTOCOL_PANASONIC_CKP,
"panasonic_dke": Protocol.PROTOCOL_PANASONIC_DKE,
"panasonic_eke": Protocol.PROTOCOL_PANASONIC_EKE,
"panasonic_jke": Protocol.PROTOCOL_PANASONIC_JKE,
"panasonic_lke": Protocol.PROTOCOL_PANASONIC_LKE,
"panasonic_nke": Protocol.PROTOCOL_PANASONIC_NKE,
@ -127,6 +128,6 @@ def to_code(config):
cg.add(var.set_max_temperature(config[CONF_MAX_TEMPERATURE]))
cg.add(var.set_min_temperature(config[CONF_MIN_TEMPERATURE]))
cg.add_library("tonia/HeatpumpIR", "1.0.27")
cg.add_library("tonia/HeatpumpIR", "1.0.32")
if CORE.is_libretiny:
CORE.add_platformio_option("lib_ignore", "IRremoteESP8266")

View File

@ -47,6 +47,7 @@ const std::map<Protocol, std::function<HeatpumpIR *()>> PROTOCOL_CONSTRUCTOR_MAP
{PROTOCOL_MITSUBISHI_SEZ, []() { return new MitsubishiSEZKDXXHeatpumpIR(); }}, // NOLINT
{PROTOCOL_PANASONIC_CKP, []() { return new PanasonicCKPHeatpumpIR(); }}, // NOLINT
{PROTOCOL_PANASONIC_DKE, []() { return new PanasonicDKEHeatpumpIR(); }}, // NOLINT
{PROTOCOL_PANASONIC_EKE, []() { return new PanasonicEKEHeatpumpIR(); }}, // NOLINT
{PROTOCOL_PANASONIC_JKE, []() { return new PanasonicJKEHeatpumpIR(); }}, // NOLINT
{PROTOCOL_PANASONIC_LKE, []() { return new PanasonicLKEHeatpumpIR(); }}, // NOLINT
{PROTOCOL_PANASONIC_NKE, []() { return new PanasonicNKEHeatpumpIR(); }}, // NOLINT

View File

@ -47,6 +47,7 @@ enum Protocol {
PROTOCOL_MITSUBISHI_SEZ,
PROTOCOL_PANASONIC_CKP,
PROTOCOL_PANASONIC_DKE,
PROTOCOL_PANASONIC_EKE,
PROTOCOL_PANASONIC_JKE,
PROTOCOL_PANASONIC_LKE,
PROTOCOL_PANASONIC_NKE,

View File

@ -1,23 +1,23 @@
from esphome import pins
import esphome.codegen as cg
import esphome.config_validation as cv
import esphome.final_validate as fv
from esphome import pins
from esphome.const import (
CONF_ADDRESS,
CONF_FREQUENCY,
CONF_TIMEOUT,
CONF_I2C_ID,
CONF_ID,
CONF_INPUT,
CONF_OUTPUT,
CONF_SCAN,
CONF_SCL,
CONF_SDA,
CONF_ADDRESS,
CONF_I2C_ID,
CONF_TIMEOUT,
PLATFORM_ESP32,
PLATFORM_ESP8266,
PLATFORM_RP2040,
)
from esphome.core import coroutine_with_priority, CORE
from esphome.core import CORE, coroutine_with_priority
import esphome.final_validate as fv
CODEOWNERS = ["@esphome/core"]
i2c_ns = cg.esphome_ns.namespace("i2c")

View File

@ -8,6 +8,10 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 3, 0)
#define SOC_HP_I2C_NUM SOC_I2C_NUM
#endif
namespace esphome {
namespace i2c {
@ -17,14 +21,14 @@ void IDFI2CBus::setup() {
ESP_LOGCONFIG(TAG, "Setting up I2C bus...");
static i2c_port_t next_port = I2C_NUM_0;
port_ = next_port;
#if SOC_I2C_NUM > 1
#if SOC_HP_I2C_NUM > 1
next_port = (next_port == I2C_NUM_0) ? I2C_NUM_1 : I2C_NUM_MAX;
#else
next_port = I2C_NUM_MAX;
#endif
if (port_ == I2C_NUM_MAX) {
ESP_LOGE(TAG, "Too many I2C buses configured. Max %u supported.", SOC_I2C_NUM);
ESP_LOGE(TAG, "Too many I2C buses configured. Max %u supported.", SOC_HP_I2C_NUM);
this->mark_failed();
return;
}

View File

@ -203,7 +203,7 @@ size_t I2SAudioSpeaker::play(const uint8_t *data, size_t length, TickType_t tick
this->start();
}
if ((this->state_ != speaker::STATE_RUNNING) || (this->audio_ring_buffer_.use_count() == 1)) {
if ((this->state_ != speaker::STATE_RUNNING) || (this->audio_ring_buffer_.use_count() != 1)) {
// Unable to write data to a running speaker, so delay the max amount of time so it can get ready
vTaskDelay(ticks_to_wait);
ticks_to_wait = 0;

View File

@ -57,6 +57,7 @@ ColorOrder = display.display_ns.enum("ColorMode")
MODELS = {
"GC9A01A": ili9xxx_ns.class_("ILI9XXXGC9A01A", ILI9XXXDisplay),
"GC9D01N": ili9xxx_ns.class_("ILI9XXXGC9D01N", ILI9XXXDisplay),
"M5STACK": ili9xxx_ns.class_("ILI9XXXM5Stack", ILI9XXXDisplay),
"M5CORE": ili9xxx_ns.class_("ILI9XXXM5CORE", ILI9XXXDisplay),
"TFT_2.4": ili9xxx_ns.class_("ILI9XXXILI9341", ILI9XXXDisplay),

View File

@ -272,6 +272,11 @@ class ILI9XXXGC9A01A : public ILI9XXXDisplay {
ILI9XXXGC9A01A() : ILI9XXXDisplay(INITCMD_GC9A01A, 240, 240) {}
};
class ILI9XXXGC9D01N : public ILI9XXXDisplay {
public:
ILI9XXXGC9D01N() : ILI9XXXDisplay(INITCMD_GC9D01N, 160, 160) {}
};
//----------- ILI9XXX_24_TFT display --------------
class ILI9XXXST7735 : public ILI9XXXDisplay {
public:

View File

@ -367,6 +367,65 @@ static const uint8_t PROGMEM INITCMD_GC9A01A[] = {
0x00 // End of list
};
static const uint8_t PROGMEM INITCMD_GC9D01N[] = {
// Enable Inter_command
0xFE, 0, // Inter Register Enable 1 (FEh)
0xEF, 0, // Inter Register Enable 2 (EFh)
// Inter_command is now enabled
0x80, 1, 0xFF,
0x81, 1, 0xFF,
0x82, 1, 0xFF,
0x83, 1, 0xFF,
0x84, 1, 0xFF,
0x85, 1, 0xFF,
0x86, 1, 0xFF,
0x87, 1, 0xFF,
0x88, 1, 0xFF,
0x89, 1, 0xFF,
0x8A, 1, 0xFF,
0x8B, 1, 0xFF,
0x8C, 1, 0xFF,
0x8D, 1, 0xFF,
0x8E, 1, 0xFF,
0x8F, 1, 0xFF,
0X3A, 1, 0x05, // COLMOD: Pixel Format Set (3Ah) MCU interface, 16 bits / pixel
0xEC, 1, 0x01, // Inversion (ECh) DINV=1+2H1V column for Dual Gate (BFh=0)
// According to datasheet Inversion (ECh) value 0x01 isn't valid, but Lilygo uses it everywhere
0x74, 7, 0x02, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x00,
0x98, 1, 0x3e,
0x99, 1, 0x3e,
0xB5, 2, 0x0D, 0x0D, // Blanking Porch Control (B5h) VFP=14 VBP=14 HBP=Off
0x60, 4, 0x38, 0x0F, 0x79, 0x67,
0x61, 4, 0x38, 0x11, 0x79, 0x67,
0x64, 6, 0x38, 0x17, 0x71, 0x5F, 0x79, 0x67,
0x65, 6, 0x38, 0x13, 0x71, 0x5B, 0x79, 0x67,
0x6A, 2, 0x00, 0x00,
0x6C, 7, 0x22, 0x02, 0x22, 0x02, 0x22, 0x22, 0x50,
0x6E, 32, 0x03, 0x03, 0x01, 0x01, 0x00, 0x00, 0x0F, 0x0F,
0x0D, 0x0D, 0x0B, 0x0B, 0x09, 0x09, 0x00, 0x00,
0x00, 0x00, 0x0A, 0x0A, 0x0C, 0x0C, 0x0E, 0x0E,
0x10, 0x10, 0x00, 0x00, 0x02, 0x02, 0x04, 0x04,
0xBF, 1, 0x01, // Dual-Single gate select (BFh) 01h = dual gate mode
0xF9, 1, 0x40,
0x9B, 5, 0x3B, 0x93, 0x33, 0x7F, 0x00,
0x7E, 1, 0x30,
0x70, 6, 0x0D, 0x02, 0x08, 0x0D, 0x02, 0x08,
0x71, 3, 0x0D, 0x02, 0x08,
0x91, 2, 0x0E, 0x09,
// Set VREG1A, VREG1B, VREG2A, VREG2B voltage
// According to datasheet set either 0xC3/0xC4 or 0xC9 only, but Lilygo sets both of them
0xC3, 5, 0x19, 0xC4, 0x19, 0xC9, 0x3C,
0xF0, 6, 0x53, 0x15, 0x0A, 0x04, 0x00, 0x3E, // SET_GAMMA1 (F0h)
0xF1, 6, 0x56, 0xA8, 0x7F, 0x33, 0x34, 0x5F, // SET_GAMMA2 (F1h)
0xF2, 6, 0x53, 0x15, 0x0A, 0x04, 0x00, 0x3A, // SET_GAMMA3 (F2h)
0xF3, 6, 0x52, 0xA4, 0x7F, 0x33, 0x34, 0xDF, // SET_GAMMA4 (F3h)
ILI9XXX_SLPOUT, 0, // Sleep Out Mode (11h)
ILI9XXX_DELAY(10),
ILI9XXX_DISPON, 0, // Display ON (29h)
ILI9XXX_DELAY(20),
0x00 // End of list
};
static const uint8_t PROGMEM INITCMD_ST7735[] = {
ILI9XXX_SWRESET, 0, // Soft reset, then delay 10ms
ILI9XXX_DELAY(10),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,876 @@
#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
if (this->presence_timeout_number_ != nullptr) {
this->pref_ = global_preferences->make_preference<float>(this->presence_timeout_number_->get_object_id_hash());
this->set_presence_timeout();
}
#endif
this->restart_and_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 (auto n : this->zone_numbers_) {
LOG_NUMBER(" ", "ZoneX1Number", n.x1);
LOG_NUMBER(" ", "ZoneY1Number", n.y1);
LOG_NUMBER(" ", "ZoneX2Number", n.x2);
LOG_NUMBER(" ", "ZoneY2Number", n.y2);
}
#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
// only one null check as all coordinates are required for a single zone
if (this->zone_numbers_[index].x1 != nullptr) {
this->zone_numbers_[index].x1->publish_state(this->zone_config_[index].x1);
this->zone_numbers_[index].y1->publish_state(this->zone_config_[index].y1);
this->zone_numbers_[index].x2->publish_state(this->zone_config_[index].x2);
this->zone_numbers_[index].y2->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(1500, [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;
#if defined(USE_BINARY_SENSOR) || defined(USE_SENSOR) || defined(USE_TEXT_SENSOR)
// Loop thru targets
for (index = 0; index < MAX_TARGETS; index++) {
#ifdef USE_SENSOR
// X
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);
}
// 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);
}
#endif
// SPEED
start = TARGET_SPEED + index * 8;
val = ld2450::decode_speed(buffer[start], buffer[start + 1]);
ts = val;
if (val) {
is_moving = true;
moving_target_count++;
}
#ifdef USE_SENSOR
sensor::Sensor *ss = this->move_speed_sensors_[index];
if (ss != nullptr) {
ss->publish_state(val);
}
#endif
// DISTANCE
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++;
}
#ifdef USE_SENSOR
sensor::Sensor *sd = this->move_distance_sensors_[index];
if (sd != nullptr) {
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
#ifdef USE_TEXT_SENSOR
// DIRECTION
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
still_target_count = target_count - moving_target_count;
#endif
#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++) {
zone_still_targets = this->count_targets_in_zone_(this->zone_config_[index], false);
zone_moving_targets = this->count_targets_in_zone_(this->zone_config_[index], true);
zone_all_targets = zone_still_targets + zone_moving_targets;
// Publish Still Target Count in Zones
sensor::Sensor *szstc = this->zone_still_target_count_sensors_[index];
if (szstc != nullptr) {
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) {
szmtc->publish_state(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
// 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_numbers_[zone].x1;
number::Number *y1sens = this->zone_numbers_[zone].y1;
number::Number *x2sens = this->zone_numbers_[zone].x2;
number::Number *y2sens = this->zone_numbers_[zone].y2;
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_numbers(uint8_t zone, number::Number *x1, number::Number *y1, number::Number *x2,
number::Number *y2) {
if (zone < MAX_ZONES) {
this->zone_numbers_[zone].x1 = x1;
this->zone_numbers_[zone].y1 = y1;
this->zone_numbers_[zone].x2 = x2;
this->zone_numbers_[zone].y2 = y2;
}
}
#endif
// Set Presence Timeout load and save from flash
#ifdef USE_NUMBER
void LD2450Component::set_presence_timeout() {
if (this->presence_timeout_number_ != nullptr) {
if (this->presence_timeout_number_->state == 0) {
float timeout = this->restore_from_flash_();
this->presence_timeout_number_->publish_state(timeout);
this->timeout_ = ld2450::convert_seconds_to_ms(timeout);
}
if (this->presence_timeout_number_->has_state()) {
this->save_to_flash_(this->presence_timeout_number_->state);
this->timeout_ = ld2450::convert_seconds_to_ms(this->presence_timeout_number_->state);
}
}
}
// Save Presence Timeout to flash
void LD2450Component::save_to_flash_(float value) { this->pref_.save(&value); }
// Load Presence Timeout from flash
float LD2450Component::restore_from_flash_() {
float value;
if (!this->pref_.load(&value)) {
value = DEFAULT_PRESENCE_TIMEOUT;
}
return value;
}
#endif
} // namespace ld2450
} // namespace esphome

View File

@ -0,0 +1,234 @@
#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;
};
#ifdef USE_NUMBER
struct ZoneOfNumbers {
number::Number *x1 = nullptr;
number::Number *y1 = nullptr;
number::Number *x2 = nullptr;
number::Number *y2 = nullptr;
};
#endif
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_numbers(uint8_t zone, number::Number *x1, number::Number *y1, number::Number *x2, number::Number *y2);
#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
ZoneOfNumbers zone_numbers_[MAX_ZONES];
#endif
#ifdef USE_SENSOR
std::vector<sensor::Sensor *> move_x_sensors_ = std::vector<sensor::Sensor *>(MAX_TARGETS);
std::vector<sensor::Sensor *> move_y_sensors_ = std::vector<sensor::Sensor *>(MAX_TARGETS);
std::vector<sensor::Sensor *> move_speed_sensors_ = std::vector<sensor::Sensor *>(MAX_TARGETS);
std::vector<sensor::Sensor *> move_angle_sensors_ = std::vector<sensor::Sensor *>(MAX_TARGETS);
std::vector<sensor::Sensor *> move_distance_sensors_ = std::vector<sensor::Sensor *>(MAX_TARGETS);
std::vector<sensor::Sensor *> move_resolution_sensors_ = std::vector<sensor::Sensor *>(MAX_TARGETS);
std::vector<sensor::Sensor *> zone_target_count_sensors_ = std::vector<sensor::Sensor *>(MAX_ZONES);
std::vector<sensor::Sensor *> zone_still_target_count_sensors_ = std::vector<sensor::Sensor *>(MAX_ZONES);
std::vector<sensor::Sensor *> zone_moving_target_count_sensors_ = std::vector<sensor::Sensor *>(MAX_ZONES);
#endif
#ifdef USE_TEXT_SENSOR
std::vector<text_sensor::TextSensor *> direction_text_sensors_ = std::vector<text_sensor::TextSensor *>(3);
#endif
};
} // namespace ld2450
} // namespace esphome

View File

@ -0,0 +1,121 @@
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 zone_num in range(MAX_ZONES):
if zone_conf := config.get(f"zone_{zone_num + 1}"):
zone_x1_config = zone_conf.get(CONF_X1)
x1 = cg.new_Pvariable(zone_x1_config[CONF_ID], zone_num)
await number.register_number(
x1, zone_x1_config, min_value=-4860, max_value=4860, step=1
)
await cg.register_parented(x1, config[CONF_LD2450_ID])
zone_y1_config = zone_conf.get(CONF_Y1)
y1 = cg.new_Pvariable(zone_y1_config[CONF_ID], zone_num)
await number.register_number(
y1, zone_y1_config, min_value=0, max_value=7560, step=1
)
await cg.register_parented(y1, config[CONF_LD2450_ID])
zone_x2_config = zone_conf.get(CONF_X2)
x2 = cg.new_Pvariable(zone_x2_config[CONF_ID], zone_num)
await number.register_number(
x2, zone_x2_config, min_value=-4860, max_value=4860, step=1
)
await cg.register_parented(x2, config[CONF_LD2450_ID])
zone_y2_config = zone_conf.get(CONF_Y2)
y2 = cg.new_Pvariable(zone_y2_config[CONF_ID], zone_num)
await number.register_number(
y2, zone_y2_config, min_value=0, max_value=7560, step=1
)
await cg.register_parented(y2, config[CONF_LD2450_ID])
cg.add(ld2450_component.set_zone_numbers(zone_num, x1, y1, x2, y2))

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -462,8 +462,6 @@ CONF_LVGL_ID = "lvgl_id"
CONF_LONG_MODE = "long_mode"
CONF_MSGBOXES = "msgboxes"
CONF_OBJ = "obj"
CONF_OFFSET_X = "offset_x"
CONF_OFFSET_Y = "offset_y"
CONF_ONE_CHECKED = "one_checked"
CONF_ONE_LINE = "one_line"
CONF_ON_PAUSE = "on_pause"

View File

@ -19,7 +19,7 @@ static bool get_glyph_dsc_cb(const lv_font_t *font, lv_font_glyph_dsc_t *dsc, ui
const auto *gd = fe->get_glyph_data(unicode_letter);
if (gd == nullptr)
return false;
dsc->adv_w = gd->offset_x + gd->width;
dsc->adv_w = gd->advance;
dsc->ofs_x = gd->offset_x;
dsc->ofs_y = fe->height - gd->height - gd->offset_y - fe->baseline;
dsc->box_w = gd->width;

View File

@ -416,37 +416,45 @@ LvglComponent::LvglComponent(std::vector<display::Display *> displays, float buf
buffer_frac_(buffer_frac),
full_refresh_(full_refresh),
resume_on_input_(resume_on_input) {
auto *display = this->displays_[0];
size_t buffer_pixels = display->get_width() * display->get_height() / this->buffer_frac_;
auto buf_bytes = buffer_pixels * LV_COLOR_DEPTH / 8;
this->rotation = display->get_rotation();
if (this->rotation != display::DISPLAY_ROTATION_0_DEGREES) {
this->rotate_buf_ = static_cast<lv_color_t *>(lv_custom_mem_alloc(buf_bytes)); // NOLINT
if (this->rotate_buf_ == nullptr)
return;
}
auto *buf = lv_custom_mem_alloc(buf_bytes); // NOLINT
if (buf == nullptr)
return;
lv_disp_draw_buf_init(&this->draw_buf_, buf, nullptr, buffer_pixels);
lv_disp_draw_buf_init(&this->draw_buf_, nullptr, nullptr, 0);
lv_disp_drv_init(&this->disp_drv_);
this->disp_drv_.draw_buf = &this->draw_buf_;
this->disp_drv_.user_data = this;
this->disp_drv_.full_refresh = this->full_refresh_;
this->disp_drv_.flush_cb = static_flush_cb;
this->disp_drv_.rounder_cb = rounder_cb;
this->disp_drv_.hor_res = (lv_coord_t) display->get_width();
this->disp_drv_.ver_res = (lv_coord_t) display->get_height();
this->disp_drv_.hor_res = 0;
this->disp_drv_.ver_res = 0;
this->disp_ = lv_disp_drv_register(&this->disp_drv_);
}
void LvglComponent::setup() {
if (this->draw_buf_.buf1 == nullptr) {
ESP_LOGCONFIG(TAG, "LVGL Setup starts");
auto *display = this->displays_[0];
auto width = display->get_width();
auto height = display->get_height();
size_t buffer_pixels = width * height / this->buffer_frac_;
auto buf_bytes = buffer_pixels * LV_COLOR_DEPTH / 8;
auto *buffer = lv_custom_mem_alloc(buf_bytes); // NOLINT
if (buffer == nullptr) {
this->mark_failed();
this->status_set_error("Memory allocation failure");
return;
}
ESP_LOGCONFIG(TAG, "LVGL Setup starts");
lv_disp_draw_buf_init(&this->draw_buf_, buffer, nullptr, buf_bytes);
this->disp_drv_.hor_res = width;
this->disp_drv_.ver_res = height;
// this->setup_driver_(display->get_width(), display->get_height());
lv_disp_drv_update(this->disp_, &this->disp_drv_);
this->rotation = display->get_rotation();
if (this->rotation != display::DISPLAY_ROTATION_0_DEGREES) {
this->rotate_buf_ = static_cast<lv_color_t *>(lv_custom_mem_alloc(this->draw_buf_.size)); // NOLINT
if (this->rotate_buf_ == nullptr) {
this->mark_failed();
this->status_set_error("Memory allocation failure");
return;
}
}
#if LV_USE_LOG
lv_log_register_print_cb([](const char *buf) {
auto next = strchr(buf, ')');
@ -458,8 +466,8 @@ void LvglComponent::setup() {
});
#endif
// Rotation will be handled by our drawing function, so reset the display rotation.
for (auto *display : this->displays_)
display->set_rotation(display::DISPLAY_ROTATION_0_DEGREES);
for (auto *disp : this->displays_)
disp->set_rotation(display::DISPLAY_ROTATION_0_DEGREES);
this->show_page(0, LV_SCR_LOAD_ANIM_NONE, 0);
lv_disp_trig_activity(this->disp_);
ESP_LOGCONFIG(TAG, "LVGL Setup complete");

View File

@ -1,11 +1,9 @@
import esphome.config_validation as cv
from esphome.const import CONF_ANGLE, CONF_MODE
from esphome.const import CONF_ANGLE, CONF_MODE, CONF_OFFSET_X, CONF_OFFSET_Y
from ..defines import (
CONF_ANTIALIAS,
CONF_MAIN,
CONF_OFFSET_X,
CONF_OFFSET_Y,
CONF_PIVOT_X,
CONF_PIVOT_Y,
CONF_SRC,

View File

@ -550,6 +550,7 @@ canbus::Error MCP2515::set_bitrate_(canbus::CanSpeed can_speed, CanClock can_clo
cfg3 = MCP_16MHZ_40KBPS_CFG3;
break;
case (canbus::CAN_50KBPS): // 50Kbps
cfg1 = MCP_16MHZ_50KBPS_CFG1;
cfg2 = MCP_16MHZ_50KBPS_CFG2;
cfg3 = MCP_16MHZ_50KBPS_CFG3;
break;

View File

@ -91,7 +91,7 @@ async def to_code(config):
add_idf_component(
name="mdns",
repo="https://github.com/espressif/esp-protocols.git",
ref="mdns-v1.5.1",
ref="mdns-v1.8.0",
path="components/mdns",
)

View File

@ -177,11 +177,15 @@ void SourceSpeaker::set_mute_state(bool mute_state) {
this->parent_->get_output_speaker()->set_mute_state(mute_state);
}
bool SourceSpeaker::get_mute_state() { return this->parent_->get_output_speaker()->get_mute_state(); }
void SourceSpeaker::set_volume(float volume) {
this->volume_ = volume;
this->parent_->get_output_speaker()->set_volume(volume);
}
float SourceSpeaker::get_volume() { return this->parent_->get_output_speaker()->get_volume(); }
size_t SourceSpeaker::process_data_from_source(TickType_t ticks_to_wait) {
if (!this->transfer_buffer_.use_count()) {
return 0;
@ -490,7 +494,8 @@ void MixerSpeaker::audio_mixer_task(void *params) {
break;
}
output_transfer_buffer->transfer_data_to_sink(pdMS_TO_TICKS(TASK_DELAY_MS));
// Never shift the data in the output transfer buffer to avoid unnecessary, slow data moves
output_transfer_buffer->transfer_data_to_sink(pdMS_TO_TICKS(TASK_DELAY_MS), false);
const uint32_t output_frames_free =
this_mixer->audio_stream_info_.value().bytes_to_frames(output_transfer_buffer->free());

View File

@ -53,9 +53,11 @@ class SourceSpeaker : public speaker::Speaker, public Component {
/// @brief Mute state changes are passed to the parent's output speaker
void set_mute_state(bool mute_state) override;
bool get_mute_state() override;
/// @brief Volume state changes are passed to the parent's output speaker
void set_volume(float volume) override;
float get_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_; }

View File

@ -1,20 +1,21 @@
from esphome import pins
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import i2c, sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_FILTER,
CONF_GAIN,
CONF_ID,
UNIT_MICROTESLA,
UNIT_CELSIUS,
STATE_CLASS_MEASUREMENT,
CONF_OVERSAMPLING,
CONF_RESOLUTION,
CONF_TEMPERATURE,
CONF_TEMPERATURE_COMPENSATION,
ICON_MAGNET,
ICON_THERMOMETER,
CONF_GAIN,
CONF_RESOLUTION,
CONF_OVERSAMPLING,
CONF_FILTER,
CONF_TEMPERATURE,
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
UNIT_MICROTESLA,
)
from esphome import pins
CODEOWNERS = ["@functionpointer"]
DEPENDENCIES = ["i2c"]
@ -26,30 +27,46 @@ MLX90393Component = mlx90393_ns.class_(
)
GAIN = {
"1X": 7,
"1_33X": 6,
"1_67X": 5,
"2X": 4,
"2_5X": 3,
"3X": 2,
"4X": 1,
"5X": 0,
"1X": 0,
"1_25X": 1,
"1_67X": 2,
"2X": 3,
"2_5X": 4,
"3X": 5,
"3_75X": 6,
"5X": 7,
}
RESOLUTION = {
"16BIT": 0,
"17BIT": 1,
"18BIT": 2,
"19BIT": 3,
"DIV_8": 3,
"DIV_4": 2,
"DIV_2": 1,
"DIV_1": 0,
}
CONF_X_AXIS = "x_axis"
CONF_Y_AXIS = "y_axis"
CONF_Z_AXIS = "z_axis"
CONF_DRDY_PIN = "drdy_pin"
CONF_HALLCONF = "hallconf"
def mlx90393_axis_schema(default_resolution: str):
def _validate(config):
if config[CONF_TEMPERATURE_COMPENSATION]:
for axis in [CONF_X_AXIS, CONF_Y_AXIS, CONF_Z_AXIS]:
if axis not in config:
continue
if (res := config[axis][CONF_RESOLUTION]) in [
"DIV_8",
"DIV_4",
]:
raise cv.Invalid(
f"{axis}: {CONF_RESOLUTION} cannot be {res} with {CONF_TEMPERATURE_COMPENSATION} enabled"
)
return config
def mlx90393_axis_schema():
return sensor.sensor_schema(
unit_of_measurement=UNIT_MICROTESLA,
accuracy_decimals=0,
@ -58,7 +75,7 @@ def mlx90393_axis_schema(default_resolution: str):
).extend(
cv.Schema(
{
cv.Optional(CONF_RESOLUTION, default=default_resolution): cv.enum(
cv.Optional(CONF_RESOLUTION, default="DIV_4"): cv.enum(
RESOLUTION, upper=True, space="_"
)
}
@ -66,19 +83,19 @@ def mlx90393_axis_schema(default_resolution: str):
)
CONFIG_SCHEMA = (
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(MLX90393Component),
cv.Optional(CONF_GAIN, default="2_5X"): cv.enum(
GAIN, upper=True, space="_"
),
cv.Optional(CONF_GAIN, default="1X"): cv.enum(GAIN, upper=True, space="_"),
cv.Optional(CONF_DRDY_PIN): pins.gpio_input_pin_schema,
cv.Optional(CONF_OVERSAMPLING, default=2): cv.int_range(min=0, max=3),
cv.Optional(CONF_OVERSAMPLING, default=0): cv.int_range(min=0, max=3),
cv.Optional(CONF_FILTER, default=6): cv.int_range(min=0, max=7),
cv.Optional(CONF_X_AXIS): mlx90393_axis_schema("19BIT"),
cv.Optional(CONF_Y_AXIS): mlx90393_axis_schema("19BIT"),
cv.Optional(CONF_Z_AXIS): mlx90393_axis_schema("16BIT"),
cv.Optional(CONF_X_AXIS): mlx90393_axis_schema(),
cv.Optional(CONF_Y_AXIS): mlx90393_axis_schema(),
cv.Optional(CONF_Z_AXIS): mlx90393_axis_schema(),
cv.Optional(CONF_TEMPERATURE_COMPENSATION, default=False): bool,
cv.Optional(CONF_HALLCONF, default=0xC): cv.one_of(0xC, 0x0),
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=1,
@ -96,7 +113,8 @@ CONFIG_SCHEMA = (
},
)
.extend(cv.polling_component_schema("60s"))
.extend(i2c.i2c_device_schema(0x0C))
.extend(i2c.i2c_device_schema(0x0C)),
_validate,
)
@ -111,6 +129,8 @@ async def to_code(config):
cg.add(var.set_gain(GAIN[config[CONF_GAIN]]))
cg.add(var.set_oversampling(config[CONF_OVERSAMPLING]))
cg.add(var.set_filter(config[CONF_FILTER]))
cg.add(var.set_temperature_compensation(config[CONF_TEMPERATURE_COMPENSATION]))
cg.add(var.set_hallconf(config[CONF_HALLCONF]))
if CONF_X_AXIS in config:
sens = await sensor.new_sensor(config[CONF_X_AXIS])

View File

@ -43,6 +43,10 @@ void MLX90393Cls::setup() {
this->mlx_.setDigitalFiltering(this->filter_);
this->mlx_.setTemperatureOverSampling(this->temperature_oversampling_);
this->mlx_.setTemperatureCompensation(this->temperature_compensation_);
this->mlx_.setHallConf(this->hallconf_);
}
void MLX90393Cls::dump_config() {

View File

@ -29,7 +29,10 @@ class MLX90393Cls : public PollingComponent, public i2c::I2CDevice, public MLX90
void set_resolution(uint8_t xyz, uint8_t res) { resolutions_[xyz] = res; }
void set_filter(uint8_t filter) { filter_ = filter; }
void set_gain(uint8_t gain_sel) { gain_ = gain_sel; }
void set_temperature_compensation(bool temperature_compensation) {
temperature_compensation_ = temperature_compensation;
}
void set_hallconf(uint8_t hallconf) { hallconf_ = hallconf; }
// overrides for MLX library
// disable lint because it keeps suggesting const uint8_t *response.
@ -49,9 +52,11 @@ class MLX90393Cls : public PollingComponent, public i2c::I2CDevice, public MLX90
sensor::Sensor *t_sensor_{nullptr};
uint8_t gain_;
uint8_t oversampling_;
uint8_t temperature_oversampling_ = 0;
uint8_t temperature_oversampling_{0};
uint8_t filter_;
uint8_t resolutions_[3] = {0};
uint8_t resolutions_[3]{0};
bool temperature_compensation_{false};
uint8_t hallconf_{0xC};
GPIOPin *drdy_pin_{nullptr};
};

View File

@ -36,6 +36,7 @@ from esphome.const import (
CONF_PAYLOAD_AVAILABLE,
CONF_PAYLOAD_NOT_AVAILABLE,
CONF_PORT,
CONF_PUBLISH_NAN_AS_NONE,
CONF_QOS,
CONF_REBOOT_TIMEOUT,
CONF_RETAIN,
@ -49,7 +50,6 @@ from esphome.const import (
CONF_USE_ABBREVIATIONS,
CONF_USERNAME,
CONF_WILL_MESSAGE,
CONF_PUBLISH_NAN_AS_NONE,
PLATFORM_BK72XX,
PLATFORM_ESP32,
PLATFORM_ESP8266,

View File

@ -0,0 +1,189 @@
from esphome import automation
import esphome.codegen as cg
from esphome.components import i2c
import esphome.config_validation as cv
from esphome.const import (
CONF_CALIBRATION,
CONF_ID,
CONF_MIRROR_X,
CONF_MIRROR_Y,
CONF_OFFSET_X,
CONF_OFFSET_Y,
CONF_OFFSET_Z,
CONF_RANGE,
CONF_RESOLUTION,
CONF_SWAP_XY,
CONF_TRANSFORM,
CONF_TYPE,
)
CODEOWNERS = ["@latonita"]
DEPENDENCIES = ["i2c"]
MULTI_CONF = True
CONF_MSA3XX_ID = "msa3xx_id"
CONF_MIRROR_Z = "mirror_z"
CONF_ON_ACTIVE = "on_active"
CONF_ON_DOUBLE_TAP = "on_double_tap"
CONF_ON_FREEFALL = "on_freefall"
CONF_ON_ORIENTATION = "on_orientation"
CONF_ON_TAP = "on_tap"
MODEL_MSA301 = "MSA301"
MODEL_MSA311 = "MSA311"
msa3xx_ns = cg.esphome_ns.namespace("msa3xx")
MSA3xxComponent = msa3xx_ns.class_(
"MSA3xxComponent", cg.PollingComponent, i2c.I2CDevice
)
MSAModels = msa3xx_ns.enum("Model", True)
MSA_MODELS = {
MODEL_MSA301: MSAModels.MSA301,
MODEL_MSA311: MSAModels.MSA311,
}
MSARange = msa3xx_ns.enum("Range", True)
MSA_RANGES = {
"2G": MSARange.RANGE_2G,
"4G": MSARange.RANGE_4G,
"8G": MSARange.RANGE_8G,
"16G": MSARange.RANGE_16G,
}
MSAResolution = msa3xx_ns.enum("Resolution", True)
RESOLUTIONS_MSA301 = {
14: MSAResolution.RES_14BIT,
12: MSAResolution.RES_12BIT,
10: MSAResolution.RES_10BIT,
8: MSAResolution.RES_8BIT,
}
RESOLUTIONS_MSA311 = {
12: MSAResolution.RES_12BIT,
}
_COMMON_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(MSA3xxComponent),
cv.Optional(CONF_RANGE, default="2G"): cv.enum(MSA_RANGES, upper=True),
cv.Optional(CONF_CALIBRATION): cv.Schema(
{
cv.Optional(CONF_OFFSET_X, default=0): cv.float_range(
min=-4.5, max=4.5
),
cv.Optional(CONF_OFFSET_Y, default=0): cv.float_range(
min=-4.5, max=4.5
),
cv.Optional(CONF_OFFSET_Z, default=0): cv.float_range(
min=-4.5, max=4.5
),
}
),
cv.Optional(CONF_TRANSFORM): cv.Schema(
{
cv.Optional(CONF_MIRROR_X, default=False): cv.boolean,
cv.Optional(CONF_MIRROR_Y, default=False): cv.boolean,
cv.Optional(CONF_MIRROR_Z, default=False): cv.boolean,
cv.Optional(CONF_SWAP_XY, default=False): cv.boolean,
}
),
cv.Optional(CONF_ON_ACTIVE): automation.validate_automation(single=True),
cv.Optional(CONF_ON_TAP): automation.validate_automation(single=True),
cv.Optional(CONF_ON_DOUBLE_TAP): automation.validate_automation(single=True),
cv.Optional(CONF_ON_FREEFALL): automation.validate_automation(single=True),
cv.Optional(CONF_ON_ORIENTATION): automation.validate_automation(single=True),
}
).extend(cv.polling_component_schema("10s"))
CONFIG_SCHEMA = cv.typed_schema(
{
MODEL_MSA301: _COMMON_SCHEMA.extend(
{
cv.Optional(CONF_RESOLUTION, default=14): cv.enum(RESOLUTIONS_MSA301),
}
).extend(i2c.i2c_device_schema(0x26)),
MODEL_MSA311: _COMMON_SCHEMA.extend(
{
cv.Optional(CONF_RESOLUTION, default=12): cv.enum(RESOLUTIONS_MSA311),
}
).extend(i2c.i2c_device_schema(0x62)),
},
upper=True,
enum=MSA_MODELS,
)
MSA_SENSOR_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_MSA3XX_ID): cv.use_id(MSA3xxComponent),
}
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
cg.add(var.set_model(config[CONF_TYPE]))
cg.add(var.set_range(MSA_RANGES[config[CONF_RANGE]]))
cg.add(var.set_resolution(RESOLUTIONS_MSA301[config[CONF_RESOLUTION]]))
if transform := config.get(CONF_TRANSFORM):
cg.add(
var.set_transform(
transform[CONF_MIRROR_X],
transform[CONF_MIRROR_Y],
transform[CONF_MIRROR_Z],
transform[CONF_SWAP_XY],
)
)
if calibration_config := config.get(CONF_CALIBRATION):
cg.add(
var.set_offset(
calibration_config[CONF_OFFSET_X],
calibration_config[CONF_OFFSET_Y],
calibration_config[CONF_OFFSET_Z],
)
)
# Triggers secton
if CONF_ON_ORIENTATION in config:
await automation.build_automation(
var.get_orientation_trigger(),
[],
config[CONF_ON_ORIENTATION],
)
if CONF_ON_TAP in config:
await automation.build_automation(
var.get_tap_trigger(),
[],
config[CONF_ON_TAP],
)
if CONF_ON_DOUBLE_TAP in config:
await automation.build_automation(
var.get_double_tap_trigger(),
[],
config[CONF_ON_DOUBLE_TAP],
)
if CONF_ON_ACTIVE in config:
await automation.build_automation(
var.get_active_trigger(),
[],
config[CONF_ON_ACTIVE],
)
if CONF_ON_FREEFALL in config:
await automation.build_automation(
var.get_freefall_trigger(),
[],
config[CONF_ON_FREEFALL],
)

View File

@ -0,0 +1,40 @@
import esphome.codegen as cg
from esphome.components import binary_sensor
import esphome.config_validation as cv
from esphome.const import CONF_ACTIVE, CONF_NAME, DEVICE_CLASS_VIBRATION, ICON_VIBRATE
from . import CONF_MSA3XX_ID, MSA_SENSOR_SCHEMA
CODEOWNERS = ["@latonita"]
DEPENDENCIES = ["msa3xx"]
CONF_TAP = "tap"
CONF_DOUBLE_TAP = "double_tap"
ICON_TAP = "mdi:gesture-tap"
ICON_DOUBLE_TAP = "mdi:gesture-double-tap"
EVENT_SENSORS = (CONF_TAP, CONF_DOUBLE_TAP, CONF_ACTIVE)
ICONS = (ICON_TAP, ICON_DOUBLE_TAP, ICON_VIBRATE)
CONFIG_SCHEMA = MSA_SENSOR_SCHEMA.extend(
{
cv.Optional(event): cv.maybe_simple_value(
binary_sensor.binary_sensor_schema(
device_class=DEVICE_CLASS_VIBRATION,
icon=icon,
),
key=CONF_NAME,
)
for event, icon in zip(EVENT_SENSORS, ICONS)
}
)
async def to_code(config):
hub = await cg.get_variable(config[CONF_MSA3XX_ID])
for sensor in EVENT_SENSORS:
if sensor in config:
sens = await binary_sensor.new_binary_sensor(config[sensor])
cg.add(getattr(hub, f"set_{sensor}_binary_sensor")(sens))

View File

@ -0,0 +1,417 @@
#include "msa3xx.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
namespace esphome {
namespace msa3xx {
static const char *const TAG = "msa3xx";
const uint8_t MSA_3XX_PART_ID = 0x13;
const float GRAVITY_EARTH = 9.80665f;
const float LSB_COEFF = 1000.0f / (GRAVITY_EARTH * 3.9); // LSB to 1 LSB = 3.9mg = 0.0039g
const float G_OFFSET_MIN = -4.5f; // -127...127 LSB = +- 0.4953g = +- 4.857 m/s^2 => +- 4.5 for the safe
const float G_OFFSET_MAX = 4.5f; // -127...127 LSB = +- 0.4953g = +- 4.857 m/s^2 => +- 4.5 for the safe
const uint8_t RESOLUTION[] = {14, 12, 10, 8};
const uint32_t TAP_COOLDOWN_MS = 500;
const uint32_t DOUBLE_TAP_COOLDOWN_MS = 500;
const uint32_t ACTIVITY_COOLDOWN_MS = 500;
const char *model_to_string(Model model) {
switch (model) {
case Model::MSA301:
return "MSA301";
case Model::MSA311:
return "MSA311";
default:
return "Unknown";
}
}
const char *power_mode_to_string(PowerMode power_mode) {
switch (power_mode) {
case PowerMode::NORMAL:
return "Normal";
case PowerMode::LOW_POWER:
return "Low Power";
case PowerMode::SUSPEND:
return "Suspend";
default:
return "Unknown";
}
}
const char *res_to_string(Resolution resolution) {
switch (resolution) {
case Resolution::RES_14BIT:
return "14-bit";
case Resolution::RES_12BIT:
return "12-bit";
case Resolution::RES_10BIT:
return "10-bit";
case Resolution::RES_8BIT:
return "8-bit";
default:
return "Unknown";
}
}
const char *range_to_string(Range range) {
switch (range) {
case Range::RANGE_2G:
return "±2g";
case Range::RANGE_4G:
return "±4g";
case Range::RANGE_8G:
return "±8g";
case Range::RANGE_16G:
return "±16g";
default:
return "Unknown";
}
}
const char *bandwidth_to_string(Bandwidth bandwidth) {
switch (bandwidth) {
case Bandwidth::BW_1_95HZ:
return "1.95 Hz";
case Bandwidth::BW_3_9HZ:
return "3.9 Hz";
case Bandwidth::BW_7_81HZ:
return "7.81 Hz";
case Bandwidth::BW_15_63HZ:
return "15.63 Hz";
case Bandwidth::BW_31_25HZ:
return "31.25 Hz";
case Bandwidth::BW_62_5HZ:
return "62.5 Hz";
case Bandwidth::BW_125HZ:
return "125 Hz";
case Bandwidth::BW_250HZ:
return "250 Hz";
case Bandwidth::BW_500HZ:
return "500 Hz";
default:
return "Unknown";
}
}
const char *orientation_xy_to_string(OrientationXY orientation) {
switch (orientation) {
case OrientationXY::PORTRAIT_UPRIGHT:
return "Portrait Upright";
case OrientationXY::PORTRAIT_UPSIDE_DOWN:
return "Portrait Upside Down";
case OrientationXY::LANDSCAPE_LEFT:
return "Landscape Left";
case OrientationXY::LANDSCAPE_RIGHT:
return "Landscape Right";
default:
return "Unknown";
}
}
const char *orientation_z_to_string(bool orientation) { return orientation ? "Downwards looking" : "Upwards looking"; }
void MSA3xxComponent::setup() {
ESP_LOGCONFIG(TAG, "Setting up MSA3xx...");
uint8_t part_id{0xff};
if (!this->read_byte(static_cast<uint8_t>(RegisterMap::PART_ID), &part_id) || (part_id != MSA_3XX_PART_ID)) {
ESP_LOGE(TAG, "Part ID is wrong or missing. Got 0x%02X", part_id);
this->mark_failed();
return;
}
// Resolution LSB/g
// Range : MSA301 : MSA311
// S2g : 1024 (2^10) : 4096 (2^12)
// S4g : 512 (2^9) : 2048 (2^11)
// S8g : 256 (2^8) : 1024 (2^10)
// S16g : 128 (2^7) : 512 (2^9)
if (this->model_ == Model::MSA301) {
this->device_params_.accel_data_width = 14;
this->device_params_.scale_factor_exp = static_cast<uint8_t>(this->range_) - 12;
} else if (this->model_ == Model::MSA311) {
this->device_params_.accel_data_width = 12;
this->device_params_.scale_factor_exp = static_cast<uint8_t>(this->range_) - 10;
} else {
ESP_LOGE(TAG, "Unknown model");
this->mark_failed();
return;
}
this->setup_odr_(this->data_rate_);
this->setup_power_mode_bandwidth_(this->power_mode_, this->bandwidth_);
this->setup_range_resolution_(this->range_, this->resolution_); // 2g...16g, 14...8 bit
this->setup_offset_(this->offset_x_, this->offset_y_, this->offset_z_); // calibration offsets
this->write_byte(static_cast<uint8_t>(RegisterMap::TAP_DURATION), 0b11000100); // set tap duration 250ms
this->write_byte(static_cast<uint8_t>(RegisterMap::SWAP_POLARITY), this->swap_.raw); // set axes polarity
this->write_byte(static_cast<uint8_t>(RegisterMap::INT_SET_0), 0b01110111); // enable all interrupts
this->write_byte(static_cast<uint8_t>(RegisterMap::INT_SET_1), 0b00011000); // including orientation
}
void MSA3xxComponent::dump_config() {
ESP_LOGCONFIG(TAG, "MSA3xx:");
LOG_I2C_DEVICE(this);
if (this->is_failed()) {
ESP_LOGE(TAG, "Communication with MSA3xx failed!");
}
ESP_LOGCONFIG(TAG, " Model: %s", model_to_string(this->model_));
ESP_LOGCONFIG(TAG, " Power Mode: %s", power_mode_to_string(this->power_mode_));
ESP_LOGCONFIG(TAG, " Bandwidth: %s", bandwidth_to_string(this->bandwidth_));
ESP_LOGCONFIG(TAG, " Range: %s", range_to_string(this->range_));
ESP_LOGCONFIG(TAG, " Resolution: %s", res_to_string(this->resolution_));
ESP_LOGCONFIG(TAG, " Offsets: {%.3f m/s², %.3f m/s², %.3f m/s²}", this->offset_x_, this->offset_y_, this->offset_z_);
ESP_LOGCONFIG(TAG, " Transform: {mirror_x=%s, mirror_y=%s, mirror_z=%s, swap_xy=%s}", YESNO(this->swap_.x_polarity),
YESNO(this->swap_.y_polarity), YESNO(this->swap_.z_polarity), YESNO(this->swap_.x_y_swap));
LOG_UPDATE_INTERVAL(this);
#ifdef USE_BINARY_SENSOR
LOG_BINARY_SENSOR(" ", "Tap", this->tap_binary_sensor_);
LOG_BINARY_SENSOR(" ", "Double Tap", this->double_tap_binary_sensor_);
LOG_BINARY_SENSOR(" ", "Active", this->active_binary_sensor_);
#endif
#ifdef USE_SENSOR
LOG_SENSOR(" ", "Acceleration X", this->acceleration_x_sensor_);
LOG_SENSOR(" ", "Acceleration Y", this->acceleration_y_sensor_);
LOG_SENSOR(" ", "Acceleration Z", this->acceleration_z_sensor_);
#endif
#ifdef USE_TEXT_SENSOR
LOG_TEXT_SENSOR(" ", "Orientation XY", this->orientation_xy_text_sensor_);
LOG_TEXT_SENSOR(" ", "Orientation Z", this->orientation_z_text_sensor_);
#endif
}
bool MSA3xxComponent::read_data_() {
uint8_t accel_data[6];
if (!this->read_bytes(static_cast<uint8_t>(RegisterMap::ACC_X_LSB), accel_data, 6)) {
return false;
}
auto raw_to_x_bit = [](uint16_t lsb, uint16_t msb, uint8_t data_bits) -> uint16_t {
return ((msb << 8) | lsb) >> (16 - data_bits);
};
auto lpf = [](float new_value, float old_value, float alpha = 0.5f) {
return alpha * new_value + (1.0f - alpha) * old_value;
};
this->data_.lsb_x =
this->twos_complement_(raw_to_x_bit(accel_data[0], accel_data[1], this->device_params_.accel_data_width),
this->device_params_.accel_data_width);
this->data_.lsb_y =
this->twos_complement_(raw_to_x_bit(accel_data[2], accel_data[3], this->device_params_.accel_data_width),
this->device_params_.accel_data_width);
this->data_.lsb_z =
this->twos_complement_(raw_to_x_bit(accel_data[4], accel_data[5], this->device_params_.accel_data_width),
this->device_params_.accel_data_width);
this->data_.x = lpf(ldexp(this->data_.lsb_x, this->device_params_.scale_factor_exp) * GRAVITY_EARTH, this->data_.x);
this->data_.y = lpf(ldexp(this->data_.lsb_y, this->device_params_.scale_factor_exp) * GRAVITY_EARTH, this->data_.y);
this->data_.z = lpf(ldexp(this->data_.lsb_z, this->device_params_.scale_factor_exp) * GRAVITY_EARTH, this->data_.z);
return true;
}
bool MSA3xxComponent::read_motion_status_() {
if (!this->read_byte(static_cast<uint8_t>(RegisterMap::MOTION_INTERRUPT), &this->status_.motion_int.raw)) {
return false;
}
if (!this->read_byte(static_cast<uint8_t>(RegisterMap::ORIENTATION_STATUS), &this->status_.orientation.raw)) {
return false;
}
return true;
}
void MSA3xxComponent::loop() {
if (!this->is_ready()) {
return;
}
RegMotionInterrupt old_motion_int = this->status_.motion_int;
if (!this->read_data_() || !this->read_motion_status_()) {
this->status_set_warning();
return;
}
this->process_motions_(old_motion_int);
}
void MSA3xxComponent::update() {
ESP_LOGV(TAG, "Updating MSA3xx...");
if (!this->is_ready()) {
ESP_LOGV(TAG, "Component MSA3xx not ready for update");
return;
}
ESP_LOGV(TAG, "Acceleration: {x = %+1.3f m/s², y = %+1.3f m/s², z = %+1.3f m/s²}; ", this->data_.x, this->data_.y,
this->data_.z);
ESP_LOGV(TAG, "Orientation: {XY = %s, Z = %s}", orientation_xy_to_string(this->status_.orientation.orient_xy),
orientation_z_to_string(this->status_.orientation.orient_z));
#ifdef USE_SENSOR
if (this->acceleration_x_sensor_ != nullptr)
this->acceleration_x_sensor_->publish_state(this->data_.x);
if (this->acceleration_y_sensor_ != nullptr)
this->acceleration_y_sensor_->publish_state(this->data_.y);
if (this->acceleration_z_sensor_ != nullptr)
this->acceleration_z_sensor_->publish_state(this->data_.z);
#endif
#ifdef USE_TEXT_SENSOR
if (this->orientation_xy_text_sensor_ != nullptr &&
(this->status_.orientation.orient_xy != this->status_.orientation_old.orient_xy ||
this->status_.never_published)) {
this->orientation_xy_text_sensor_->publish_state(orientation_xy_to_string(this->status_.orientation.orient_xy));
}
if (this->orientation_z_text_sensor_ != nullptr &&
(this->status_.orientation.orient_z != this->status_.orientation_old.orient_z || this->status_.never_published)) {
this->orientation_z_text_sensor_->publish_state(orientation_z_to_string(this->status_.orientation.orient_z));
}
this->status_.orientation_old = this->status_.orientation;
#endif
this->status_.never_published = false;
this->status_clear_warning();
}
float MSA3xxComponent::get_setup_priority() const { return setup_priority::DATA; }
void MSA3xxComponent::set_offset(float offset_x, float offset_y, float offset_z) {
this->offset_x_ = offset_x;
this->offset_y_ = offset_y;
this->offset_z_ = offset_z;
}
void MSA3xxComponent::set_transform(bool mirror_x, bool mirror_y, bool mirror_z, bool swap_xy) {
this->swap_.x_polarity = mirror_x;
this->swap_.y_polarity = mirror_y;
this->swap_.z_polarity = mirror_z;
this->swap_.x_y_swap = swap_xy;
}
void MSA3xxComponent::setup_odr_(DataRate rate) {
RegOutputDataRate reg_odr;
auto reg = this->read_byte(static_cast<uint8_t>(RegisterMap::ODR));
if (reg.has_value()) {
reg_odr.raw = reg.value();
} else {
reg_odr.raw = 0x0F; // defaut from datasheet
}
reg_odr.x_axis_disable = false;
reg_odr.y_axis_disable = false;
reg_odr.z_axis_disable = false;
reg_odr.odr = rate;
this->write_byte(static_cast<uint8_t>(RegisterMap::ODR), reg_odr.raw);
}
void MSA3xxComponent::setup_power_mode_bandwidth_(PowerMode power_mode, Bandwidth bandwidth) {
// 0x11 POWER_MODE_BANDWIDTH
auto reg = this->read_byte(static_cast<uint8_t>(RegisterMap::POWER_MODE_BANDWIDTH));
RegPowerModeBandwidth power_mode_bandwidth;
if (reg.has_value()) {
power_mode_bandwidth.raw = reg.value();
} else {
power_mode_bandwidth.raw = 0xde; // defaut from datasheet
}
power_mode_bandwidth.power_mode = power_mode;
power_mode_bandwidth.low_power_bandwidth = bandwidth;
this->write_byte(static_cast<uint8_t>(RegisterMap::POWER_MODE_BANDWIDTH), power_mode_bandwidth.raw);
}
void MSA3xxComponent::setup_range_resolution_(Range range, Resolution resolution) {
RegRangeResolution reg;
reg.raw = this->read_byte(static_cast<uint8_t>(RegisterMap::RANGE_RESOLUTION)).value_or(0x00);
reg.range = range;
if (this->model_ == Model::MSA301) {
reg.resolution = resolution;
}
this->write_byte(static_cast<uint8_t>(RegisterMap::RANGE_RESOLUTION), reg.raw);
}
void MSA3xxComponent::setup_offset_(float offset_x, float offset_y, float offset_z) {
uint8_t offset[3];
auto offset_g_to_lsb = [](float accel) -> int8_t {
float acccel_clamped = clamp(accel, G_OFFSET_MIN, G_OFFSET_MAX);
return static_cast<int8_t>(acccel_clamped * LSB_COEFF);
};
offset[0] = offset_g_to_lsb(offset_x);
offset[1] = offset_g_to_lsb(offset_y);
offset[2] = offset_g_to_lsb(offset_z);
ESP_LOGV(TAG, "Offset (%.3f, %.3f, %.3f)=>LSB(%d, %d, %d)", offset_x, offset_y, offset_z, offset[0], offset[1],
offset[2]);
this->write_bytes(static_cast<uint8_t>(RegisterMap::OFFSET_COMP_X), (uint8_t *) &offset, 3);
}
int64_t MSA3xxComponent::twos_complement_(uint64_t value, uint8_t bits) {
if (value > (1ULL << (bits - 1))) {
return (int64_t) (value - (1ULL << bits));
} else {
return (int64_t) value;
}
}
void binary_event_debounce(bool state, bool old_state, uint32_t now, uint32_t &last_ms, Trigger<> &trigger,
uint32_t cooldown_ms, void *bs, const char *desc) {
if (state && now - last_ms > cooldown_ms) {
ESP_LOGV(TAG, "%s detected", desc);
trigger.trigger();
last_ms = now;
#ifdef USE_BINARY_SENSOR
if (bs != nullptr) {
static_cast<binary_sensor::BinarySensor *>(bs)->publish_state(true);
}
#endif
} else if (!state && now - last_ms > cooldown_ms && bs != nullptr) {
#ifdef USE_BINARY_SENSOR
static_cast<binary_sensor::BinarySensor *>(bs)->publish_state(false);
#endif
}
}
#ifdef USE_BINARY_SENSOR
#define BS_OPTIONAL_PTR(x) ((void *) (x))
#else
#define BS_OPTIONAL_PTR(x) (nullptr)
#endif
void MSA3xxComponent::process_motions_(RegMotionInterrupt old) {
uint32_t now = millis();
binary_event_debounce(this->status_.motion_int.single_tap_interrupt, old.single_tap_interrupt, now,
this->status_.last_tap_ms, this->tap_trigger_, TAP_COOLDOWN_MS,
BS_OPTIONAL_PTR(this->tap_binary_sensor_), "Tap");
binary_event_debounce(this->status_.motion_int.double_tap_interrupt, old.double_tap_interrupt, now,
this->status_.last_double_tap_ms, this->double_tap_trigger_, DOUBLE_TAP_COOLDOWN_MS,
BS_OPTIONAL_PTR(this->double_tap_binary_sensor_), "Double Tap");
binary_event_debounce(this->status_.motion_int.active_interrupt, old.active_interrupt, now,
this->status_.last_action_ms, this->active_trigger_, ACTIVITY_COOLDOWN_MS,
BS_OPTIONAL_PTR(this->active_binary_sensor_), "Activity");
if (this->status_.motion_int.orientation_interrupt) {
ESP_LOGVV(TAG, "Orientation changed");
this->orientation_trigger_.trigger();
}
}
} // namespace msa3xx
} // namespace esphome

View File

@ -0,0 +1,311 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/core/automation.h"
#ifdef USE_BINARY_SENSOR
#include "esphome/components/binary_sensor/binary_sensor.h"
#endif
#ifdef USE_SENSOR
#include "esphome/components/sensor/sensor.h"
#endif
#ifdef USE_TEXT_SENSOR
#include "esphome/components/text_sensor/text_sensor.h"
#endif
namespace esphome {
namespace msa3xx {
// Combined register map of MSA301 and MSA311
// Differences
// What | MSA301 | MSA11 |
// - Resolution | 14-bit | 12-bit |
//
// I2c address
enum class Model : uint8_t {
MSA301 = 0x26,
MSA311 = 0x62,
};
// Combined MSA301 and MSA311 register map
enum class RegisterMap : uint8_t {
SOFT_RESET = 0x00,
PART_ID = 0x01,
ACC_X_LSB = 0x02,
ACC_X_MSB = 0x03,
ACC_Y_LSB = 0x04,
ACC_Y_MSB = 0x05,
ACC_Z_LSB = 0x06,
ACC_Z_MSB = 0x07,
MOTION_INTERRUPT = 0x09,
DATA_INTERRUPT = 0x0A,
TAP_ACTIVE_STATUS = 0x0B,
ORIENTATION_STATUS = 0x0C,
RESOLUTION_RANGE_CONFIG = 0x0D,
RANGE_RESOLUTION = 0x0F,
ODR = 0x10,
POWER_MODE_BANDWIDTH = 0x11,
SWAP_POLARITY = 0x12,
INT_SET_0 = 0x16,
INT_SET_1 = 0x17,
INT_MAP_0 = 0x19,
INT_MAP_1 = 0x1A,
INT_CONFIG = 0x20,
INT_LATCH = 0x21,
FREEFALL_DURATION = 0x22,
FREEFALL_THRESHOLD = 0x23,
FREEFALL_HYSTERESIS = 0x24,
ACTIVE_DURATION = 0x27,
ACTIVE_THRESHOLD = 0x28,
TAP_DURATION = 0x2A,
TAP_THRESHOLD = 0x2B,
ORIENTATION_CONFIG = 0x2C,
Z_BLOCK = 0x2D,
OFFSET_COMP_X = 0x38,
OFFSET_COMP_Y = 0x39,
OFFSET_COMP_Z = 0x3A,
};
enum class Range : uint8_t {
RANGE_2G = 0b00,
RANGE_4G = 0b01,
RANGE_8G = 0b10,
RANGE_16G = 0b11,
};
enum class Resolution : uint8_t {
RES_14BIT = 0b00,
RES_12BIT = 0b01,
RES_10BIT = 0b10,
RES_8BIT = 0b11,
};
enum class PowerMode : uint8_t {
NORMAL = 0b00,
LOW_POWER = 0b01,
SUSPEND = 0b11,
};
enum class Bandwidth : uint8_t {
BW_1_95HZ = 0b0000,
BW_3_9HZ = 0b0011,
BW_7_81HZ = 0b0100,
BW_15_63HZ = 0b0101,
BW_31_25HZ = 0b0110,
BW_62_5HZ = 0b0111,
BW_125HZ = 0b1000,
BW_250HZ = 0b1001,
BW_500HZ = 0b1010,
};
enum class DataRate : uint8_t {
ODR_1HZ = 0b0000, // not available in normal mode
ODR_1_95HZ = 0b0001, // not available in normal mode
ODR_3_9HZ = 0b0010,
ODR_7_81HZ = 0b0011,
ODR_15_63HZ = 0b0100,
ODR_31_25HZ = 0b0101,
ODR_62_5HZ = 0b0110,
ODR_125HZ = 0b0111,
ODR_250HZ = 0b1000,
ODR_500HZ = 0b1001, // not available in low power mode
ODR_1000HZ = 0b1010, // not available in low power mode
};
enum class OrientationXY : uint8_t {
PORTRAIT_UPRIGHT = 0b00,
PORTRAIT_UPSIDE_DOWN = 0b01,
LANDSCAPE_LEFT = 0b10,
LANDSCAPE_RIGHT = 0b11,
};
union Orientation {
struct {
OrientationXY xy : 2;
bool z : 1;
uint8_t reserved : 5;
} __attribute__((packed));
uint8_t raw;
};
// 0x09
union RegMotionInterrupt {
struct {
bool freefall_interrupt : 1;
bool reserved_1 : 1;
bool active_interrupt : 1;
bool reserved_3 : 1;
bool double_tap_interrupt : 1;
bool single_tap_interrupt : 1;
bool orientation_interrupt : 1;
bool reserved_7 : 1;
} __attribute__((packed));
uint8_t raw;
};
// 0x0C
union RegOrientationStatus {
struct {
uint8_t reserved_0_3 : 4;
OrientationXY orient_xy : 2;
bool orient_z : 1;
uint8_t reserved_7 : 1;
} __attribute__((packed));
uint8_t raw{0x00};
};
// 0x0f
union RegRangeResolution {
struct {
Range range : 2;
Resolution resolution : 2;
uint8_t reserved_2 : 4;
} __attribute__((packed));
uint8_t raw{0x00};
};
// 0x10
union RegOutputDataRate {
struct {
DataRate odr : 4;
uint8_t reserved_4 : 1;
bool z_axis_disable : 1;
bool y_axis_disable : 1;
bool x_axis_disable : 1;
} __attribute__((packed));
uint8_t raw{0xde};
};
// 0x11
union RegPowerModeBandwidth {
struct {
uint8_t reserved_0 : 1;
Bandwidth low_power_bandwidth : 4;
uint8_t reserved_5 : 1;
PowerMode power_mode : 2;
} __attribute__((packed));
uint8_t raw{0xde};
};
// 0x12
union RegSwapPolarity {
struct {
bool x_y_swap : 1;
bool z_polarity : 1;
bool y_polarity : 1;
bool x_polarity : 1;
uint8_t reserved : 4;
} __attribute__((packed));
uint8_t raw{0};
};
// 0x2a
union RegTapDuration {
struct {
uint8_t duration : 3;
uint8_t reserved : 3;
bool tap_shock : 1;
bool tap_quiet : 1;
} __attribute__((packed));
uint8_t raw{0x04};
};
class MSA3xxComponent : public PollingComponent, public i2c::I2CDevice {
public:
void setup() override;
void dump_config() override;
void loop() override;
void update() override;
float get_setup_priority() const override;
void set_model(Model model) { this->model_ = model; }
void set_offset(float offset_x, float offset_y, float offset_z);
void set_range(Range range) { this->range_ = range; }
void set_bandwidth(Bandwidth bandwidth) { this->bandwidth_ = bandwidth; }
void set_resolution(Resolution resolution) { this->resolution_ = resolution; }
void set_transform(bool mirror_x, bool mirror_y, bool mirror_z, bool swap_xy);
#ifdef USE_BINARY_SENSOR
SUB_BINARY_SENSOR(tap)
SUB_BINARY_SENSOR(double_tap)
SUB_BINARY_SENSOR(active)
#endif
#ifdef USE_SENSOR
SUB_SENSOR(acceleration_x)
SUB_SENSOR(acceleration_y)
SUB_SENSOR(acceleration_z)
#endif
#ifdef USE_TEXT_SENSOR
SUB_TEXT_SENSOR(orientation_xy)
SUB_TEXT_SENSOR(orientation_z)
#endif
Trigger<> *get_tap_trigger() { return &this->tap_trigger_; }
Trigger<> *get_double_tap_trigger() { return &this->double_tap_trigger_; }
Trigger<> *get_orientation_trigger() { return &this->orientation_trigger_; }
Trigger<> *get_freefall_trigger() { return &this->freefall_trigger_; }
Trigger<> *get_active_trigger() { return &this->active_trigger_; }
protected:
Model model_{Model::MSA311};
PowerMode power_mode_{PowerMode::NORMAL};
DataRate data_rate_{DataRate::ODR_250HZ};
Bandwidth bandwidth_{Bandwidth::BW_250HZ};
Range range_{Range::RANGE_2G};
Resolution resolution_{Resolution::RES_14BIT};
float offset_x_, offset_y_, offset_z_; // in m/s²
RegSwapPolarity swap_;
struct {
int scale_factor_exp;
uint8_t accel_data_width;
} device_params_{};
struct {
int16_t lsb_x, lsb_y, lsb_z;
float x, y, z;
} data_{};
struct {
RegMotionInterrupt motion_int;
RegOrientationStatus orientation;
RegOrientationStatus orientation_old;
uint32_t last_tap_ms{0};
uint32_t last_double_tap_ms{0};
uint32_t last_action_ms{0};
bool never_published{true};
} status_{};
void setup_odr_(DataRate rate);
void setup_power_mode_bandwidth_(PowerMode power_mode, Bandwidth bandwidth);
void setup_range_resolution_(Range range, Resolution resolution);
void setup_offset_(float offset_x, float offset_y, float offset_z);
bool read_data_();
bool read_motion_status_();
int64_t twos_complement_(uint64_t value, uint8_t bits);
//
// Actons / Triggers
//
Trigger<> tap_trigger_;
Trigger<> double_tap_trigger_;
Trigger<> orientation_trigger_;
Trigger<> freefall_trigger_;
Trigger<> active_trigger_;
void process_motions_(RegMotionInterrupt old);
};
} // namespace msa3xx
} // namespace esphome

View File

@ -0,0 +1,42 @@
import esphome.codegen as cg
from esphome.components import sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_ACCELERATION_X,
CONF_ACCELERATION_Y,
CONF_ACCELERATION_Z,
CONF_NAME,
ICON_BRIEFCASE_DOWNLOAD,
STATE_CLASS_MEASUREMENT,
UNIT_METER_PER_SECOND_SQUARED,
)
from . import CONF_MSA3XX_ID, MSA_SENSOR_SCHEMA
CODEOWNERS = ["@latonita"]
DEPENDENCIES = ["msa3xx"]
ACCELERATION_SENSORS = (CONF_ACCELERATION_X, CONF_ACCELERATION_Y, CONF_ACCELERATION_Z)
accel_schema = cv.maybe_simple_value(
sensor.sensor_schema(
unit_of_measurement=UNIT_METER_PER_SECOND_SQUARED,
icon=ICON_BRIEFCASE_DOWNLOAD,
accuracy_decimals=2,
state_class=STATE_CLASS_MEASUREMENT,
),
key=CONF_NAME,
)
CONFIG_SCHEMA = MSA_SENSOR_SCHEMA.extend(
{cv.Optional(sensor): accel_schema for sensor in ACCELERATION_SENSORS}
)
async def to_code(config):
hub = await cg.get_variable(config[CONF_MSA3XX_ID])
for accel_key in ACCELERATION_SENSORS:
if accel_key in config:
sens = await sensor.new_sensor(config[accel_key])
cg.add(getattr(hub, f"set_{accel_key}_sensor")(sens))

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