mirror of
https://github.com/esphome/esphome.git
synced 2026-10-26 16:02:13 +00:00
Compare commits
304
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad82e8c078 | ||
|
|
93b6388cb9 | ||
|
|
8677844629 | ||
|
|
c1a9c8f2f1 | ||
|
|
f35e45febf | ||
|
|
9f47b5c7f8 | ||
|
|
58b6779dbe | ||
|
|
bd881b025f | ||
|
|
c41b1f8f0f | ||
|
|
87b20ffdbb | ||
|
|
a3f38a9d4d | ||
|
|
76491691f5 | ||
|
|
6791dc7750 | ||
|
|
db76f0b1cd | ||
|
|
09f7b71efd | ||
|
|
fd54bf7a07 | ||
|
|
011497c447 | ||
|
|
31bfdcb316 | ||
|
|
42f50ac3bc | ||
|
|
3edbfb47b9 | ||
|
|
b622a59d74 | ||
|
|
0bfdbb948e | ||
|
|
8ce475ccef | ||
|
|
eb3394e332 | ||
|
|
88eedeec3f | ||
|
|
c9fe8b6b45 | ||
|
|
b7e652399b | ||
|
|
955998021c | ||
|
|
5a6983822d | ||
|
|
11a3372843 | ||
|
|
e388c459b3 | ||
|
|
c83ea861eb | ||
|
|
955afc4847 | ||
|
|
7a01c71022 | ||
|
|
9f254c69a1 | ||
|
|
1b6cc0bf18 | ||
|
|
0801211ba1 | ||
|
|
5a2f059be7 | ||
|
|
14998c0905 | ||
|
|
d69a1e9bf9 | ||
|
|
8eb5d17f3d | ||
|
|
808afc024d | ||
|
|
0bb98b715f | ||
|
|
90af765fae | ||
|
|
f17b18d51a | ||
|
|
5f11c430cc | ||
|
|
baf57195e2 | ||
|
|
27a97f36af | ||
|
|
0a28a5b023 | ||
|
|
5c6e62ffe0 | ||
|
|
0801b29617 | ||
|
|
b00ad7ca8a | ||
|
|
7ad51b4b8f | ||
|
|
65c1b06f7f | ||
|
|
e158205e4c | ||
|
|
f5debdad32 | ||
|
|
2a95fa44dc | ||
|
|
314557f83b | ||
|
|
e4e464a3b9 | ||
|
|
a41dbed69c | ||
|
|
ba10832ed7 | ||
|
|
d6b35f8ccf | ||
|
|
03efcf6bb5 | ||
|
|
0d46b1047e | ||
|
|
456f5b3042 | ||
|
|
83c9603b4b | ||
|
|
ad9a41ed61 | ||
|
|
e6b7b3a028 | ||
|
|
9a4a550fb9 | ||
|
|
42dc4025c9 | ||
|
|
901fd35a7e | ||
|
|
4344ffd682 | ||
|
|
1c671be1bd | ||
|
|
52dc51c7c6 | ||
|
|
671b6216a7 | ||
|
|
5764ca607d | ||
|
|
273c80ecd3 | ||
|
|
1933126ad7 | ||
|
|
64034b5b23 | ||
|
|
830854d661 | ||
|
|
87b7076cd1 | ||
|
|
5ec7047151 | ||
|
|
45d921f77a | ||
|
|
321b00143d | ||
|
|
e6e9016ed4 | ||
|
|
a292aa2654 | ||
|
|
4353f5ac9c | ||
|
|
4925f6648b | ||
|
|
47bd8f5ffc | ||
|
|
31771da92c | ||
|
|
1978f203bc | ||
|
|
ae906ada94 | ||
|
|
6a2465da63 | ||
|
|
9a5aab63d5 | ||
|
|
deeec17d0d | ||
|
|
735f810f08 | ||
|
|
cd48023a56 | ||
|
|
9330204908 | ||
|
|
45501056d8 | ||
|
|
2e6ad94bd1 | ||
|
|
5994105d34 | ||
|
|
0ac9a0f8bd | ||
|
|
77b8b384bd | ||
|
|
3728669fa6 | ||
|
|
578e4774a9 | ||
|
|
8696255ccc | ||
|
|
594931265b | ||
|
|
b0f753a7a2 | ||
|
|
fc69d8543e | ||
|
|
d586a6c8d4 | ||
|
|
9301391792 | ||
|
|
1b801427c4 | ||
|
|
be391feaa9 | ||
|
|
7e369f332c | ||
|
|
a40ecfdf9c | ||
|
|
9a6765eb8f | ||
|
|
1d0a87daa8 | ||
|
|
c98dd74e11 | ||
|
|
aceff27948 | ||
|
|
211337cd6d | ||
|
|
4276815734 | ||
|
|
3c57064bf2 | ||
|
|
f9be82d1a6 | ||
|
|
10a0bf0ff8 | ||
|
|
484674b85d | ||
|
|
49c1547fda | ||
|
|
7b4353b417 | ||
|
|
5f20346d54 | ||
|
|
356c1d9134 | ||
|
|
9534a1df55 | ||
|
|
fb64b18047 | ||
|
|
5572bf77dd | ||
|
|
d8ec76e02b | ||
|
|
cf5faa454c | ||
|
|
55d14c6fbf | ||
|
|
d86bdb36cf | ||
|
|
c64684dd00 | ||
|
|
ed002feee9 | ||
|
|
38f31d85b6 | ||
|
|
d04ef2378e | ||
|
|
5e26633231 | ||
|
|
6c78b480c4 | ||
|
|
3d33a16b71 | ||
|
|
e3aa8773e6 | ||
|
|
be5e00aefd | ||
|
|
9c5ebdffc9 | ||
|
|
9554e1a212 | ||
|
|
28491f83b4 | ||
|
|
2f71ec16d7 | ||
|
|
3daa1d2450 | ||
|
|
a513cef4e2 | ||
|
|
fa17215053 | ||
|
|
073d9a72a0 | ||
|
|
3a24d0b03b | ||
|
|
b229bd4d8a | ||
|
|
c8e240198d | ||
|
|
0512339c58 | ||
|
|
e95bebb7d0 | ||
|
|
084ff9d6e3 | ||
|
|
4a9e5f5da9 | ||
|
|
f6158597ac | ||
|
|
8924640593 | ||
|
|
26c000c417 | ||
|
|
7cb32f9546 | ||
|
|
13de0275c8 | ||
|
|
22af86a65b | ||
|
|
83b099d708 | ||
|
|
6957e2ef15 | ||
|
|
743a10fa3e | ||
|
|
f7b1e9ae65 | ||
|
|
53a5c7b051 | ||
|
|
54c50c07c5 | ||
|
|
64df0c1da1 | ||
|
|
9c31e08e95 | ||
|
|
93124f1f1f | ||
|
|
8abb5f102d | ||
|
|
12fb1b0697 | ||
|
|
81216da8e7 | ||
|
|
ed2cdad9b4 | ||
|
|
be818a5bf2 | ||
|
|
444a217ee7 | ||
|
|
93f9d4a1ad | ||
|
|
ac7d39a8c1 | ||
|
|
612033062f | ||
|
|
f9dbd24a2a | ||
|
|
833a835e0f | ||
|
|
399dc26236 | ||
|
|
0d9aea9594 | ||
|
|
f2261d6632 | ||
|
|
d224a82e38 | ||
|
|
ce436ff8b4 | ||
|
|
cbda1e71be | ||
|
|
ceabfeb6f4 | ||
|
|
5e11a886f1 | ||
|
|
e771b2522a | ||
|
|
ee96293c4c | ||
|
|
fa18f5a0ad | ||
|
|
269d52ac2e | ||
|
|
06a52f682f | ||
|
|
409ff027cc | ||
|
|
3c7e5000a5 | ||
|
|
7af211ab54 | ||
|
|
cc81069d02 | ||
|
|
ba567cecc3 | ||
|
|
ee7fb83334 | ||
|
|
9db31db24d | ||
|
|
ceca66c01a | ||
|
|
dc97c5a161 | ||
|
|
97bbcf55d9 | ||
|
|
d135ac97e5 | ||
|
|
296dd19361 | ||
|
|
138304cfbe | ||
|
|
9fef1862be | ||
|
|
80758f11aa | ||
|
|
9d8362738f | ||
|
|
b2c22b5802 | ||
|
|
f62009ecc9 | ||
|
|
c09e3e0d45 | ||
|
|
cdd7bef74a | ||
|
|
0e1321a813 | ||
|
|
022b612366 | ||
|
|
2dc9fbec5e | ||
|
|
9dafff6a61 | ||
|
|
3c7a1062c8 | ||
|
|
8164cdd018 | ||
|
|
4ea491d46f | ||
|
|
7fdb6cd2d5 | ||
|
|
2d50e07e90 | ||
|
|
78a451a1c0 | ||
|
|
2483621500 | ||
|
|
4f086b6b25 | ||
|
|
c4850a9320 | ||
|
|
91bcb14c09 | ||
|
|
85e4a6cc33 | ||
|
|
149d7cd801 | ||
|
|
ce2e113a6d | ||
|
|
ae5b6d271f | ||
|
|
e1572d7520 | ||
|
|
536afce394 | ||
|
|
152b6a5d50 | ||
|
|
f68ebcf7d1 | ||
|
|
ca5fa6dfaf | ||
|
|
962a82d157 | ||
|
|
c6d4ca7377 | ||
|
|
465a6a3596 | ||
|
|
03c9b001f4 | ||
|
|
d680200e5a | ||
|
|
98dae49440 | ||
|
|
7ec366c1ba | ||
|
|
b1e1b0d7c5 | ||
|
|
ec49eb12dd | ||
|
|
949474efc6 | ||
|
|
708f095b6f | ||
|
|
bf4af703b6 | ||
|
|
541616e19c | ||
|
|
952c3e6584 | ||
|
|
2ea55bee7b | ||
|
|
1931ef32ee | ||
|
|
f2da6c1a56 | ||
|
|
6e9e83bdfd | ||
|
|
fe29a53b71 | ||
|
|
fa2dafcbca | ||
|
|
dc18fced19 | ||
|
|
389d3629c2 | ||
|
|
36a4d87611 | ||
|
|
65db614f22 | ||
|
|
591856e7dc | ||
|
|
b680482092 | ||
|
|
7fe447a4a2 | ||
|
|
8fae4f6767 | ||
|
|
3d458bd9c8 | ||
|
|
fd7510d6e5 | ||
|
|
ffe55f7691 | ||
|
|
40ac344bf2 | ||
|
|
667ffe98d9 | ||
|
|
270f776675 | ||
|
|
965bbd4425 | ||
|
|
e485e5af90 | ||
|
|
c795d0734d | ||
|
|
0e77d47df4 | ||
|
|
47c2f8f31a | ||
|
|
fe53d24e46 | ||
|
|
daf988fde8 | ||
|
|
6604a9cca7 | ||
|
|
ddd45e393b | ||
|
|
0974e8a1f5 | ||
|
|
1478307280 | ||
|
|
fc77cdcb7d | ||
|
|
fcb9da489e | ||
|
|
ab458fffab | ||
|
|
c0c3afe6b1 | ||
|
|
b092f64744 | ||
|
|
6b2938c1f7 | ||
|
|
0e5d902c95 | ||
|
|
11fc8e4c07 | ||
|
|
d9f0f40373 | ||
|
|
e671a602e9 | ||
|
|
7f44b9b826 | ||
|
|
47804f8d08 | ||
|
|
67cc43c8ef | ||
|
|
3e9808162b | ||
|
|
a6f81f3185 | ||
|
|
5296539849 | ||
|
|
de9074fb5e |
No files matched your search
@@ -1,7 +1,6 @@
|
||||
from esphome import automation
|
||||
from esphome.automation import maybe_simple_id
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import mqtt, web_server
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_CODE,
|
||||
@@ -10,11 +9,11 @@ from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_MQTT_ID,
|
||||
CONF_ON_STATE,
|
||||
CONF_WEB_SERVER,
|
||||
)
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority, entity_helpers
|
||||
from esphome.core.entity_helpers import (
|
||||
entity_duplicate_validator,
|
||||
mqtt_component_class,
|
||||
queue_entity_register,
|
||||
setup_entity,
|
||||
)
|
||||
@@ -44,12 +43,12 @@ AlarmControlPanelState = alarm_control_panel_ns.enum("AlarmControlPanelState")
|
||||
|
||||
|
||||
_ALARM_CONTROL_PANEL_SCHEMA = (
|
||||
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
|
||||
cv.ENTITY_BASE_SCHEMA.extend(entity_helpers.WEBSERVER_SORTING_SCHEMA)
|
||||
.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
|
||||
.extend(
|
||||
{
|
||||
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(
|
||||
mqtt.MQTTAlarmControlPanelComponent
|
||||
mqtt_component_class("MQTTAlarmControlPanelComponent")
|
||||
),
|
||||
cv.Optional(CONF_ON_STATE): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_TRIGGERED): automation.validate_automation({}),
|
||||
@@ -166,11 +165,7 @@ _CALLBACK_AUTOMATIONS = (
|
||||
@setup_entity("alarm_control_panel")
|
||||
async def setup_alarm_control_panel_core_(var, config):
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
if web_server_config := config.get(CONF_WEB_SERVER):
|
||||
await web_server.add_entity_config(var, web_server_config)
|
||||
if mqtt_id := config.get(CONF_MQTT_ID):
|
||||
mqtt_ = cg.new_Pvariable(mqtt_id, var)
|
||||
await mqtt.register_mqtt_component(mqtt_, config)
|
||||
await entity_helpers.setup_entity_integrations(var, config)
|
||||
|
||||
|
||||
async def register_alarm_control_panel(var, config):
|
||||
|
||||
@@ -4,14 +4,13 @@ import esphome.config_validation as cv
|
||||
from esphome.const import DEVICE_CLASS_SWITCH, ICON_WIFI
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import CONF_AT581X_ID, AT581XComponent, at581x_ns
|
||||
from .. import CONF_AT581X_ID, AT581XComponent
|
||||
|
||||
DEPENDENCIES = ["at581x"]
|
||||
|
||||
RFSwitch = at581x_ns.class_("RFSwitch", switch.Switch)
|
||||
|
||||
CONFIG_SCHEMA = switch.switch_schema(
|
||||
RFSwitch,
|
||||
switch.ApplySwitch,
|
||||
device_class=DEVICE_CLASS_SWITCH,
|
||||
icon=ICON_WIFI,
|
||||
).extend(
|
||||
@@ -24,7 +23,7 @@ CONFIG_SCHEMA = switch.switch_schema(
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
at581x_component = await cg.get_variable(config[CONF_AT581X_ID])
|
||||
s = await switch.new_switch(config)
|
||||
await cg.register_parented(s, config[CONF_AT581X_ID])
|
||||
cg.add(at581x_component.set_rf_power_switch(s))
|
||||
hub = await cg.get_variable(config[CONF_AT581X_ID])
|
||||
await switch.sub_switches(config, parent=hub).create(
|
||||
config, hub.set_rf_power_switch, apply="set_rf_mode(state)", publish="before"
|
||||
)
|
||||
@@ -1,10 +0,0 @@
|
||||
#include "rf_switch.h"
|
||||
|
||||
namespace esphome::at581x {
|
||||
|
||||
void RFSwitch::write_state(bool state) {
|
||||
this->publish_state(state);
|
||||
this->parent_->set_rf_mode(state);
|
||||
}
|
||||
|
||||
} // namespace esphome::at581x
|
||||
@@ -1,13 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/switch/switch.h"
|
||||
#include "../at581x.h"
|
||||
|
||||
namespace esphome::at581x {
|
||||
|
||||
class RFSwitch final : public switch_::Switch, public Parented<AT581XComponent> {
|
||||
protected:
|
||||
void write_state(bool state) override;
|
||||
};
|
||||
|
||||
} // namespace esphome::at581x
|
||||
@@ -63,6 +63,7 @@ from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority, entity
|
||||
from esphome.core.entity_helpers import (
|
||||
SubEntities,
|
||||
entity_duplicate_validator,
|
||||
lazy_load_setup,
|
||||
lazy_load_validator,
|
||||
mqtt_component_class,
|
||||
queue_entity_register,
|
||||
@@ -621,10 +622,7 @@ async def setup_binary_sensor_core_(var, config):
|
||||
|
||||
await entity_helpers.setup_entity_integrations(var, config)
|
||||
|
||||
if "zigbee" in CORE.loaded_integrations:
|
||||
from esphome.components import zigbee
|
||||
|
||||
await zigbee.setup_binary_sensor(var, config)
|
||||
await lazy_load_setup("zigbee", "setup_binary_sensor", var, config)
|
||||
|
||||
|
||||
async def register_binary_sensor(var, config):
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "bl0939.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <cinttypes>
|
||||
|
||||
@@ -20,7 +21,7 @@ static const uint8_t BL0939_REG_SOFT_RESET = 0x19;
|
||||
static const uint8_t BL0939_REG_USR_WRPROT = 0x1A;
|
||||
static const uint8_t BL0939_REG_TPS_CTRL = 0x1B;
|
||||
|
||||
const uint8_t BL0939_INIT[6][6] = {
|
||||
static constexpr uint8_t BL0939_INIT[6][6] PROGMEM = {
|
||||
// Reset to default
|
||||
{BL0939_WRITE_COMMAND, BL0939_REG_SOFT_RESET, 0x5A, 0x5A, 0x5A, 0x33},
|
||||
// Enable User Operation Write
|
||||
@@ -66,8 +67,10 @@ void BL0939::update() {
|
||||
}
|
||||
|
||||
void BL0939::setup() {
|
||||
for (auto *i : BL0939_INIT) {
|
||||
this->write_array(i, 6);
|
||||
uint8_t cmd[sizeof(BL0939_INIT[0])];
|
||||
for (const auto *i : BL0939_INIT) {
|
||||
progmem_memcpy(cmd, i, sizeof(cmd));
|
||||
this->write_array(cmd, sizeof(cmd));
|
||||
delay(1);
|
||||
}
|
||||
this->flush();
|
||||
|
||||
@@ -1,11 +1,23 @@
|
||||
from collections.abc import Coroutine
|
||||
from collections.abc import Callable, Coroutine
|
||||
import functools
|
||||
from typing import Any
|
||||
|
||||
from esphome import automation
|
||||
from esphome.automation import maybe_simple_id
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import esp32_ble, esp32_ble_client, esp32_ble_tracker
|
||||
from esphome.components.esp32_ble import BTLoggers
|
||||
from esphome.components import ble_device_base, bluetooth_connection
|
||||
from esphome.components.ble_device_base import (
|
||||
BT_UUID16_FORMAT as bt_uuid16_format,
|
||||
BT_UUID32_FORMAT as bt_uuid32_format,
|
||||
BT_UUID128_FORMAT as bt_uuid128_format,
|
||||
as_hex,
|
||||
as_reversed_hex_array,
|
||||
bt_uuid,
|
||||
)
|
||||
from esphome.config_helpers import (
|
||||
filter_source_files_from_platform,
|
||||
frameworks_for_platforms,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_CHARACTERISTIC_UUID,
|
||||
@@ -18,15 +30,55 @@ from esphome.const import (
|
||||
CONF_SERVICE_UUID,
|
||||
CONF_TRIGGER_ID,
|
||||
CONF_VALUE,
|
||||
PLATFORM_ESP32,
|
||||
PlatformFramework,
|
||||
)
|
||||
from esphome.core import ID
|
||||
from esphome.core import CORE, ID
|
||||
from esphome.enum import StrEnum
|
||||
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
|
||||
from esphome.types import ConfigType
|
||||
|
||||
AUTO_LOAD = ["esp32_ble_client"]
|
||||
# The esp32 BLE stack (esp32_ble, esp32_ble_tracker) is imported lazily inside
|
||||
# the esp32 schema/codegen arms: importing those modules registers esp32-only
|
||||
# automations as a side effect, which must not leak into the neutral
|
||||
# platforms' registries (the bluetooth_proxy pattern).
|
||||
|
||||
|
||||
def _legacy_engine() -> bool:
|
||||
"""True when the build uses the legacy raw-gattc engine - one line to
|
||||
flip when esp32 moves to the neutral engine (with
|
||||
USE_BLE_CLIENT_LEGACY_ENGINE in _to_code_esp32)."""
|
||||
return CORE.is_esp32
|
||||
|
||||
|
||||
def AUTO_LOAD() -> list[str]:
|
||||
"""The engine's closure per platform: the legacy esp32 engine builds on
|
||||
esp32_ble_client plus bluetooth_connection (the shared service-table
|
||||
materializer; its sources compile empty in builds without a neutral
|
||||
node), the neutral engine on the bluetooth_connection backend. The
|
||||
platform-less arm is the union for manifest-resolving tooling."""
|
||||
if _legacy_engine() or CORE.target_platform is None:
|
||||
return ["bluetooth_connection", "esp32_ble_client"]
|
||||
return ["bluetooth_connection"]
|
||||
|
||||
|
||||
CODEOWNERS = ["@buxtronix", "@clydebarrow"]
|
||||
DEPENDENCIES = ["esp32_ble_tracker"]
|
||||
DOMAIN = "ble_client"
|
||||
|
||||
FILTER_SOURCE_FILES = filter_source_files_from_platform(
|
||||
{
|
||||
"ble_client.cpp": {
|
||||
PlatformFramework.ESP32_ARDUINO,
|
||||
PlatformFramework.ESP32_IDF,
|
||||
},
|
||||
# Every framework of every non-esp32 registry platform: a platform
|
||||
# that validates the neutral arm must also compile the neutral engine.
|
||||
"ble_client_gatt.cpp": frameworks_for_platforms(
|
||||
set(bluetooth_connection.GATT_CLIENT_PLATFORMS) - {PLATFORM_ESP32}
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
CONF_DESCRIPTOR_UUID = "descriptor_uuid"
|
||||
CONF_ON_NOTIFY = "on_notify"
|
||||
|
||||
@@ -62,16 +114,12 @@ def notify_from_on_notify(config: ConfigType) -> ConfigType:
|
||||
|
||||
|
||||
ble_client_ns = cg.esphome_ns.namespace("ble_client")
|
||||
BLEClient = ble_client_ns.class_("BLEClient", esp32_ble_client.BLEClientBase)
|
||||
# One codegen class for both engines: the exclusively-gated headers resolve
|
||||
# the name to exactly one C++ definition per build.
|
||||
BLEClient = ble_client_ns.class_("BLEClient", cg.Component)
|
||||
BLEClientNode = ble_client_ns.class_("BLEClientNode")
|
||||
BLEClientNodeConstRef = BLEClientNode.operator("ref").operator("const")
|
||||
# Triggers
|
||||
BLEClientConnectTrigger = ble_client_ns.class_(
|
||||
"BLEClientConnectTrigger", automation.Trigger.template(BLEClientNodeConstRef)
|
||||
)
|
||||
BLEClientDisconnectTrigger = ble_client_ns.class_(
|
||||
"BLEClientDisconnectTrigger", automation.Trigger.template(BLEClientNodeConstRef)
|
||||
)
|
||||
BLEClientPasskeyRequestTrigger = ble_client_ns.class_(
|
||||
"BLEClientPasskeyRequestTrigger", automation.Trigger.template(BLEClientNodeConstRef)
|
||||
)
|
||||
@@ -109,62 +157,175 @@ CONF_AUTO_CONNECT = "auto_connect"
|
||||
|
||||
MULTI_CONF = True
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(BLEClient),
|
||||
cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
|
||||
cv.Optional(CONF_NAME): cv.string,
|
||||
cv.Optional(CONF_AUTO_CONNECT, default=True): cv.boolean,
|
||||
cv.Optional(CONF_ON_CONNECT): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
BLEClientConnectTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_DISCONNECT): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
BLEClientDisconnectTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_PASSKEY_REQUEST): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
BLEClientPasskeyRequestTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_PASSKEY_NOTIFICATION): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
BLEClientPasskeyNotificationTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(
|
||||
CONF_ON_NUMERIC_COMPARISON_REQUEST
|
||||
): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
BLEClientNumericComparisonRequestTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA),
|
||||
esp32_ble.consume_connection_slots(1, "ble_client"),
|
||||
# Keys shared by both engines' schemas.
|
||||
_COMMON_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(BLEClient),
|
||||
cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
|
||||
cv.Optional(CONF_AUTO_CONNECT, default=True): cv.boolean,
|
||||
cv.Optional(CONF_ON_CONNECT): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_DISCONNECT): automation.validate_automation({}),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
# Both engines register these on the client; the Bluedroid-security triggers below stay trigger classes
|
||||
_CALLBACK_AUTOMATIONS = (
|
||||
automation.CallbackAutomation(CONF_ON_CONNECT, "add_on_connect_callback"),
|
||||
automation.CallbackAutomation(CONF_ON_DISCONNECT, "add_on_disconnect_callback"),
|
||||
)
|
||||
|
||||
|
||||
@functools.cache
|
||||
def _esp32_config_schema() -> cv.All:
|
||||
"""The legacy engine's schema, byte-compatible with what esp32 always had
|
||||
(including the Bluedroid security triggers)."""
|
||||
from esphome.components import esp32_ble_tracker
|
||||
|
||||
return cv.All(
|
||||
_COMMON_SCHEMA.extend(
|
||||
{
|
||||
# Accepted-but-unused legacy key; not propagated to the
|
||||
# neutral schema.
|
||||
cv.Optional(CONF_NAME): cv.string,
|
||||
cv.Optional(CONF_ON_PASSKEY_REQUEST): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
BLEClientPasskeyRequestTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(
|
||||
CONF_ON_PASSKEY_NOTIFICATION
|
||||
): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
BLEClientPasskeyNotificationTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(
|
||||
CONF_ON_NUMERIC_COMPARISON_REQUEST
|
||||
): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
BLEClientNumericComparisonRequestTrigger
|
||||
),
|
||||
}
|
||||
),
|
||||
}
|
||||
).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA),
|
||||
bluetooth_connection.consume_gatt_slot("ble_client"),
|
||||
)
|
||||
|
||||
|
||||
@functools.cache
|
||||
def _gatt_config_schema(platform: str) -> cv.All:
|
||||
"""The neutral engine's schema: the shared keys plus the hub reference
|
||||
(parsed-advertisement sightings) and the GATT backend declaration.
|
||||
Keyed by platform - the backend fragment differs per platform."""
|
||||
return cv.All(
|
||||
_COMMON_SCHEMA.extend(ble_device_base.BLE_DEVICE_SCHEMA).extend(
|
||||
bluetooth_connection.gatt_client_schema(platform)
|
||||
),
|
||||
bluetooth_connection.consume_gatt_slot("ble_client"),
|
||||
)
|
||||
|
||||
|
||||
@schema_extractor("schema")
|
||||
def _validate_platform(config: ConfigType) -> ConfigType:
|
||||
if config is SCHEMA_EXTRACT:
|
||||
# Deliberate gap (the bluetooth_proxy pattern): the dumper gets only
|
||||
# this shape, so the neutral arm's ble_hub_id is absent from editor
|
||||
# schemas and the esp32-only keys are advertised on every platform.
|
||||
# The language-schema dumper runs without a platform; expose the
|
||||
# esp32 (legacy-engine) shape.
|
||||
return _esp32_config_schema()
|
||||
if _legacy_engine():
|
||||
return _esp32_config_schema()(config)
|
||||
if CORE.target_platform in bluetooth_connection.GATT_CLIENT_PLATFORMS:
|
||||
return _gatt_config_schema(CORE.target_platform)(config)
|
||||
raise cv.Invalid(f"ble_client is not supported on {CORE.target_platform}")
|
||||
|
||||
|
||||
CONFIG_SCHEMA = _validate_platform
|
||||
|
||||
CONF_BLE_CLIENT_ID = "ble_client_id"
|
||||
|
||||
|
||||
class BLEClientFeatures(StrEnum):
|
||||
"""Per-platform engine capabilities consumers declare against."""
|
||||
|
||||
# The platform-neutral node interface (on_connected/table + completion
|
||||
# callbacks) - every platform with a ble_client engine.
|
||||
GATT_NODE = "gatt_node"
|
||||
# The raw esp32 GATT client event stream (gattc/gap handlers,
|
||||
# node_state) - the legacy engine only.
|
||||
RAW_GATTC = "raw_gattc"
|
||||
# Pairing dialog replies and bond management (Bluedroid GAP/SMP).
|
||||
SECURITY = "security"
|
||||
|
||||
|
||||
def _engine_features() -> set[BLEClientFeatures]:
|
||||
"""Features the validated platform's engine provides."""
|
||||
if _legacy_engine():
|
||||
return {
|
||||
BLEClientFeatures.GATT_NODE,
|
||||
BLEClientFeatures.RAW_GATTC,
|
||||
BLEClientFeatures.SECURITY,
|
||||
}
|
||||
if CORE.target_platform in bluetooth_connection.GATT_CLIENT_PLATFORMS:
|
||||
return {BLEClientFeatures.GATT_NODE}
|
||||
return set()
|
||||
|
||||
|
||||
def requires_feature(
|
||||
feature: BLEClientFeatures, description: str
|
||||
) -> Callable[[Any], Any]:
|
||||
"""Validator gating a consumer to platforms whose engine provides
|
||||
`feature`, naming the missing capability in the error."""
|
||||
|
||||
def validator(value: Any) -> Any:
|
||||
features = _engine_features()
|
||||
if feature not in features:
|
||||
available = (
|
||||
f"; this platform's engine provides: {', '.join(sorted(features))}"
|
||||
if features
|
||||
else ""
|
||||
)
|
||||
raise cv.Invalid(
|
||||
f"{description} requires the ble_client '{feature}' feature, "
|
||||
f"which {CORE.target_platform} does not provide{available}"
|
||||
)
|
||||
return value
|
||||
|
||||
return validator
|
||||
|
||||
|
||||
# The one choke point for every node component still on the raw esp32 event
|
||||
# stream; migrating to the neutral interface (NODE_BLE_CLIENT_SCHEMA +
|
||||
# register_gatt_node) lifts it.
|
||||
_legacy_engine_only = requires_feature(
|
||||
BLEClientFeatures.RAW_GATTC,
|
||||
"This component drives the raw ESP32 GATT client events and has not "
|
||||
"been migrated to the platform-neutral node interface yet; it",
|
||||
)
|
||||
|
||||
BLE_CLIENT_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_BLE_CLIENT_ID): cv.use_id(BLEClient),
|
||||
cv.GenerateID(CONF_BLE_CLIENT_ID): cv.All(
|
||||
cv.use_id(BLEClient), _legacy_engine_only
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
# For node components on the neutral interface: valid wherever ble_client
|
||||
# itself is.
|
||||
NODE_BLE_CLIENT_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_BLE_CLIENT_ID): cv.All(
|
||||
cv.use_id(BLEClient),
|
||||
requires_feature(BLEClientFeatures.GATT_NODE, "This component"),
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -174,6 +335,26 @@ async def register_ble_node(var, config):
|
||||
cg.add(parent.register_ble_node(var))
|
||||
|
||||
|
||||
def _request_gatt_node_build() -> None:
|
||||
"""Node storage and the one define meaning "the neutral node surface is
|
||||
compiled in", plus the esp32 bridge/materializer defines."""
|
||||
_request_node_slot()
|
||||
cg.add_define("USE_BLE_CLIENT_GATT_NODES")
|
||||
if _legacy_engine():
|
||||
# Deliberately not ble_device_base.request_gatt_client(): that would
|
||||
# claim a phantom backend slot on combined proxy builds.
|
||||
cg.add_define("USE_BLE_GATT_CLIENT")
|
||||
cg.add_define("USE_BLE_GATT_BACKEND_BLUEDROID")
|
||||
cg.add_define("USE_BLUEDROID_GATT_SERVICE_TABLE")
|
||||
|
||||
|
||||
async def register_gatt_node(var, config):
|
||||
"""Register a node on the platform-neutral interface (both engines)."""
|
||||
parent = await cg.get_variable(config[CONF_BLE_CLIENT_ID])
|
||||
_request_gatt_node_build()
|
||||
cg.add(parent.register_gatt_node(var))
|
||||
|
||||
|
||||
def new_ble_node_component(
|
||||
config: ConfigType, *args: Any
|
||||
) -> Coroutine[Any, Any, cg.MockObj]:
|
||||
@@ -184,8 +365,8 @@ def new_ble_node_component(
|
||||
BLE_WRITE_ACTION_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_ID): cv.use_id(BLEClient),
|
||||
cv.Required(CONF_SERVICE_UUID): esp32_ble_tracker.bt_uuid,
|
||||
cv.Required(CONF_CHARACTERISTIC_UUID): esp32_ble_tracker.bt_uuid,
|
||||
cv.Required(CONF_SERVICE_UUID): bt_uuid,
|
||||
cv.Required(CONF_CHARACTERISTIC_UUID): bt_uuid,
|
||||
cv.Required(CONF_VALUE): cv.templatable(cv.ensure_list(cv.hex_uint8_t)),
|
||||
}
|
||||
)
|
||||
@@ -196,25 +377,34 @@ BLE_CONNECT_ACTION_SCHEMA = maybe_simple_id(
|
||||
}
|
||||
)
|
||||
|
||||
BLE_NUMERIC_COMPARISON_REPLY_ACTION_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_ID): cv.use_id(BLEClient),
|
||||
cv.Required(CONF_ACCEPT): cv.templatable(cv.boolean),
|
||||
}
|
||||
BLE_NUMERIC_COMPARISON_REPLY_ACTION_SCHEMA = cv.All(
|
||||
requires_feature(BLEClientFeatures.SECURITY, "This action"),
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_ID): cv.use_id(BLEClient),
|
||||
cv.Required(CONF_ACCEPT): cv.templatable(cv.boolean),
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
BLE_PASSKEY_REPLY_ACTION_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_ID): cv.use_id(BLEClient),
|
||||
cv.Required(CONF_PASSKEY): cv.templatable(cv.int_range(min=0, max=999999)),
|
||||
}
|
||||
BLE_PASSKEY_REPLY_ACTION_SCHEMA = cv.All(
|
||||
requires_feature(BLEClientFeatures.SECURITY, "This action"),
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_ID): cv.use_id(BLEClient),
|
||||
cv.Required(CONF_PASSKEY): cv.templatable(cv.int_range(min=0, max=999999)),
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
BLE_REMOVE_BOND_ACTION_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_ID): cv.use_id(BLEClient),
|
||||
}
|
||||
BLE_REMOVE_BOND_ACTION_SCHEMA = cv.All(
|
||||
requires_feature(BLEClientFeatures.SECURITY, "This action"),
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_ID): cv.use_id(BLEClient),
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -242,6 +432,8 @@ automation.register_simple_action(
|
||||
)
|
||||
async def ble_write_to_code(config, action_id, template_arg, args):
|
||||
parent = await cg.get_variable(config[CONF_ID])
|
||||
# The action registers itself as a neutral node in its constructor.
|
||||
_request_gatt_node_build()
|
||||
var = cg.new_Pvariable(action_id, template_arg, parent)
|
||||
|
||||
value = config[CONF_VALUE]
|
||||
@@ -256,38 +448,20 @@ async def ble_write_to_code(config, action_id, template_arg, args):
|
||||
arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*value))
|
||||
cg.add(var.set_value_simple(arr, len(value)))
|
||||
|
||||
if len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid16_format):
|
||||
cg.add(
|
||||
var.set_service_uuid16(esp32_ble_tracker.as_hex(config[CONF_SERVICE_UUID]))
|
||||
)
|
||||
elif len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid32_format):
|
||||
cg.add(
|
||||
var.set_service_uuid32(esp32_ble_tracker.as_hex(config[CONF_SERVICE_UUID]))
|
||||
)
|
||||
elif len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid128_format):
|
||||
uuid128 = esp32_ble_tracker.as_reversed_hex_array(config[CONF_SERVICE_UUID])
|
||||
if len(config[CONF_SERVICE_UUID]) == len(bt_uuid16_format):
|
||||
cg.add(var.set_service_uuid16(as_hex(config[CONF_SERVICE_UUID])))
|
||||
elif len(config[CONF_SERVICE_UUID]) == len(bt_uuid32_format):
|
||||
cg.add(var.set_service_uuid32(as_hex(config[CONF_SERVICE_UUID])))
|
||||
elif len(config[CONF_SERVICE_UUID]) == len(bt_uuid128_format):
|
||||
uuid128 = as_reversed_hex_array(config[CONF_SERVICE_UUID])
|
||||
cg.add(var.set_service_uuid128(uuid128))
|
||||
|
||||
if len(config[CONF_CHARACTERISTIC_UUID]) == len(esp32_ble_tracker.bt_uuid16_format):
|
||||
cg.add(
|
||||
var.set_char_uuid16(
|
||||
esp32_ble_tracker.as_hex(config[CONF_CHARACTERISTIC_UUID])
|
||||
)
|
||||
)
|
||||
elif len(config[CONF_CHARACTERISTIC_UUID]) == len(
|
||||
esp32_ble_tracker.bt_uuid32_format
|
||||
):
|
||||
cg.add(
|
||||
var.set_char_uuid32(
|
||||
esp32_ble_tracker.as_hex(config[CONF_CHARACTERISTIC_UUID])
|
||||
)
|
||||
)
|
||||
elif len(config[CONF_CHARACTERISTIC_UUID]) == len(
|
||||
esp32_ble_tracker.bt_uuid128_format
|
||||
):
|
||||
uuid128 = esp32_ble_tracker.as_reversed_hex_array(
|
||||
config[CONF_CHARACTERISTIC_UUID]
|
||||
)
|
||||
if len(config[CONF_CHARACTERISTIC_UUID]) == len(bt_uuid16_format):
|
||||
cg.add(var.set_char_uuid16(as_hex(config[CONF_CHARACTERISTIC_UUID])))
|
||||
elif len(config[CONF_CHARACTERISTIC_UUID]) == len(bt_uuid32_format):
|
||||
cg.add(var.set_char_uuid32(as_hex(config[CONF_CHARACTERISTIC_UUID])))
|
||||
elif len(config[CONF_CHARACTERISTIC_UUID]) == len(bt_uuid128_format):
|
||||
uuid128 = as_reversed_hex_array(config[CONF_CHARACTERISTIC_UUID])
|
||||
cg.add(var.set_char_uuid128(uuid128))
|
||||
|
||||
return var
|
||||
@@ -341,22 +515,48 @@ automation.register_simple_action(
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def _to_code_esp32(config: ConfigType) -> cg.MockObj:
|
||||
from esphome.components import esp32_ble, esp32_ble_tracker
|
||||
from esphome.components.esp32_ble import BTLoggers
|
||||
|
||||
# Register the loggers this component needs
|
||||
esp32_ble.register_bt_logger(BTLoggers.GATT, BTLoggers.SMP)
|
||||
cg.add_define("USE_ESP32_BLE_UUID")
|
||||
cg.add_define("USE_BLE_CLIENT_LEGACY_ENGINE")
|
||||
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await esp32_ble_tracker.register_client(var, config)
|
||||
return var
|
||||
|
||||
|
||||
# Sizes the neutral client's node storage; the client itself requests a
|
||||
# baseline slot so the define exists on every build that compiles the engine.
|
||||
_request_node_slot = cg.slot_counter("ESPHOME_BLE_CLIENT_MAX_NODES")
|
||||
|
||||
|
||||
async def _to_code_gatt(config: ConfigType) -> cg.MockObj:
|
||||
# The engine always carries the node surface (the client itself owns the
|
||||
# baseline slot).
|
||||
_request_gatt_node_build()
|
||||
backend = await bluetooth_connection.new_gatt_backend(config)
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
cg.add(var.set_backend(backend))
|
||||
# Sighting-gated connects: the client listens for the peer's parsed
|
||||
# advertisements through the hub.
|
||||
await ble_device_base.register_ble_device(var, config)
|
||||
return var
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
if _legacy_engine():
|
||||
var = await _to_code_esp32(config)
|
||||
else:
|
||||
var = await _to_code_gatt(config)
|
||||
cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
|
||||
cg.add(var.set_auto_connect(config[CONF_AUTO_CONNECT]))
|
||||
for conf in config.get(CONF_ON_CONNECT, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
for conf in config.get(CONF_ON_DISCONNECT, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
for conf in config.get(CONF_ON_PASSKEY_REQUEST, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include "automation.h"
|
||||
|
||||
namespace esphome::ble_client {
|
||||
|
||||
const char *const Automation::TAG = "ble_client.automation"; // NOLINT: ble_client never builds for ESP8266
|
||||
|
||||
} // namespace esphome::ble_client
|
||||
|
||||
#endif
|
||||
@@ -1,27 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_ESP32
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#ifdef USE_BLE_CLIENT_LEGACY_ENGINE
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/components/ble_client/ble_client.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
// Maximum bytes to log in hex format for BLE writes (many logging buffers are 256 chars)
|
||||
static constexpr size_t BLE_WRITE_MAX_LOG_BYTES = 64;
|
||||
|
||||
namespace esphome::ble_client {
|
||||
|
||||
// placeholder class for static TAG .
|
||||
class Automation {
|
||||
public:
|
||||
// could be made inline with C++17
|
||||
static const char *const TAG;
|
||||
};
|
||||
|
||||
// Base for nodes that never read the parent's services.
|
||||
// The parent releases its services only once every node reports Established, so a node that never
|
||||
// reports it keeps that memory allocated for the life of the connection.
|
||||
@@ -39,45 +26,6 @@ class BLEClientServicelessNode : public BLEClientNode {
|
||||
virtual void on_gattc_event(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) {}
|
||||
};
|
||||
|
||||
// implement on_connect automation.
|
||||
class BLEClientConnectTrigger final : public Trigger<>, public BLEClientNode {
|
||||
public:
|
||||
explicit BLEClientConnectTrigger(BLEClient *parent) { parent->register_ble_node(this); }
|
||||
void loop() override {}
|
||||
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
||||
esp_ble_gattc_cb_param_t *param) override {
|
||||
if (event == ESP_GATTC_SEARCH_CMPL_EVT) {
|
||||
this->node_state = espbt::ClientState::ESTABLISHED;
|
||||
this->trigger();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// on_disconnect automation
|
||||
class BLEClientDisconnectTrigger final : public Trigger<>, public BLEClientNode {
|
||||
public:
|
||||
explicit BLEClientDisconnectTrigger(BLEClient *parent) { parent->register_ble_node(this); }
|
||||
void loop() override {}
|
||||
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
||||
esp_ble_gattc_cb_param_t *param) override {
|
||||
// test for CLOSE and not DISCONNECT - DISCONNECT can occur even if no virtual connection (OPEN event) occurred.
|
||||
// So this will not trigger unless a complete open has previously succeeded.
|
||||
switch (event) {
|
||||
case ESP_GATTC_SEARCH_CMPL_EVT: {
|
||||
this->node_state = espbt::ClientState::ESTABLISHED;
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_CLOSE_EVT: {
|
||||
this->trigger();
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class BLEClientPasskeyRequestTrigger final : public Trigger<>, public BLEClientServicelessNode {
|
||||
public:
|
||||
explicit BLEClientPasskeyRequestTrigger(BLEClient *parent) { parent->register_ble_node(this); }
|
||||
@@ -110,144 +58,6 @@ class BLEClientNumericComparisonRequestTrigger final : public Trigger<uint32_t>,
|
||||
}
|
||||
};
|
||||
|
||||
// implement the ble_client.ble_write action.
|
||||
template<typename... Ts> class BLEClientWriteAction final : public Action<Ts...>, public BLEClientNode {
|
||||
public:
|
||||
BLEClientWriteAction(BLEClient *ble_client) {
|
||||
ble_client->register_ble_node(this);
|
||||
ble_client_ = ble_client;
|
||||
}
|
||||
|
||||
void set_service_uuid16(uint16_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); }
|
||||
void set_service_uuid32(uint32_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); }
|
||||
void set_service_uuid128(uint8_t *uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_raw(uuid); }
|
||||
|
||||
void set_char_uuid16(uint16_t uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); }
|
||||
void set_char_uuid32(uint32_t uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); }
|
||||
void set_char_uuid128(uint8_t *uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_raw(uuid); }
|
||||
|
||||
void set_value_template(std::vector<uint8_t> (*func)(Ts...)) {
|
||||
this->value_.func = func;
|
||||
this->len_ = -1; // Sentinel value indicates template mode
|
||||
}
|
||||
|
||||
// Store pointer to static data in flash (no RAM copy)
|
||||
void set_value_simple(const uint8_t *data, size_t len) {
|
||||
this->value_.data = data;
|
||||
this->len_ = len; // Length >= 0 indicates static mode
|
||||
}
|
||||
|
||||
void play(const Ts &...x) override {}
|
||||
|
||||
void play_complex(const Ts &...x) override {
|
||||
this->num_running_++;
|
||||
this->var_ = std::make_tuple(x...);
|
||||
|
||||
bool result;
|
||||
if (this->len_ >= 0) {
|
||||
// Static mode: write directly from flash pointer
|
||||
result = this->write(this->value_.data, this->len_);
|
||||
} else {
|
||||
// Template mode: call function and write the vector
|
||||
std::vector<uint8_t> value = this->value_.func(x...);
|
||||
result = this->write(value);
|
||||
}
|
||||
|
||||
// on write failure, continue the automation chain rather than stopping so that e.g. disconnect can work.
|
||||
if (!result)
|
||||
this->play_next_(x...);
|
||||
}
|
||||
|
||||
/**
|
||||
* Note about logging: the esph_log_X macros are used here because the CI checks complain about use of the ESP LOG
|
||||
* macros in header files (Can't even write it in a comment!)
|
||||
* Not sure why, because they seem to work just fine.
|
||||
* The problem is that the implementation of a templated class can't be placed in a .cpp file when using C++ less than
|
||||
* 17, so the methods have to be here. The esph_log_X macros are equivalent in function, but don't trigger the CI
|
||||
* errors.
|
||||
*/
|
||||
// initiate the write. Return true if all went well, will be followed by a WRITE_CHAR event.
|
||||
bool write(const uint8_t *data, size_t len) {
|
||||
if (this->node_state != espbt::ClientState::ESTABLISHED) {
|
||||
esph_log_w(Automation::TAG, "Cannot write to BLE characteristic - not connected");
|
||||
return false;
|
||||
}
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
||||
char hex_buf[format_hex_pretty_size(BLE_WRITE_MAX_LOG_BYTES)];
|
||||
esph_log_vv(Automation::TAG, "Will write %d bytes: %s", len, format_hex_pretty_to(hex_buf, data, len));
|
||||
#endif
|
||||
esp_err_t err =
|
||||
esp_ble_gattc_write_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->char_handle_, len,
|
||||
const_cast<uint8_t *>(data), this->write_type_, ESP_GATT_AUTH_REQ_NONE);
|
||||
if (err != ESP_OK) {
|
||||
esph_log_e(Automation::TAG, "Error writing to characteristic: %s!", esp_err_to_name(err));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool write(const std::vector<uint8_t> &value) { return this->write(value.data(), value.size()); }
|
||||
|
||||
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
||||
esp_ble_gattc_cb_param_t *param) override {
|
||||
switch (event) {
|
||||
case ESP_GATTC_WRITE_CHAR_EVT:
|
||||
// upstream code checked the MAC address, verify the characteristic.
|
||||
if (param->write.handle == this->char_handle_)
|
||||
this->parent()->run_later([this]() { this->play_next_tuple_(this->var_); });
|
||||
break;
|
||||
case ESP_GATTC_DISCONNECT_EVT:
|
||||
if (this->num_running_ != 0)
|
||||
this->stop_complex();
|
||||
break;
|
||||
case ESP_GATTC_SEARCH_CMPL_EVT: {
|
||||
auto *chr = this->parent()->get_characteristic(this->service_uuid_, this->char_uuid_);
|
||||
if (chr == nullptr) {
|
||||
char char_buf[esp32_ble::UUID_STR_LEN];
|
||||
char service_buf[esp32_ble::UUID_STR_LEN];
|
||||
esph_log_w("ble_write_action", "Characteristic %s was not found in service %s",
|
||||
this->char_uuid_.to_str(char_buf), this->service_uuid_.to_str(service_buf));
|
||||
break;
|
||||
}
|
||||
this->char_handle_ = chr->handle;
|
||||
this->char_props_ = chr->properties;
|
||||
if (this->char_props_ & ESP_GATT_CHAR_PROP_BIT_WRITE) {
|
||||
this->write_type_ = ESP_GATT_WRITE_TYPE_RSP;
|
||||
esph_log_d(Automation::TAG, "Write type: ESP_GATT_WRITE_TYPE_RSP");
|
||||
} else if (this->char_props_ & ESP_GATT_CHAR_PROP_BIT_WRITE_NR) {
|
||||
this->write_type_ = ESP_GATT_WRITE_TYPE_NO_RSP;
|
||||
esph_log_d(Automation::TAG, "Write type: ESP_GATT_WRITE_TYPE_NO_RSP");
|
||||
} else {
|
||||
char char_buf[esp32_ble::UUID_STR_LEN];
|
||||
esph_log_e(Automation::TAG, "Characteristic %s does not allow writing", this->char_uuid_.to_str(char_buf));
|
||||
break;
|
||||
}
|
||||
this->node_state = espbt::ClientState::ESTABLISHED;
|
||||
char char_buf[esp32_ble::UUID_STR_LEN];
|
||||
esph_log_d(Automation::TAG, "Found characteristic %s on device %s", this->char_uuid_.to_str(char_buf),
|
||||
ble_client_->address_str());
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
BLEClient *ble_client_;
|
||||
ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length
|
||||
union Value {
|
||||
std::vector<uint8_t> (*func)(Ts...); // Function pointer (stateless lambdas)
|
||||
const uint8_t *data; // Pointer to static data in flash
|
||||
} value_;
|
||||
espbt::ESPBTUUID service_uuid_;
|
||||
espbt::ESPBTUUID char_uuid_;
|
||||
std::tuple<Ts...> var_{};
|
||||
uint16_t char_handle_{};
|
||||
esp_gatt_char_prop_t char_props_{};
|
||||
esp_gatt_write_type_t write_type_{};
|
||||
};
|
||||
|
||||
template<typename... Ts> class BLEClientPasskeyReplyAction final : public Action<Ts...> {
|
||||
public:
|
||||
BLEClientPasskeyReplyAction(BLEClient *ble_client) { parent_ = ble_client; }
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
// Neutral twins of the shared ble_client automations. Class names, namespace,
|
||||
// and codegen-visible signatures are IDENTICAL to automation.h so generated
|
||||
// main.cpp compiles against whichever engine the build gates in; only the
|
||||
// internals differ (client callbacks and the neutral node interface instead
|
||||
// of raw gattc events). The Bluedroid-security automations (passkey, numeric
|
||||
// comparison, remove bond) have no neutral equivalent and stay esp32-only.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
#if defined(USE_BLE_GATT_CLIENT) && !defined(USE_BLE_CLIENT_LEGACY_ENGINE)
|
||||
|
||||
#include <tuple>
|
||||
|
||||
#include "ble_client_gatt.h"
|
||||
#include "esphome/core/automation.h"
|
||||
|
||||
namespace esphome::ble_client {
|
||||
|
||||
template<typename... Ts> class BLEClientConnectAction final : public Action<Ts...> {
|
||||
public:
|
||||
BLEClientConnectAction(BLEClient *ble_client) {
|
||||
ble_client_ = ble_client;
|
||||
ble_client->add_on_connect_callback([this]() {
|
||||
if (this->num_running_ != 0)
|
||||
this->play_next_tuple_(this->var_);
|
||||
});
|
||||
// A connect attempt that dies (or a later disconnect) terminates the
|
||||
// chain - unless a fresh attempt is already in flight, meaning a user
|
||||
// trigger on this same list started this action (an on_disconnect that
|
||||
// reconnects) and the wait belongs to that attempt. Legacy split these:
|
||||
// it aborted on DISCONNECT_EVT, the trigger fired on the later CLOSE_EVT.
|
||||
auto terminate = [this]() {
|
||||
if (this->num_running_ != 0 && this->ble_client_->idle())
|
||||
this->stop_complex();
|
||||
};
|
||||
ble_client->add_on_connect_failed_callback(terminate);
|
||||
ble_client->add_on_disconnect_callback(terminate);
|
||||
}
|
||||
|
||||
// not used since we override play_complex_
|
||||
void play(const Ts &...x) override {}
|
||||
|
||||
void play_complex(const Ts &...x) override {
|
||||
// it makes no sense to have multiple instances of this running at the
|
||||
// same time; cancel a re-trigger while still running.
|
||||
if (this->num_running_ != 0) {
|
||||
this->stop_complex();
|
||||
return;
|
||||
}
|
||||
this->num_running_++;
|
||||
if (this->ble_client_->connected()) {
|
||||
this->play_next_(x...);
|
||||
} else {
|
||||
this->var_ = std::make_tuple(x...);
|
||||
// No-op while already connecting; the callback resolves the wait.
|
||||
this->ble_client_->connect();
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
BLEClient *ble_client_;
|
||||
std::tuple<Ts...> var_{};
|
||||
};
|
||||
|
||||
template<typename... Ts> class BLEClientDisconnectAction final : public Action<Ts...> {
|
||||
public:
|
||||
BLEClientDisconnectAction(BLEClient *ble_client) {
|
||||
ble_client_ = ble_client;
|
||||
// Both terminal outcomes resolve the wait: a completed teardown and a
|
||||
// connect attempt that died on the way down.
|
||||
ble_client->add_on_disconnect_callback([this]() {
|
||||
if (this->num_running_ != 0)
|
||||
this->play_next_tuple_(this->var_);
|
||||
});
|
||||
ble_client->add_on_connect_failed_callback([this]() {
|
||||
if (this->num_running_ != 0)
|
||||
this->play_next_tuple_(this->var_);
|
||||
});
|
||||
}
|
||||
|
||||
// not used since we override play_complex_
|
||||
void play(const Ts &...x) override {}
|
||||
|
||||
void play_complex(const Ts &...x) override {
|
||||
this->num_running_++;
|
||||
if (this->ble_client_->idle()) {
|
||||
this->play_next_(x...);
|
||||
} else {
|
||||
this->var_ = std::make_tuple(x...);
|
||||
this->ble_client_->disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
BLEClient *ble_client_;
|
||||
std::tuple<Ts...> var_{};
|
||||
};
|
||||
|
||||
} // namespace esphome::ble_client
|
||||
|
||||
#endif // USE_BLE_GATT_CLIENT && !USE_BLE_CLIENT_LEGACY_ENGINE
|
||||
@@ -2,10 +2,16 @@
|
||||
#include "esphome/components/esp32_ble_client/ble_client_base.h"
|
||||
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
#ifdef USE_BLE_CLIENT_LEGACY_ENGINE
|
||||
|
||||
#ifdef USE_BLE_CLIENT_GATT_NODES
|
||||
#include "esphome/components/bluetooth_connection/bluetooth_connection.h"
|
||||
#include "esphome/components/bluetooth_connection/gatt_service_table_bluedroid.h"
|
||||
#endif
|
||||
|
||||
namespace esphome::ble_client {
|
||||
|
||||
@@ -30,6 +36,10 @@ void BLEClient::dump_config() {
|
||||
bool BLEClient::parse_device(const espbt::ESPBTDevice &device) {
|
||||
if (!this->enabled)
|
||||
return false;
|
||||
#ifdef USE_BLE_CLIENT_GATT_NODES
|
||||
if (device.address_uint64() == this->address_ && this->gatt_backoff_.holding_off())
|
||||
return false;
|
||||
#endif
|
||||
return BLEClientBase::parse_device(device);
|
||||
}
|
||||
|
||||
@@ -40,24 +50,65 @@ void BLEClient::set_enabled(bool enabled) {
|
||||
if (!enabled) {
|
||||
ESP_LOGI(TAG, "[%s] Disabling BLE client.", this->address_str());
|
||||
this->disconnect();
|
||||
return;
|
||||
}
|
||||
#ifdef USE_BLE_CLIENT_GATT_NODES
|
||||
// A re-enable clears the backoff (neutral-engine parity).
|
||||
this->gatt_backoff_.reset();
|
||||
#endif
|
||||
}
|
||||
|
||||
bool BLEClient::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t esp_gattc_if,
|
||||
esp_ble_gattc_cb_param_t *param) {
|
||||
#ifdef USE_BLE_CLIENT_GATT_NODES
|
||||
// Bridge-initiated registrations bypass the base's REG_FOR_NOTIFY handling:
|
||||
// its automatic CCCD write would double the node's own.
|
||||
// Handle-keyed: mixed legacy/neutral subscriptions to one characteristic
|
||||
// are unsupported during the migration window.
|
||||
if (event == ESP_GATTC_REG_FOR_NOTIFY_EVT && esp_gattc_if == this->gattc_if_ &&
|
||||
this->take_pending_gatt_reg_(param->reg_for_notify.handle)) {
|
||||
if (this->pending_notify_regs_ > 0)
|
||||
this->pending_notify_regs_--;
|
||||
int err = param->reg_for_notify.status == ESP_GATT_OK ? 0 : param->reg_for_notify.status;
|
||||
this->notify_state_to_gatt_nodes_(param->reg_for_notify.handle, true, err);
|
||||
// A retiring last registration must still release the cache.
|
||||
this->maybe_release_services_();
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
if (!BLEClientBase::gattc_event_handler(event, esp_gattc_if, param))
|
||||
return false;
|
||||
|
||||
#ifdef USE_BLE_CLIENT_GATT_NODES
|
||||
// Before the legacy fan-out so gatt nodes resolve before any trigger fires.
|
||||
if (!this->gatt_nodes_.empty()) {
|
||||
if (event == ESP_GATTC_SEARCH_CMPL_EVT) {
|
||||
// A failed discovery tears the link down; the on_connect trigger must
|
||||
// not fire into the teardown.
|
||||
if (!this->handle_gatt_search_cmpl_(param->search_cmpl.status))
|
||||
return true;
|
||||
} else {
|
||||
this->dispatch_gatt_event_(event, param);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
for (auto *node : this->nodes_)
|
||||
node->gattc_event_handler(event, esp_gattc_if, param);
|
||||
// The automations run after the nodes; on_connect runs before the release so it can still use the services
|
||||
if (event == ESP_GATTC_SEARCH_CMPL_EVT)
|
||||
this->connect_callbacks_.call();
|
||||
this->maybe_release_services_();
|
||||
if (event == ESP_GATTC_CLOSE_EVT)
|
||||
this->disconnect_callbacks_.call();
|
||||
return true;
|
||||
}
|
||||
|
||||
void BLEClient::maybe_release_services_() {
|
||||
// The release frees the GATT cache that BLEClientBase's CCCD lookup still needs.
|
||||
// The last REG_FOR_NOTIFY event clears the counter before node dispatch, so the release still runs here.
|
||||
if (!this->services_.empty() && !this->notify_registration_pending() && this->all_nodes_established_()) {
|
||||
this->release_services();
|
||||
ESP_LOGD(TAG, "All clients established, services released");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void BLEClient::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
|
||||
@@ -65,10 +116,19 @@ void BLEClient::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_p
|
||||
|
||||
for (auto *node : this->nodes_)
|
||||
node->gap_event_handler(event, param);
|
||||
#ifdef USE_BLE_CLIENT_GATT_NODES
|
||||
if (event == ESP_GAP_BLE_AUTH_CMPL_EVT && this->check_addr(param->ble_security.auth_cmpl.bd_addr)) {
|
||||
int status = param->ble_security.auth_cmpl.success ? 0 : param->ble_security.auth_cmpl.fail_reason;
|
||||
for (auto *node : this->gatt_nodes_)
|
||||
node->on_pairing_result(status);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void BLEClient::set_state(espbt::ClientState state) {
|
||||
BLEClientBase::set_state(state);
|
||||
// ESTABLISHED never flows through here; gatt nodes are promoted after the
|
||||
// on_connected fan-out.
|
||||
for (auto &node : nodes_)
|
||||
node->node_state = state;
|
||||
}
|
||||
@@ -83,6 +143,221 @@ bool BLEClient::all_nodes_established_() {
|
||||
return true;
|
||||
}
|
||||
|
||||
#ifdef USE_BLE_CLIENT_GATT_NODES
|
||||
|
||||
void BLEClient::register_gatt_node(BLEClientNode *node) {
|
||||
// Parent before the capacity check so a dropped node still has a usable
|
||||
// parent() (neutral-engine parity).
|
||||
node->set_ble_client_parent(this);
|
||||
if (this->gatt_nodes_.size() == ESPHOME_BLE_CLIENT_MAX_NODES) {
|
||||
// push_back past capacity is a silent no-op; an undersized slot count
|
||||
// must be loud at boot, not an unresolvable node at runtime.
|
||||
ESP_LOGE(TAG, "[%s] Node capacity exceeded; node dropped", this->address_str());
|
||||
this->status_set_error(LOG_STR("node capacity exceeded"));
|
||||
return;
|
||||
}
|
||||
this->gatt_nodes_.push_back(node);
|
||||
// nodes_ covers the shared state bookkeeping; gatt_nodes_ is the neutral
|
||||
// fan-out subset.
|
||||
this->register_ble_node(node);
|
||||
}
|
||||
|
||||
int BLEClient::find_pending_gatt_reg_(uint16_t handle) const {
|
||||
for (uint8_t i = 0; i < this->pending_gatt_reg_count_; i++) {
|
||||
if (this->pending_gatt_regs_[i] == handle)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool BLEClient::take_pending_gatt_reg_(uint16_t handle) {
|
||||
int i = this->find_pending_gatt_reg_(handle);
|
||||
if (i < 0)
|
||||
return false;
|
||||
// No duplicates (notify_characteristic refuses a re-push); swap-with-last.
|
||||
this->pending_gatt_regs_[i] = this->pending_gatt_regs_[--this->pending_gatt_reg_count_];
|
||||
return true;
|
||||
}
|
||||
|
||||
void BLEClient::notify_state_to_gatt_nodes_(uint16_t handle, bool enabled, int error) {
|
||||
if (error != 0) {
|
||||
ESP_LOGW(TAG, "[%s] Notify %s on handle 0x%04x failed, status=%d", this->address_str(),
|
||||
enabled ? "enable" : "disable", handle, error);
|
||||
}
|
||||
for (auto *node : this->gatt_nodes_)
|
||||
node->on_notify_state(handle, enabled, error);
|
||||
}
|
||||
|
||||
void BLEClient::dispatch_gatt_event_(esp_gattc_cb_event_t event, esp_ble_gattc_cb_param_t *param) {
|
||||
switch (event) {
|
||||
case ESP_GATTC_READ_CHAR_EVT:
|
||||
case ESP_GATTC_READ_DESCR_EVT: {
|
||||
bool ok = param->read.status == ESP_GATT_OK;
|
||||
if (!ok) {
|
||||
// Breadcrumb even when no node claims the handle.
|
||||
ESP_LOGD(TAG, "[%s] Read on handle 0x%04x completed with status %d", this->address_str(), param->read.handle,
|
||||
param->read.status);
|
||||
}
|
||||
for (auto *node : this->gatt_nodes_) {
|
||||
node->on_read_result(param->read.handle, ok ? param->read.value : nullptr, ok ? param->read.value_len : 0,
|
||||
ok ? 0 : param->read.status);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_WRITE_CHAR_EVT:
|
||||
case ESP_GATTC_WRITE_DESCR_EVT:
|
||||
if (param->write.status != ESP_GATT_OK) {
|
||||
// Breadcrumb even when no node claims the handle.
|
||||
ESP_LOGD(TAG, "[%s] Write on handle 0x%04x completed with status %d", this->address_str(), param->write.handle,
|
||||
param->write.status);
|
||||
}
|
||||
for (auto *node : this->gatt_nodes_) {
|
||||
node->on_write_result(param->write.handle, param->write.status == ESP_GATT_OK ? 0 : param->write.status);
|
||||
}
|
||||
break;
|
||||
case ESP_GATTC_NOTIFY_EVT:
|
||||
for (auto *node : this->gatt_nodes_) {
|
||||
node->on_notify(param->notify.handle, param->notify.value, param->notify.value_len);
|
||||
}
|
||||
break;
|
||||
case ESP_GATTC_UNREG_FOR_NOTIFY_EVT:
|
||||
// The base does no CCCD work for unregister; no interception needed.
|
||||
this->notify_state_to_gatt_nodes_(
|
||||
param->unreg_for_notify.handle, false,
|
||||
param->unreg_for_notify.status == ESP_GATT_OK ? 0 : param->unreg_for_notify.status);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool BLEClient::handle_gatt_search_cmpl_(esp_gatt_status_t status) {
|
||||
// The base ignores the search status; the neutral contract must not.
|
||||
uint16_t service_total = 0;
|
||||
bool counted = status == ESP_GATT_OK && bluetooth_connection::BluedroidServiceTable::count_services(
|
||||
this->gattc_if_, this->conn_id_, &service_total);
|
||||
if (!counted || service_total == 0) {
|
||||
// A failed search poisons the whole discovery, legacy nodes included.
|
||||
ESP_LOGW(TAG, "[%s] Discovery failed (status=%d, services=%u)", this->address_str(), status, service_total);
|
||||
this->gatt_backoff_.register_failure(this->address_str());
|
||||
this->disconnect();
|
||||
return false;
|
||||
}
|
||||
// Stack-owned; nodes copy their handles during on_connected().
|
||||
bluetooth_connection::BluedroidServiceTable table;
|
||||
if (!table.build(this->gattc_if_, this->conn_id_, service_total, this->connection_index_)) {
|
||||
if (!this->has_legacy_nodes_()) {
|
||||
ESP_LOGW(TAG, "[%s] Service table build failed; treating as failed discovery", this->address_str());
|
||||
this->gatt_backoff_.register_failure(this->address_str());
|
||||
this->disconnect();
|
||||
return false;
|
||||
}
|
||||
// Only the table build failed; legacy nodes read the base's services_
|
||||
// and keep the link. Gatt nodes catch the next connection.
|
||||
ESP_LOGW(TAG, "[%s] Service table build failed; gatt nodes skip this connection", this->address_str());
|
||||
this->status_set_warning(LOG_STR("gatt nodes inactive: service table build failed"));
|
||||
// A peer whose database never walks must not retry the build at
|
||||
// advertisement cadence once the legacy nodes drop the link.
|
||||
this->gatt_backoff_.register_failure(this->address_str());
|
||||
} else {
|
||||
this->gatt_connected_ = true;
|
||||
auto view = table.view();
|
||||
for (auto *node : this->gatt_nodes_) {
|
||||
node->on_connected(view);
|
||||
if (this->state() != espbt::ClientState::ESTABLISHED) {
|
||||
// The node tore the link down; remaining nodes get on_disconnected
|
||||
// with no preceding on_connected, so leave a trace of why.
|
||||
ESP_LOGW(TAG, "[%s] A node aborted the connection during setup", this->address_str());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
this->gatt_backoff_.reset();
|
||||
this->status_clear_warning();
|
||||
}
|
||||
// Promote so the legacy release condition can fire.
|
||||
for (auto *node : this->gatt_nodes_)
|
||||
node->node_state = espbt::ClientState::ESTABLISHED;
|
||||
return true;
|
||||
}
|
||||
|
||||
void BLEClient::on_disconnect_complete(esp_err_t reason) {
|
||||
this->pending_gatt_reg_count_ = 0;
|
||||
if (!this->gatt_connected_)
|
||||
return; // Never-established links report nothing (neutral parity).
|
||||
this->gatt_connected_ = false;
|
||||
for (auto *node : this->gatt_nodes_)
|
||||
node->on_disconnected();
|
||||
}
|
||||
|
||||
int BLEClient::check_and_log_error_(const char *operation, esp_err_t err) {
|
||||
if (err != ESP_OK)
|
||||
this->log_gattc_warning_(operation, err);
|
||||
return err;
|
||||
}
|
||||
|
||||
int BLEClient::write_characteristic(uint16_t handle, const uint8_t *data, uint16_t len, bool response) {
|
||||
if (this->conn_id_ == UNSET_CONN_ID)
|
||||
return ble_device_base::GATT_ERR_NOT_CONNECTED;
|
||||
return this->check_and_log_error_(
|
||||
"esp_ble_gattc_write_char",
|
||||
esp_ble_gattc_write_char(this->gattc_if_, this->conn_id_, handle, len, const_cast<uint8_t *>(data),
|
||||
response ? ESP_GATT_WRITE_TYPE_RSP : ESP_GATT_WRITE_TYPE_NO_RSP,
|
||||
ESP_GATT_AUTH_REQ_NONE));
|
||||
}
|
||||
|
||||
int BLEClient::read_characteristic(uint16_t handle) {
|
||||
if (this->conn_id_ == UNSET_CONN_ID)
|
||||
return ble_device_base::GATT_ERR_NOT_CONNECTED;
|
||||
return this->check_and_log_error_("esp_ble_gattc_read_char", esp_ble_gattc_read_char(this->gattc_if_, this->conn_id_,
|
||||
handle, ESP_GATT_AUTH_REQ_NONE));
|
||||
}
|
||||
|
||||
int BLEClient::read_descriptor(uint16_t handle) {
|
||||
if (this->conn_id_ == UNSET_CONN_ID)
|
||||
return ble_device_base::GATT_ERR_NOT_CONNECTED;
|
||||
return this->check_and_log_error_(
|
||||
"esp_ble_gattc_read_char_descr",
|
||||
esp_ble_gattc_read_char_descr(this->gattc_if_, this->conn_id_, handle, ESP_GATT_AUTH_REQ_NONE));
|
||||
}
|
||||
|
||||
int BLEClient::write_descriptor(uint16_t handle, const uint8_t *data, uint16_t len) {
|
||||
if (this->conn_id_ == UNSET_CONN_ID)
|
||||
return ble_device_base::GATT_ERR_NOT_CONNECTED;
|
||||
return this->check_and_log_error_(
|
||||
"esp_ble_gattc_write_char_descr",
|
||||
esp_ble_gattc_write_char_descr(this->gattc_if_, this->conn_id_, handle, len, const_cast<uint8_t *>(data),
|
||||
ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE));
|
||||
}
|
||||
|
||||
int BLEClient::notify_characteristic(uint16_t handle, bool enable) {
|
||||
if (this->conn_id_ == UNSET_CONN_ID)
|
||||
return ble_device_base::GATT_ERR_NOT_CONNECTED;
|
||||
if (enable) {
|
||||
if (this->find_pending_gatt_reg_(handle) >= 0) {
|
||||
// ESP_OK: the in-flight registration's completion fans out to all nodes.
|
||||
ESP_LOGW(TAG, "[%s] Notify registration already pending for handle 0x%04x", this->address_str(), handle);
|
||||
return ESP_OK;
|
||||
}
|
||||
if (this->pending_gatt_reg_count_ == MAX_PENDING_NOTIFY_REGS) {
|
||||
// An untracked registration would let the base's auto-CCCD through.
|
||||
ESP_LOGE(TAG, "[%s] Too many pending notify registrations", this->address_str());
|
||||
return ble_device_base::GATT_ERR_NO_MEMORY;
|
||||
}
|
||||
// The base helper's pending count holds the service-release until the
|
||||
// (intercepted) completion.
|
||||
esp_err_t err = this->register_for_notify(handle);
|
||||
if (err == ESP_OK)
|
||||
this->pending_gatt_regs_[this->pending_gatt_reg_count_++] = handle;
|
||||
return this->check_and_log_error_("esp_ble_gattc_register_for_notify", err);
|
||||
}
|
||||
return this->check_and_log_error_("esp_ble_gattc_unregister_for_notify",
|
||||
esp_ble_gattc_unregister_for_notify(this->gattc_if_, this->remote_bda_, handle));
|
||||
}
|
||||
|
||||
int BLEClient::unpair() { return bluetooth_connection::unpair_device(this->get_address()); }
|
||||
|
||||
#endif // USE_BLE_CLIENT_GATT_NODES
|
||||
|
||||
} // namespace esphome::ble_client
|
||||
|
||||
#endif
|
||||
@@ -1,18 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
#ifdef USE_BLE_CLIENT_LEGACY_ENGINE
|
||||
|
||||
#include "ble_client_node.h"
|
||||
#include "connect_backoff.h"
|
||||
#include "esphome/components/esp32_ble_client/ble_client_base.h"
|
||||
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include <esp_bt_defs.h>
|
||||
#include <esp_gap_ble_api.h>
|
||||
#include <esp_gatt_common_api.h>
|
||||
#include <esp_gattc_api.h>
|
||||
#include <array>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace esphome::ble_client {
|
||||
@@ -21,34 +23,6 @@ namespace espbt = esphome::esp32_ble_tracker;
|
||||
|
||||
using namespace esp32_ble_client;
|
||||
|
||||
class BLEClient;
|
||||
|
||||
class BLEClientNode {
|
||||
public:
|
||||
virtual void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
||||
esp_ble_gattc_cb_param_t *param) {};
|
||||
virtual void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {}
|
||||
virtual void loop() {}
|
||||
void set_address(uint64_t address) { address_ = address; }
|
||||
espbt::ESPBTClient *client;
|
||||
// This should be transitioned to Established once the node no longer needs
|
||||
// the services/descriptors/characteristics of the parent client. This will
|
||||
// allow some memory to be freed.
|
||||
// The parent frees the peer's GATT cache once every node reports Established.
|
||||
// Never report Established while an operation that reads that cache is outstanding.
|
||||
// - esp_ble_gattc_register_for_notify() completes asynchronously.
|
||||
// - Register from ESP_GATTC_SEARCH_CMPL_EVT, then set this from ESP_GATTC_REG_FOR_NOTIFY_EVT.
|
||||
// - BLEClientBase::register_for_notify() holds the release until the registration completes.
|
||||
espbt::ClientState node_state;
|
||||
|
||||
BLEClient *parent() { return this->parent_; }
|
||||
void set_ble_client_parent(BLEClient *parent) { this->parent_ = parent; }
|
||||
|
||||
protected:
|
||||
BLEClient *parent_;
|
||||
uint64_t address_;
|
||||
};
|
||||
|
||||
class BLEClient final : public BLEClientBase {
|
||||
public:
|
||||
void setup() override;
|
||||
@@ -64,19 +38,75 @@ class BLEClient final : public BLEClientBase {
|
||||
void set_enabled(bool enabled);
|
||||
|
||||
void register_ble_node(BLEClientNode *node) {
|
||||
node->client = this;
|
||||
node->set_ble_client_parent(this);
|
||||
this->nodes_.push_back(node);
|
||||
}
|
||||
|
||||
// on_connect fires once discovery completes; on_disconnect on CLOSE_EVT, so only after a completed open
|
||||
template<typename F> void add_on_connect_callback(F &&callback) {
|
||||
this->connect_callbacks_.add(std::forward<F>(callback));
|
||||
}
|
||||
template<typename F> void add_on_disconnect_callback(F &&callback) {
|
||||
this->disconnect_callbacks_.add(std::forward<F>(callback));
|
||||
}
|
||||
|
||||
bool enabled;
|
||||
|
||||
void set_state(espbt::ClientState state) override;
|
||||
|
||||
#ifdef USE_BLE_CLIENT_GATT_NODES
|
||||
// ---- the neutral node surface (signatures shared with the non-esp32
|
||||
// engine, so nodes on the neutral interface compile against either) ----
|
||||
void register_gatt_node(BLEClientNode *node);
|
||||
|
||||
bool idle() const { return this->state() == espbt::ClientState::IDLE; }
|
||||
|
||||
int write_characteristic(uint16_t handle, const uint8_t *data, uint16_t len, bool response);
|
||||
int read_characteristic(uint16_t handle);
|
||||
int read_descriptor(uint16_t handle);
|
||||
int write_descriptor(uint16_t handle, const uint8_t *data, uint16_t len);
|
||||
/// Local registration only; per the neutral contract the CCCD write is the
|
||||
/// node's job (the legacy auto-CCCD is suppressed for these handles).
|
||||
int notify_characteristic(uint16_t handle, bool enable);
|
||||
// pair() comes from BLEClientBase, matching the neutral engine's.
|
||||
int unpair();
|
||||
#endif
|
||||
|
||||
protected:
|
||||
bool all_nodes_established_();
|
||||
void maybe_release_services_();
|
||||
#ifdef USE_BLE_CLIENT_GATT_NODES
|
||||
int check_and_log_error_(const char *operation, esp_err_t err);
|
||||
int find_pending_gatt_reg_(uint16_t handle) const;
|
||||
void notify_state_to_gatt_nodes_(uint16_t handle, bool enabled, int error);
|
||||
void dispatch_gatt_event_(esp_gattc_cb_event_t event, esp_ble_gattc_cb_param_t *param);
|
||||
// False = failed discovery: the link comes down and the caller suppresses
|
||||
// the legacy fan-out.
|
||||
bool handle_gatt_search_cmpl_(esp_gatt_status_t status);
|
||||
bool take_pending_gatt_reg_(uint16_t handle);
|
||||
void on_disconnect_complete(esp_err_t reason) override;
|
||||
#endif
|
||||
|
||||
std::vector<BLEClientNode *> nodes_;
|
||||
LazyCallbackManager<void()> connect_callbacks_;
|
||||
LazyCallbackManager<void()> disconnect_callbacks_;
|
||||
#ifdef USE_BLE_CLIENT_GATT_NODES
|
||||
// Raise if a migrated node needs more concurrent registrations.
|
||||
static constexpr uint8_t MAX_PENDING_NOTIFY_REGS = 4;
|
||||
|
||||
// Nodes on the neutral surface; fed the translated callbacks and
|
||||
// auto-established after the on_connected fan-out. Every gatt node is
|
||||
// also in nodes_ (registration pushes into both).
|
||||
StaticVector<BLEClientNode *, ESPHOME_BLE_CLIENT_MAX_NODES> gatt_nodes_;
|
||||
bool has_legacy_nodes_() const { return this->nodes_.size() > this->gatt_nodes_.size(); }
|
||||
// Reconnect backoff after materializer failures.
|
||||
ConnectBackoff gatt_backoff_;
|
||||
// Bridge-initiated notify registrations awaiting REG_FOR_NOTIFY_EVT.
|
||||
uint16_t pending_gatt_regs_[MAX_PENDING_NOTIFY_REGS];
|
||||
uint8_t pending_gatt_reg_count_{0};
|
||||
// on_connected fan-out started; on_disconnected is owed at teardown.
|
||||
bool gatt_connected_{false};
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace esphome::ble_client
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
#include "ble_client_gatt.h"
|
||||
|
||||
#if defined(USE_BLE_GATT_CLIENT) && !defined(USE_BLE_CLIENT_LEGACY_ENGINE)
|
||||
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::ble_client {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "ble_client");
|
||||
|
||||
void BLEClient::register_ble_node(BLEClientNode *node) {
|
||||
node->set_ble_client_parent(this);
|
||||
if (this->nodes_.size() == ESPHOME_BLE_CLIENT_MAX_NODES) {
|
||||
// push_back past capacity is a silent no-op; an undersized slot count
|
||||
// must be loud at boot, not an unresolvable node at runtime.
|
||||
ESP_LOGE(TAG, "[%s] Node capacity exceeded; node dropped", this->address_str_);
|
||||
this->status_set_error(LOG_STR("node capacity exceeded"));
|
||||
return;
|
||||
}
|
||||
this->nodes_.push_back(node);
|
||||
}
|
||||
|
||||
void BLEClient::set_address(uint64_t address) {
|
||||
this->address_ = address;
|
||||
uint8_t mac[6];
|
||||
ble_device_base::uint64_to_mac_msb_first(address, mac);
|
||||
format_mac_addr_upper(mac, this->address_str_);
|
||||
}
|
||||
|
||||
void BLEClient::set_enabled(bool enabled) {
|
||||
if (enabled == this->enabled)
|
||||
return;
|
||||
ESP_LOGI(TAG, "[%s] %s", this->address_str_, enabled ? "Enabled" : "Disabled");
|
||||
this->enabled = enabled;
|
||||
if (!enabled) {
|
||||
this->disconnect();
|
||||
return;
|
||||
}
|
||||
// A re-enable clears the backoff; the next sighting connects (legacy
|
||||
// parity: enabling does not itself connect).
|
||||
this->backoff_.reset();
|
||||
}
|
||||
|
||||
bool BLEClient::parse_device(const ble_device_base::ESPBTDevice &device) {
|
||||
if (device.address_uint64() != this->address_)
|
||||
return false;
|
||||
// The sighting is the source of truth for the address type.
|
||||
this->address_type_ = device.get_address_type();
|
||||
this->address_type_known_ = true;
|
||||
if (!this->enabled || !this->auto_connect_ || this->state_ != State::IDLE)
|
||||
return true;
|
||||
if (this->backoff_.holding_off())
|
||||
return true;
|
||||
this->attempt_connect_();
|
||||
return true;
|
||||
}
|
||||
|
||||
void BLEClient::connect() {
|
||||
if (this->state_ != State::IDLE) {
|
||||
ESP_LOGD(TAG, "[%s] Connect requested while busy, ignoring", this->address_str_);
|
||||
return;
|
||||
}
|
||||
// An absent peer can inhibit scanning for the backend's full connect
|
||||
// timeout, so this is worth a breadcrumb - but it is a supported action.
|
||||
ESP_LOGI(TAG, "[%s] Connecting on request", this->address_str_);
|
||||
if (!this->address_type_known_) {
|
||||
// Legacy parity: without a sighting the address type defaults to
|
||||
// public, which never matches a random-static peer.
|
||||
ESP_LOGW(TAG, "[%s] No sighting yet; assuming a public address type", this->address_str_);
|
||||
}
|
||||
this->attempt_connect_();
|
||||
}
|
||||
|
||||
void BLEClient::attempt_connect_() {
|
||||
int err = this->backend_->connect(this->address_, this->address_type_);
|
||||
if (err != 0) {
|
||||
// A refused connect never produces a callback: stay idle, charge the
|
||||
// backoff, and resolve any waiting connect action through the failure
|
||||
// path so its chain terminates.
|
||||
ESP_LOGW(TAG, "[%s] Connect refused, err=%d", this->address_str_, err);
|
||||
this->backoff_.register_failure(this->address_str_);
|
||||
this->defer([this]() { this->connect_failed_callbacks_.call(); });
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "[%s] Connecting", this->address_str_);
|
||||
this->state_ = State::CONNECTING;
|
||||
}
|
||||
|
||||
void BLEClient::disconnect() {
|
||||
if (this->state_ == State::IDLE) {
|
||||
ESP_LOGD(TAG, "[%s] Disconnect requested while idle, ignoring", this->address_str_);
|
||||
return;
|
||||
}
|
||||
if (this->cancel_requested_) {
|
||||
// The accepted teardown still reports; a refused second gatt_disconnect()
|
||||
// would settle the link twice (phantom connect failure, backoff charge).
|
||||
ESP_LOGD(TAG, "[%s] Disconnect already in flight, ignoring", this->address_str_);
|
||||
return;
|
||||
}
|
||||
// A deliberate teardown's failure report must not feed the backoff.
|
||||
this->cancel_requested_ = true;
|
||||
int err = this->backend_->gatt_disconnect();
|
||||
if (err != 0) {
|
||||
// Refused synchronously: backend and client disagree about the link
|
||||
// state. Warn, then settle through the deliberate-cancel path.
|
||||
ESP_LOGW(TAG, "[%s] Disconnect refused, err=%d; settling locally", this->address_str_, err);
|
||||
this->on_connection_state(false, 0, err);
|
||||
}
|
||||
}
|
||||
|
||||
void BLEClient::on_connection_state(bool connected, uint16_t mtu, int error) {
|
||||
if (connected) {
|
||||
this->state_ = State::DISCOVERING;
|
||||
int discover_err = this->backend_->discover_services();
|
||||
if (discover_err != 0) {
|
||||
// Synchronous refusal: no discovery completion will follow.
|
||||
ESP_LOGW(TAG, "[%s] Service discovery refused, err=%d", this->address_str_, discover_err);
|
||||
this->backoff_.register_failure(this->address_str_);
|
||||
// Deliberate teardown: its report must not charge the backoff again.
|
||||
this->disconnect();
|
||||
}
|
||||
return;
|
||||
}
|
||||
bool was_connected = this->state_ == State::CONNECTED;
|
||||
bool cancelled = this->cancel_requested_;
|
||||
this->cancel_requested_ = false;
|
||||
this->state_ = State::IDLE;
|
||||
if (was_connected) {
|
||||
ESP_LOGI(TAG, "[%s] Disconnected, status=%d", this->address_str_, error);
|
||||
for (auto *node : this->nodes_) {
|
||||
node->on_disconnected();
|
||||
}
|
||||
// Continuations leave the backend's event-drain stack first.
|
||||
this->defer([this]() { this->disconnect_callbacks_.call(); });
|
||||
} else {
|
||||
if (cancelled) {
|
||||
// status carries the refusal code when the teardown settled
|
||||
// synchronously; 0 on a backend-completed cancel.
|
||||
ESP_LOGD(TAG, "[%s] Connect attempt cancelled, status=%d", this->address_str_, error);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "[%s] Connect failed, status=%d", this->address_str_, error);
|
||||
this->backoff_.register_failure(this->address_str_);
|
||||
}
|
||||
this->defer([this]() { this->connect_failed_callbacks_.call(); });
|
||||
}
|
||||
}
|
||||
|
||||
void BLEClient::on_service_discovery_done(int error) {
|
||||
if (this->state_ != State::DISCOVERING || this->cancel_requested_) {
|
||||
// Raced a teardown; the terminal report settles the link.
|
||||
ESP_LOGD(TAG, "[%s] Discovery completed during teardown, ignoring", this->address_str_);
|
||||
this->backend_->release_services();
|
||||
return;
|
||||
}
|
||||
if (error != 0) {
|
||||
ESP_LOGW(TAG, "[%s] Service discovery failed, status=%d", this->address_str_, error);
|
||||
this->backoff_.register_failure(this->address_str_);
|
||||
// The teardown is deliberate: do not charge the backoff again for its
|
||||
// connection report.
|
||||
this->disconnect();
|
||||
return;
|
||||
}
|
||||
ble_device_base::GattServiceTable table{};
|
||||
if (!this->nodes_.empty()) {
|
||||
// Materialize only when a node will read it: a client with no nodes
|
||||
// would pay the build/free cycle on every (re)connect for nothing.
|
||||
table = this->backend_->get_service_table();
|
||||
if (table.service_count == 0) {
|
||||
// A failed materialization is indistinguishable from a service-less
|
||||
// peer, and a real GATT peer always exposes at least GAP/GATT: fail
|
||||
// the discovery before CONNECTED so the teardown resolves through
|
||||
// connect_failed, never a spurious on_disconnect.
|
||||
ESP_LOGW(TAG, "[%s] Service table is empty; treating as failed discovery", this->address_str_);
|
||||
this->backend_->release_services();
|
||||
this->backoff_.register_failure(this->address_str_);
|
||||
this->disconnect();
|
||||
return;
|
||||
}
|
||||
}
|
||||
// CONNECTED before the fan-out so nodes may consult connected() from
|
||||
// their own on_connected().
|
||||
this->state_ = State::CONNECTED;
|
||||
for (auto *node : this->nodes_) {
|
||||
node->on_connected(table);
|
||||
if (this->state_ != State::CONNECTED || this->cancel_requested_) {
|
||||
// A node tore the link down mid-fan-out: on_disconnect fires with no
|
||||
// preceding on_connect, so leave a trace of why.
|
||||
ESP_LOGW(TAG, "[%s] A node aborted the connection during setup", this->address_str_);
|
||||
this->backend_->release_services();
|
||||
return;
|
||||
}
|
||||
}
|
||||
this->backend_->release_services();
|
||||
this->backoff_.reset();
|
||||
ESP_LOGI(TAG, "[%s] Connected", this->address_str_);
|
||||
this->defer([this]() { this->connect_callbacks_.call(); });
|
||||
}
|
||||
|
||||
void BLEClient::on_write_result(uint16_t handle, int error) {
|
||||
if (error != 0) {
|
||||
// Breadcrumb even when no node claims the handle.
|
||||
ESP_LOGD(TAG, "[%s] Write on handle 0x%04x completed with status %d", this->address_str_, handle, error);
|
||||
}
|
||||
for (auto *node : this->nodes_) {
|
||||
node->on_write_result(handle, error);
|
||||
}
|
||||
}
|
||||
|
||||
void BLEClient::on_read_result(uint16_t handle, const uint8_t *data, uint16_t len, int error) {
|
||||
if (error != 0) {
|
||||
// Breadcrumb even when no node claims the handle.
|
||||
ESP_LOGD(TAG, "[%s] Read on handle 0x%04x completed with status %d", this->address_str_, handle, error);
|
||||
}
|
||||
for (auto *node : this->nodes_) {
|
||||
node->on_read_result(handle, data, len, error);
|
||||
}
|
||||
}
|
||||
|
||||
void BLEClient::on_notify_data(uint16_t handle, const uint8_t *data, uint16_t len) {
|
||||
// Every node sees every notification and filters by handle (legacy parity).
|
||||
for (auto *node : this->nodes_) {
|
||||
node->on_notify(handle, data, len);
|
||||
}
|
||||
}
|
||||
|
||||
void BLEClient::on_notify_state(uint16_t handle, bool enabled, int error) {
|
||||
if (error != 0) {
|
||||
ESP_LOGW(TAG, "[%s] Notify %s on handle 0x%04x failed, status=%d", this->address_str_,
|
||||
enabled ? "enable" : "disable", handle, error);
|
||||
}
|
||||
for (auto *node : this->nodes_) {
|
||||
node->on_notify_state(handle, enabled, error);
|
||||
}
|
||||
}
|
||||
|
||||
void BLEClient::on_pairing_result(int status) {
|
||||
if (status != 0) {
|
||||
ESP_LOGW(TAG, "[%s] Pairing failed, status=%d", this->address_str_, status);
|
||||
} else {
|
||||
ESP_LOGI(TAG, "[%s] Paired", this->address_str_);
|
||||
}
|
||||
for (auto *node : this->nodes_) {
|
||||
node->on_pairing_result(status);
|
||||
}
|
||||
}
|
||||
|
||||
void BLEClient::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"BLE Client:\n"
|
||||
" Address: %s\n"
|
||||
" Auto connect: %s",
|
||||
this->address_str_, YESNO(this->auto_connect_));
|
||||
if (this->enabled && this->state_ == State::IDLE) {
|
||||
ESP_LOGCONFIG(TAG, " Waiting for an advertisement from the device");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::ble_client
|
||||
|
||||
#endif // USE_BLE_GATT_CLIENT && !USE_BLE_CLIENT_LEGACY_ENGINE
|
||||
@@ -0,0 +1,151 @@
|
||||
// Platform-neutral ble_client engine on the ble_device_base GATT contract.
|
||||
//
|
||||
// Compiled on every platform with a GATT backend except esp32, which keeps
|
||||
// the legacy BLEClientBase engine (ble_client.h) until its raw-gattc node
|
||||
// family migrates - the exclusive gates make the same class names resolve to
|
||||
// exactly one definition per build, so codegen is shared.
|
||||
//
|
||||
// Connects are sighting-gated like the legacy engine: the client is a parsed
|
||||
// advertisement listener, captures the peer's address type from the sighting,
|
||||
// and asks the backend to connect only when enabled and idle.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
#if defined(USE_BLE_GATT_CLIENT) && !defined(USE_BLE_CLIENT_LEGACY_ENGINE)
|
||||
|
||||
#include "ble_client_node.h"
|
||||
#include "connect_backoff.h"
|
||||
#include "esphome/components/ble_device_base/ble_device.h"
|
||||
#include "esphome/components/ble_device_base/ble_gatt_client.h"
|
||||
#include "esphome/components/bluetooth_connection/bluetooth_connection.h"
|
||||
#include "esphome/components/bluetooth_connection/bluetooth_connection_gatt_backend.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
|
||||
namespace esphome::ble_client {
|
||||
|
||||
class BLEClient : public Component,
|
||||
public ble_device_base::ESPBTDeviceListener,
|
||||
public ble_device_base::GattClientListener {
|
||||
public:
|
||||
void dump_config() override;
|
||||
|
||||
// Public field for legacy parity (the switch platform republishes it).
|
||||
bool enabled{true};
|
||||
|
||||
void set_backend(ble_device_base::BLEGattConnection *backend) {
|
||||
this->backend_ = backend;
|
||||
backend->set_listener(this);
|
||||
}
|
||||
void set_address(uint64_t address);
|
||||
void set_auto_connect(bool auto_connect) { this->auto_connect_ = auto_connect; }
|
||||
void set_enabled(bool enabled);
|
||||
const char *address_str() const { return this->address_str_; }
|
||||
|
||||
void register_ble_node(BLEClientNode *node);
|
||||
// One registration spelling shared with the esp32 engine's bridge.
|
||||
void register_gatt_node(BLEClientNode *node) { this->register_ble_node(node); }
|
||||
|
||||
// An accepted teardown already owns the link: the pending report cannot be
|
||||
// a connect. state_ stays put so that report still fires on_disconnect.
|
||||
bool connected() const { return this->state_ == State::CONNECTED && !this->cancel_requested_; }
|
||||
bool idle() const { return this->state_ == State::IDLE; }
|
||||
|
||||
/// Action-initiated connect (no sighting needed; uses the last captured
|
||||
/// address type, public until a sighting arrives). No-op unless idle.
|
||||
void connect();
|
||||
void disconnect();
|
||||
|
||||
/// Legacy-named deferral used by the automation twins: neutral listener
|
||||
/// callbacks run inside the backend's event drain, so automation chain
|
||||
/// continuations must leave that stack first.
|
||||
void run_later(std::function<void()> &&f) { this->defer(std::move(f)); } // NOLINT
|
||||
|
||||
// Backend ops for nodes and actions - the frozen node-facing surface.
|
||||
// Only write_characteristic has an in-tree caller; subscribing means
|
||||
// notify_characteristic plus a CCCD write_descriptor (the caller's job
|
||||
// per the contract).
|
||||
int write_characteristic(uint16_t handle, const uint8_t *data, uint16_t len, bool response) {
|
||||
return this->backend_->write_characteristic(handle, data, len, response);
|
||||
}
|
||||
int read_characteristic(uint16_t handle) { return this->backend_->read_characteristic(handle); }
|
||||
int read_descriptor(uint16_t handle) { return this->backend_->read_descriptor(handle); }
|
||||
int write_descriptor(uint16_t handle, const uint8_t *data, uint16_t len) {
|
||||
return this->backend_->write_descriptor(handle, data, len);
|
||||
}
|
||||
int notify_characteristic(uint16_t handle, bool enable) {
|
||||
return this->backend_->notify_characteristic(handle, enable);
|
||||
}
|
||||
int pair() { return this->backend_->pair(); }
|
||||
int unpair() { return bluetooth_connection::unpair_device(this->address_); }
|
||||
|
||||
// Automation callback registration.
|
||||
template<typename F> void add_on_connect_callback(F &&callback) {
|
||||
this->connect_callbacks_.add(std::forward<F>(callback));
|
||||
}
|
||||
template<typename F> void add_on_disconnect_callback(F &&callback) {
|
||||
this->disconnect_callbacks_.add(std::forward<F>(callback));
|
||||
}
|
||||
// Fired when a connect attempt dies before being established; the user
|
||||
// on_disconnect trigger deliberately does NOT fire here (legacy parity).
|
||||
template<typename F> void add_on_connect_failed_callback(F &&callback) {
|
||||
this->connect_failed_callbacks_.add(std::forward<F>(callback));
|
||||
}
|
||||
|
||||
// ---- ble_device_base::ESPBTDeviceListener ----
|
||||
bool parse_device(const ble_device_base::ESPBTDevice &device) override;
|
||||
|
||||
// ---- ble_device_base::GattClientListener ----
|
||||
void on_connection_state(bool connected, uint16_t mtu, int error) override;
|
||||
void on_service_discovery_done(int error) override;
|
||||
void on_read_result(uint16_t handle, const uint8_t *data, uint16_t len, int error) override;
|
||||
void on_write_result(uint16_t handle, int error) override;
|
||||
void on_notify_data(uint16_t handle, const uint8_t *data, uint16_t len) override;
|
||||
void on_notify_state(uint16_t handle, bool enabled, int error) override;
|
||||
void on_pairing_result(int status) override;
|
||||
|
||||
protected:
|
||||
enum class State : uint8_t { IDLE, CONNECTING, DISCOVERING, CONNECTED };
|
||||
|
||||
void attempt_connect_();
|
||||
|
||||
// Group 1: pointers / containers
|
||||
ble_device_base::BLEGattConnection *backend_{nullptr};
|
||||
// Codegen-sized (ESPHOME_BLE_CLIENT_MAX_NODES); filled during setup.
|
||||
StaticVector<BLEClientNode *, ESPHOME_BLE_CLIENT_MAX_NODES> nodes_;
|
||||
|
||||
// Group 2: 8-byte types
|
||||
uint64_t address_{0};
|
||||
|
||||
// Group 3: callback managers (pointer-sized when empty)
|
||||
LazyCallbackManager<void()> connect_callbacks_;
|
||||
LazyCallbackManager<void()> disconnect_callbacks_;
|
||||
LazyCallbackManager<void()> connect_failed_callbacks_;
|
||||
|
||||
// Group 4: 4-byte types
|
||||
// Backoff so an undiscoverable database or a dead peer cannot produce a
|
||||
// battery-draining connect loop.
|
||||
ConnectBackoff backoff_;
|
||||
|
||||
// Group 5: arrays
|
||||
char address_str_[MAC_ADDRESS_PRETTY_BUFFER_SIZE]{};
|
||||
|
||||
// Group 6: 1-byte types
|
||||
State state_{State::IDLE};
|
||||
uint8_t address_type_{0}; // BLE_ADDR_TYPE_*, captured from the sighting
|
||||
// Distinguishes a captured public type from the never-sighted default.
|
||||
bool address_type_known_{false};
|
||||
bool auto_connect_{true};
|
||||
// A user-initiated teardown in flight; its failure report is not a
|
||||
// connect failure and must not feed the backoff.
|
||||
bool cancel_requested_{false};
|
||||
};
|
||||
|
||||
} // namespace esphome::ble_client
|
||||
|
||||
#endif // USE_BLE_GATT_CLIENT && !USE_BLE_CLIENT_LEGACY_ENGINE
|
||||
@@ -0,0 +1,68 @@
|
||||
// The single BLEClientNode both ble_client engines share. The neutral
|
||||
// callback surface is the one interface node components build on; the raw
|
||||
// esp32 surface below it remains for components that have not migrated yet.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
#ifdef USE_BLE_GATT_CLIENT
|
||||
#include "esphome/components/ble_device_base/ble_gatt_client.h"
|
||||
#endif
|
||||
#ifdef USE_BLE_CLIENT_LEGACY_ENGINE
|
||||
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
|
||||
|
||||
#include <esp_gap_ble_api.h>
|
||||
#include <esp_gattc_api.h>
|
||||
#endif
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace esphome::ble_client {
|
||||
|
||||
class BLEClient;
|
||||
|
||||
class BLEClientNode {
|
||||
public:
|
||||
#ifdef USE_BLE_CLIENT_GATT_NODES
|
||||
// Neutral surface, delivered by both engines. The table is borrowed: copy
|
||||
// handles during on_connected(). All nodes see all completions; filter by
|
||||
// handle.
|
||||
// A node that disconnects from inside on_connected() aborts the fan-out;
|
||||
// the user's on_disconnect may then fire without a preceding on_connect.
|
||||
virtual void on_connected(const ble_device_base::GattServiceTable &table) {}
|
||||
virtual void on_disconnected() {}
|
||||
virtual void on_notify(uint16_t handle, const uint8_t *data, uint16_t len) {}
|
||||
// One in-flight registration per handle; its completion fans out to every
|
||||
// node, so a refused duplicate request still sees on_notify_state.
|
||||
virtual void on_notify_state(uint16_t handle, bool enabled, int error) {}
|
||||
virtual void on_read_result(uint16_t handle, const uint8_t *data, uint16_t len, int error) {}
|
||||
virtual void on_write_result(uint16_t handle, int error) {}
|
||||
virtual void on_pairing_result(int status) {}
|
||||
#endif
|
||||
#ifdef USE_BLE_CLIENT_LEGACY_ENGINE
|
||||
// Legacy raw surface; components overriding these need the legacy engine
|
||||
// until migrated to the neutral surface above.
|
||||
virtual void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
||||
esp_ble_gattc_cb_param_t *param) {}
|
||||
virtual void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {}
|
||||
virtual void loop() {}
|
||||
// This should be transitioned to Established once the node no longer needs
|
||||
// the services/descriptors/characteristics of the parent client. This will
|
||||
// allow some memory to be freed.
|
||||
// The parent frees the peer's GATT cache once every node reports Established.
|
||||
// Never report Established while an operation that reads that cache is outstanding.
|
||||
// - esp_ble_gattc_register_for_notify() completes asynchronously.
|
||||
// - Register from ESP_GATTC_SEARCH_CMPL_EVT, then set this from ESP_GATTC_REG_FOR_NOTIFY_EVT.
|
||||
// - BLEClientBase::register_for_notify() holds the release until the registration completes.
|
||||
esp32_ble_tracker::ClientState node_state;
|
||||
#endif
|
||||
|
||||
BLEClient *parent() const { return this->parent_; }
|
||||
void set_ble_client_parent(BLEClient *parent) { this->parent_ = parent; }
|
||||
|
||||
protected:
|
||||
BLEClient *parent_{nullptr};
|
||||
};
|
||||
|
||||
} // namespace esphome::ble_client
|
||||
@@ -0,0 +1,172 @@
|
||||
// The ble_client.ble_write action: a node on the platform-neutral interface,
|
||||
// so one implementation serves both engines (the esp32 bridge and the
|
||||
// neutral engine).
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
#ifdef USE_BLE_CLIENT_GATT_NODES
|
||||
|
||||
#include <tuple>
|
||||
#include <vector>
|
||||
|
||||
// One of the two engine headers resolves per build.
|
||||
#include "ble_client.h"
|
||||
#include "ble_client_gatt.h"
|
||||
#include "ble_client_node.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::ble_client {
|
||||
|
||||
static const char *const BLE_WRITE_TAG = "ble_client.automation";
|
||||
|
||||
// Maximum bytes to log in hex format for BLE writes (many logging buffers are 256 chars)
|
||||
static constexpr size_t BLE_WRITE_MAX_LOG_BYTES = 64;
|
||||
|
||||
template<typename... Ts> class BLEClientWriteAction final : public Action<Ts...>, public BLEClientNode {
|
||||
public:
|
||||
BLEClientWriteAction(BLEClient *ble_client) {
|
||||
ble_client->register_gatt_node(this);
|
||||
ble_client_ = ble_client;
|
||||
}
|
||||
|
||||
void set_service_uuid16(uint16_t uuid) { this->service_uuid_ = ble_device_base::ESPBTUUID::from_uint16(uuid); }
|
||||
void set_service_uuid32(uint32_t uuid) { this->service_uuid_ = ble_device_base::ESPBTUUID::from_uint32(uuid); }
|
||||
void set_service_uuid128(uint8_t *uuid) { this->service_uuid_ = ble_device_base::ESPBTUUID::from_raw(uuid); }
|
||||
|
||||
void set_char_uuid16(uint16_t uuid) { this->char_uuid_ = ble_device_base::ESPBTUUID::from_uint16(uuid); }
|
||||
void set_char_uuid32(uint32_t uuid) { this->char_uuid_ = ble_device_base::ESPBTUUID::from_uint32(uuid); }
|
||||
void set_char_uuid128(uint8_t *uuid) { this->char_uuid_ = ble_device_base::ESPBTUUID::from_raw(uuid); }
|
||||
|
||||
void set_value_template(std::vector<uint8_t> (*func)(Ts...)) {
|
||||
this->value_.func = func;
|
||||
this->len_ = -1; // Sentinel value indicates template mode
|
||||
}
|
||||
|
||||
// Store pointer to static data in flash (no RAM copy)
|
||||
void set_value_simple(const uint8_t *data, size_t len) {
|
||||
this->value_.data = data;
|
||||
this->len_ = len; // Length >= 0 indicates static mode
|
||||
}
|
||||
|
||||
void play(const Ts &...x) override {}
|
||||
|
||||
void play_complex(const Ts &...x) override {
|
||||
this->num_running_++;
|
||||
this->var_ = std::make_tuple(x...);
|
||||
|
||||
bool result;
|
||||
if (this->len_ >= 0) {
|
||||
result = this->write(this->value_.data, this->len_);
|
||||
} else {
|
||||
std::vector<uint8_t> value = this->value_.func(x...);
|
||||
result = this->write(value.data(), value.size());
|
||||
}
|
||||
|
||||
// on write failure, continue the automation chain rather than stopping so
|
||||
// that e.g. disconnect can work.
|
||||
if (!result)
|
||||
this->play_next_(x...);
|
||||
}
|
||||
|
||||
// Initiate the write; the completion arrives in on_write_result. The
|
||||
// response-less path can complete synchronously inside the call, so the
|
||||
// handle is armed before the backend is touched.
|
||||
bool write(const uint8_t *data, size_t len) {
|
||||
if (!this->ble_client_->connected()) {
|
||||
esph_log_w(BLE_WRITE_TAG, "Cannot write to BLE characteristic - not connected");
|
||||
return false;
|
||||
}
|
||||
if (!this->resolved_) {
|
||||
esph_log_w(BLE_WRITE_TAG, "Cannot write to BLE characteristic - characteristic was not resolved");
|
||||
return false;
|
||||
}
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
||||
char hex_buf[format_hex_pretty_size(BLE_WRITE_MAX_LOG_BYTES)];
|
||||
esph_log_vv(BLE_WRITE_TAG, "Will write %d bytes: %s", len, format_hex_pretty_to(hex_buf, data, len));
|
||||
#endif
|
||||
int err = this->ble_client_->write_characteristic(this->char_handle_, data, len, this->write_response_);
|
||||
if (err != 0) {
|
||||
esph_log_e(BLE_WRITE_TAG, "Error writing to characteristic: %d!", err);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void on_connected(const ble_device_base::GattServiceTable &table) override {
|
||||
// Resolution comes from this table only; a re-discovery must not keep a
|
||||
// stale handle.
|
||||
this->resolved_ = false;
|
||||
this->char_handle_ = 0;
|
||||
const auto *service = ble_device_base::find_service(table, this->service_uuid_);
|
||||
const auto *chr =
|
||||
service == nullptr ? nullptr : ble_device_base::find_characteristic(table, *service, this->char_uuid_);
|
||||
if (chr == nullptr) {
|
||||
char char_buf[ble_device_base::UUID_STR_LEN];
|
||||
char service_buf[ble_device_base::UUID_STR_LEN];
|
||||
esph_log_w(BLE_WRITE_TAG, "Characteristic %s was not found in service %s", this->char_uuid_.to_str(char_buf),
|
||||
this->service_uuid_.to_str(service_buf));
|
||||
return;
|
||||
}
|
||||
if (chr->properties & ble_device_base::GATT_CHAR_PROP_WRITE) {
|
||||
this->write_response_ = true;
|
||||
} else if (chr->properties & ble_device_base::GATT_CHAR_PROP_WRITE_NO_RSP) {
|
||||
this->write_response_ = false;
|
||||
} else {
|
||||
char char_buf[ble_device_base::UUID_STR_LEN];
|
||||
esph_log_e(BLE_WRITE_TAG, "Characteristic %s does not allow writing", this->char_uuid_.to_str(char_buf));
|
||||
return;
|
||||
}
|
||||
this->char_handle_ = chr->value_handle;
|
||||
this->resolved_ = true;
|
||||
char char_buf[ble_device_base::UUID_STR_LEN];
|
||||
esph_log_d(BLE_WRITE_TAG, "Found characteristic %s on device %s", this->char_uuid_.to_str(char_buf),
|
||||
this->ble_client_->address_str());
|
||||
}
|
||||
|
||||
void on_disconnected() override {
|
||||
this->resolved_ = false;
|
||||
this->char_handle_ = 0;
|
||||
if (this->num_running_ != 0)
|
||||
this->stop_complex();
|
||||
}
|
||||
|
||||
void on_write_result(uint16_t handle, int error) override {
|
||||
if (this->num_running_ == 0) {
|
||||
return;
|
||||
}
|
||||
if (!this->resolved_ || handle != this->char_handle_) {
|
||||
// A parked chain waiting on a completion that never matches would
|
||||
// otherwise stall silently until disconnect.
|
||||
esph_log_d(BLE_WRITE_TAG, "Write result for handle 0x%04x ignored, waiting on 0x%04x", handle,
|
||||
this->char_handle_);
|
||||
return;
|
||||
}
|
||||
if (error != 0) {
|
||||
// Continue the chain (legacy parity) but leave a breadcrumb.
|
||||
esph_log_w(BLE_WRITE_TAG, "Write completed with status %d", error);
|
||||
}
|
||||
this->ble_client_->run_later([this]() { this->play_next_tuple_(this->var_); });
|
||||
}
|
||||
|
||||
private:
|
||||
BLEClient *ble_client_;
|
||||
ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length
|
||||
union Value {
|
||||
std::vector<uint8_t> (*func)(Ts...); // Function pointer (stateless lambdas)
|
||||
const uint8_t *data; // Pointer to static data in flash
|
||||
} value_;
|
||||
ble_device_base::ESPBTUUID service_uuid_;
|
||||
ble_device_base::ESPBTUUID char_uuid_;
|
||||
std::tuple<Ts...> var_{};
|
||||
uint16_t char_handle_{};
|
||||
bool write_response_{false};
|
||||
bool resolved_{false};
|
||||
};
|
||||
|
||||
} // namespace esphome::ble_client
|
||||
|
||||
#endif // USE_BLE_CLIENT_GATT_NODES
|
||||
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
#ifdef USE_BLE_GATT_CLIENT
|
||||
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace esphome::ble_client {
|
||||
|
||||
/// Reconnect backoff after repeated connect/discovery failures, shared by
|
||||
/// both engines. 256 ms ticks in a uint16_t keep it 4 bytes; the ~4.7 h tick
|
||||
/// wrap can at worst reinstate one stale hold-off of a minute.
|
||||
class ConnectBackoff {
|
||||
public:
|
||||
bool holding_off() const {
|
||||
return this->failures_ != 0 && static_cast<uint16_t>(now() - this->start_) < this->failures_ * STEP_TICKS;
|
||||
}
|
||||
void register_failure(const char *address_str) {
|
||||
if (this->failures_ < MAX_STEPS)
|
||||
this->failures_++;
|
||||
this->start_ = now();
|
||||
esph_log_w("ble_client", "[%s] Holding off reconnect for %u s", address_str, this->failures_ * 10u);
|
||||
}
|
||||
void reset() { this->failures_ = 0; }
|
||||
|
||||
private:
|
||||
// ~10 s per consecutive failure, capped so a flapping peer retries within
|
||||
// a minute at worst.
|
||||
static constexpr uint16_t STEP_TICKS = 40; // x 256 ms
|
||||
static constexpr uint8_t MAX_STEPS = 6;
|
||||
static uint16_t now() { return static_cast<uint16_t>(millis() >> 8); }
|
||||
|
||||
uint16_t start_{0};
|
||||
uint8_t failures_{0};
|
||||
};
|
||||
|
||||
} // namespace esphome::ble_client
|
||||
|
||||
#endif // USE_BLE_GATT_CLIENT
|
||||
@@ -0,0 +1,48 @@
|
||||
#include "ble_gatt_client.h"
|
||||
|
||||
#ifdef USE_BLE_GATT_CLIENT
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::ble_device_base {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "ble_device_base");
|
||||
|
||||
const GattCharacteristic *find_characteristic(const GattServiceTable &table, const GattService &service,
|
||||
const ESPBTUUID &uuid) {
|
||||
// 32-bit range math: a corrupt first/count pair cannot wrap past the check.
|
||||
uint32_t end = uint32_t(service.first_characteristic) + service.characteristic_count;
|
||||
if (end > table.characteristic_count) {
|
||||
ESP_LOGW(TAG, "characteristic range out of bounds");
|
||||
return nullptr;
|
||||
}
|
||||
for (uint32_t i = service.first_characteristic; i < end; i++) {
|
||||
if (table.characteristics[i].uuid == uuid)
|
||||
return &table.characteristics[i];
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const GattDescriptor *find_descriptor(const GattServiceTable &table, const GattCharacteristic &characteristic,
|
||||
const ESPBTUUID &uuid) {
|
||||
uint32_t end = uint32_t(characteristic.first_descriptor) + characteristic.descriptor_count;
|
||||
if (end > table.descriptor_count) {
|
||||
// Corrupt range, not a missing descriptor.
|
||||
ESP_LOGW(TAG, "descriptor range out of bounds");
|
||||
return nullptr;
|
||||
}
|
||||
for (uint32_t i = characteristic.first_descriptor; i < end; i++) {
|
||||
if (table.descriptors[i].uuid == uuid)
|
||||
return &table.descriptors[i];
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
uint16_t find_cccd(const GattServiceTable &table, const GattCharacteristic &characteristic) {
|
||||
const GattDescriptor *desc = find_descriptor(table, characteristic, ESPBTUUID::from_uint16(CCCD_UUID));
|
||||
return desc != nullptr ? desc->handle : 0;
|
||||
}
|
||||
|
||||
} // namespace esphome::ble_device_base
|
||||
|
||||
#endif // USE_BLE_GATT_CLIENT
|
||||
@@ -11,13 +11,16 @@
|
||||
// interface. All listener calls are delivered on the ESPHome main loop;
|
||||
// borrowed data pointers are valid only for the duration of the call.
|
||||
//
|
||||
// Error domain (plain int, forwarded to the API without translation):
|
||||
// Error domain (plain int, forwarded to the API without translation, so the
|
||||
// values are wire-frozen - API clients interpret them):
|
||||
// 0 success
|
||||
// 1..0x11 ATT error codes (Bluetooth spec; BTstack and Bluedroid agree)
|
||||
// 1..0x11 ATT error codes (Bluetooth spec) - reserved; a backend whose
|
||||
// native error codes land in this window must remap them out
|
||||
// GATT_ERR_NOT_CONNECTED (-1) no connection to the peer (on esp32 a raw
|
||||
// ESP_FAIL from the stack shares this value; both read as a
|
||||
// failed, unusable connection on the client side)
|
||||
// GATT_ERR_NO_MEMORY (-2) backend storage exhausted
|
||||
// -1..-15 reserved for future contract sentinels
|
||||
// anything else: platform stack error/status code, surfaced opaquely.
|
||||
// Connection events carry HCI status/disconnect reason codes (same code
|
||||
// space on every controller).
|
||||
@@ -99,9 +102,23 @@ class GattClientListener {
|
||||
// The BLEGattConnection op surface, asserted where the alias binds
|
||||
// (bluetooth_connection_gatt_backend.h). Operations return 0 when accepted (completion arrives
|
||||
// through the listener) or a synchronous error (busy, not connected, stack
|
||||
// rejection); one operation may be outstanding at a time. Semantics beyond
|
||||
// the signatures:
|
||||
// rejection); one operation may be outstanding at a time. A connection-state
|
||||
// or discovery completion always arrives from the event loop, never
|
||||
// synchronously from inside the op call - a synchronous terminal
|
||||
// on_connection_state from within gatt_disconnect() would re-enter the
|
||||
// consumer mid-teardown. A local-only completion may instead report inline,
|
||||
// before the op returns: notify_characteristic (a subscription-list edit on
|
||||
// the rp2 backend) and a write without response whose payload the stack took
|
||||
// immediately. Consumers must therefore arm whatever state their
|
||||
// on_write_result/on_notify_state reads before issuing the op, never after
|
||||
// the call returns. Semantics beyond the signatures:
|
||||
// - connect: addr_type is a BLE_ADDR_TYPE_* constant (ble_device.h).
|
||||
// Returning 0 means the request is accepted, not that the radio acted: the
|
||||
// backend owns integration with its platform's scan/connect arbitration
|
||||
// (Bluedroid parks the request for the tracker's promote loop, which owns
|
||||
// scan-stop/coex/one-connect-at-a-time; the rp2 backend opens immediately
|
||||
// and relies on sighting-gated consumers). Consumers must not assume
|
||||
// connect timing.
|
||||
// - gatt_disconnect: also cancels a connect in progress (named to coexist
|
||||
// with a platform stack's own void disconnect() on one backend class).
|
||||
// Nonzero means nothing to tear down and no completion will follow; an
|
||||
@@ -143,6 +160,41 @@ concept BLEGattConnectionContract = requires(T conn, GattClientListener *listene
|
||||
{ conn.set_connection_type(ConnectionType{}) } -> std::same_as<void>;
|
||||
};
|
||||
|
||||
// ---- service table lookup helpers ----
|
||||
//
|
||||
// Neutral, bounds-checked walks over a materialized GattServiceTable for
|
||||
// direct consumers that resolve a known device's handles by UUID (streaming
|
||||
// consumers forward the raw database and never need these). Linear search:
|
||||
// the table exists only between discovery and release_services(), for one
|
||||
// small known device.
|
||||
|
||||
/// Client Characteristic Configuration descriptor UUID (Bluetooth spec).
|
||||
static constexpr uint16_t CCCD_UUID = 0x2902;
|
||||
|
||||
// Characteristic property bits (the Bluetooth-spec declaration byte carried
|
||||
// in GattCharacteristic::properties; the ESP-IDF macros for these do not
|
||||
// exist on the other platforms).
|
||||
static constexpr uint8_t GATT_CHAR_PROP_WRITE_NO_RSP = 0x04;
|
||||
static constexpr uint8_t GATT_CHAR_PROP_WRITE = 0x08;
|
||||
|
||||
inline const GattService *find_service(const GattServiceTable &table, const ESPBTUUID &uuid) {
|
||||
for (uint16_t i = 0; i < table.service_count; i++) {
|
||||
if (table.services[i].uuid == uuid)
|
||||
return &table.services[i];
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const GattCharacteristic *find_characteristic(const GattServiceTable &table, const GattService &service,
|
||||
const ESPBTUUID &uuid);
|
||||
|
||||
const GattDescriptor *find_descriptor(const GattServiceTable &table, const GattCharacteristic &characteristic,
|
||||
const ESPBTUUID &uuid);
|
||||
|
||||
/// Handle of the characteristic's Client Characteristic Configuration
|
||||
/// descriptor (0x2902), or 0 when it has none.
|
||||
uint16_t find_cccd(const GattServiceTable &table, const GattCharacteristic &characteristic);
|
||||
|
||||
} // namespace esphome::ble_device_base
|
||||
|
||||
#endif // USE_BLE_GATT_CLIENT
|
||||
@@ -1,12 +1,14 @@
|
||||
"""Per-platform GATT connection backends and the helpers to embed one.
|
||||
|
||||
Backends: esp32 Bluedroid, rp2 BTstack. No user-facing configuration; the
|
||||
Bluetooth proxy's codegen declares and registers the backend instances
|
||||
through gatt_client_schema()/hub_connection_schema() + new_gatt_backend().
|
||||
Backends: esp32 Bluedroid, rp2 BTstack. No user-facing configuration; a
|
||||
consumer's codegen declares and registers the backend instances — the
|
||||
Bluetooth proxy through its per-slot connection wrappers (a streaming
|
||||
consumer), and the neutral ble_client through gatt_client_schema() +
|
||||
new_gatt_backend().
|
||||
"""
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import rp2040_ble
|
||||
@@ -23,7 +25,8 @@ from esphome.types import ConfigType
|
||||
def AUTO_LOAD() -> list[str]:
|
||||
"""ble_device_base plus the platform BLE stack the build's backend
|
||||
registers with (the Bluedroid header includes the tracker's), so
|
||||
consumers need not know. The platform-less arm serves manifest tooling."""
|
||||
consumers stay platform-blind. The platform-less arm serves tooling that
|
||||
resolves the manifest without a target."""
|
||||
if CORE.is_esp32:
|
||||
return ["ble_device_base", "esp32_ble_tracker"]
|
||||
if CORE.is_rp2:
|
||||
@@ -63,6 +66,8 @@ DOMAIN = "bluetooth_connection"
|
||||
@dataclass
|
||||
class _ConnectionData:
|
||||
rp2_backend_count: int = 0
|
||||
# GATT connection slots claimed this run, for the platform cap check.
|
||||
slot_consumers: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
def _get_data() -> _ConnectionData:
|
||||
@@ -119,6 +124,10 @@ class _PlatformBackend:
|
||||
backend_class: cg.MockObjClass
|
||||
schema_fragment: Callable[[], cv.Schema]
|
||||
register: Callable[[cg.MockObj, ConfigType], Awaitable[None]]
|
||||
# Selects the backend's alias-ladder arm (order-independent arms).
|
||||
define: str
|
||||
# The backend's on-demand materializer gate, when it has one.
|
||||
materializer_define: str | None = None
|
||||
|
||||
|
||||
# The single registry of platforms with a GATT client backend; a platform
|
||||
@@ -126,11 +135,20 @@ class _PlatformBackend:
|
||||
# platform's arm.
|
||||
_PLATFORM_BACKENDS: dict[str, _PlatformBackend] = {
|
||||
PLATFORM_ESP32: _PlatformBackend(
|
||||
BluedroidGattClient, _esp32_schema_fragment, _esp32_register
|
||||
BluedroidGattClient,
|
||||
_esp32_schema_fragment,
|
||||
_esp32_register,
|
||||
"USE_BLE_GATT_BACKEND_BLUEDROID",
|
||||
materializer_define="USE_BLUEDROID_GATT_SERVICE_TABLE",
|
||||
),
|
||||
PLATFORM_RP2: _PlatformBackend(
|
||||
RP2GattClient, _rp2_schema_fragment, _rp2_register, "USE_BLE_GATT_BACKEND_RP2"
|
||||
),
|
||||
PLATFORM_RP2: _PlatformBackend(RP2GattClient, _rp2_schema_fragment, _rp2_register),
|
||||
}
|
||||
|
||||
# Gates dedicated-backend consumers (cv.only_on).
|
||||
GATT_CLIENT_PLATFORMS = list(_PLATFORM_BACKENDS)
|
||||
|
||||
|
||||
def _backend_entry(platform: str | None = None) -> _PlatformBackend:
|
||||
key = platform if platform is not None else CORE.target_platform
|
||||
@@ -165,21 +183,97 @@ def hub_connection_schema(platform: str | None = None) -> cv.Schema:
|
||||
)
|
||||
|
||||
|
||||
async def new_gatt_backend(config: ConfigType) -> cg.MockObj:
|
||||
def _charge_esp32_budget(count: int, consumer: str, config: ConfigType) -> None:
|
||||
from esphome.components import esp32_ble
|
||||
|
||||
esp32_ble.consume_connection_slots(count, consumer)(config)
|
||||
|
||||
|
||||
def _charge_rp2_budget(count: int, consumer: str, config: ConfigType) -> None:
|
||||
rp2040_ble.consume_connection_slots(count, consumer)(config)
|
||||
|
||||
|
||||
# Platforms whose BLE stack owns its own connection budget: the claim is
|
||||
# charged there and that stack's final validation is the one place an
|
||||
# overcommit is reported (never two messages for one misconfiguration). One
|
||||
# mapping drives both, so a platform can never be skipped without a charge.
|
||||
_STACK_BUDGETS: dict[str, Callable[[int, str, ConfigType], None]] = {
|
||||
PLATFORM_ESP32: _charge_esp32_budget,
|
||||
PLATFORM_RP2: _charge_rp2_budget,
|
||||
}
|
||||
|
||||
|
||||
def consume_gatt_slot(
|
||||
consumer: str, count: int = 1
|
||||
) -> Callable[[ConfigType], ConfigType]:
|
||||
"""Validator claiming GATT connection slots - the one spelling for every
|
||||
claimant. The neutral ledger covers backend platforms without a stack
|
||||
budget (the cap check in FINAL_VALIDATE_SCHEMA)."""
|
||||
|
||||
def validator(config: ConfigType) -> ConfigType:
|
||||
_get_data().slot_consumers.extend([consumer] * count)
|
||||
if (charge := _STACK_BUDGETS.get(CORE.target_platform)) is not None:
|
||||
charge(count, consumer, config)
|
||||
return config
|
||||
|
||||
return validator
|
||||
|
||||
|
||||
def _validate_slot_totals(config: ConfigType) -> ConfigType:
|
||||
# Skipped in testing mode so grouped component builds can co-exist
|
||||
# (mirrors esp32_ble.validate_connection_slots).
|
||||
if CORE.testing_mode:
|
||||
return config
|
||||
if CORE.target_platform in _STACK_BUDGETS:
|
||||
return config
|
||||
if (cap := HUB_MAX_CONNECTIONS.get(CORE.target_platform)) is None:
|
||||
# Any backend platform without a stack budget must carry a cap here
|
||||
# or fail loudly, never fail open.
|
||||
if CORE.target_platform in _PLATFORM_BACKENDS:
|
||||
raise cv.Invalid(
|
||||
f"{CORE.target_platform} has a GATT backend but no slot cap "
|
||||
"in HUB_MAX_CONNECTIONS"
|
||||
)
|
||||
return config
|
||||
claimed = _get_data().slot_consumers
|
||||
if len(claimed) > cap:
|
||||
raise cv.Invalid(
|
||||
f"{CORE.target_platform} supports at most {cap} GATT client "
|
||||
f"connection(s); {len(claimed)} requested by: {', '.join(claimed)}"
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _validate_slot_totals
|
||||
|
||||
|
||||
async def new_gatt_backend(
|
||||
config: ConfigType, *, service_table: bool = True
|
||||
) -> cg.MockObj:
|
||||
"""Instantiate the backend declared by gatt_client_schema() and register
|
||||
it with its platform stack. The connection slot is claimed at validation
|
||||
(the proxy's slot validators), not here.
|
||||
(the consume_gatt_slot validators), not here.
|
||||
|
||||
service_table is honored by the Bluedroid backend only: forward
|
||||
scaffolding for the first esp32 direct consumer, load-bearing on no
|
||||
current build (rp2 ignores the define and always materializes - its
|
||||
proxy hub streams through get_service_table(), so it must keep the
|
||||
materializer regardless of the flag).
|
||||
"""
|
||||
from esphome.components import ble_device_base
|
||||
|
||||
entry = _backend_entry()
|
||||
ble_device_base.request_gatt_client()
|
||||
cg.add_define(entry.define)
|
||||
if service_table and entry.materializer_define is not None:
|
||||
cg.add_define(entry.materializer_define)
|
||||
backend = cg.new_Pvariable(config[CONF_BACKEND_ID])
|
||||
# The backend is the slot's real Component: component keys from the
|
||||
# connection entry (setup_priority, ...) apply to it. Consumers whose own
|
||||
# schema carries keys that register_component would misapply to the
|
||||
# backend (e.g. a polling interval) must not put them in this config.
|
||||
await cg.register_component(backend, config)
|
||||
await _backend_entry().register(backend, config)
|
||||
await entry.register(backend, config)
|
||||
return backend
|
||||
|
||||
|
||||
@@ -187,6 +281,7 @@ async def new_gatt_backend(config: ConfigType) -> cg.MockObj:
|
||||
# list (this module cannot import bluetooth_proxy to derive it).
|
||||
SOURCE_FILE_FRAMEWORKS: dict[str, set[PlatformFramework]] = {
|
||||
"bluetooth_connection_bluedroid.cpp": frameworks_for_platforms([PLATFORM_ESP32]),
|
||||
"gatt_service_table_bluedroid.cpp": frameworks_for_platforms([PLATFORM_ESP32]),
|
||||
# Every hub platform the proxy admits (the file compiles empty where
|
||||
# USE_BLE_GATT_CLIENT is not defined), so a platform gaining a backend
|
||||
# cannot hit a missing-symbol trap here.
|
||||
|
||||
@@ -46,7 +46,7 @@ BatchClose close_service_batch(api::BluetoothGATTGetServicesResponse &resp, size
|
||||
|
||||
#endif // USE_BLUETOOTH_PROXY_CONNECTIONS
|
||||
|
||||
#if defined(USE_ESP32) && defined(USE_BLE_GATT_CLIENT)
|
||||
#if defined(USE_ESP32_BLE) && defined(USE_BLE_GATT_CLIENT)
|
||||
namespace esphome::bluetooth_connection {
|
||||
|
||||
// Address-scoped Bluedroid maintenance. Gated with the connection surface:
|
||||
@@ -65,4 +65,4 @@ conn_err_t clear_gatt_cache(uint64_t address) {
|
||||
}
|
||||
|
||||
} // namespace esphome::bluetooth_connection
|
||||
#endif // USE_ESP32 && USE_BLE_GATT_CLIENT
|
||||
#endif // USE_ESP32_BLE && USE_BLE_GATT_CLIENT
|
||||
@@ -49,15 +49,16 @@ static constexpr conn_err_t CONN_OK = 0;
|
||||
// GATT contract so backend and wrapper cannot drift.
|
||||
static constexpr conn_err_t GATT_NOT_CONNECTED = ble_device_base::GATT_ERR_NOT_CONNECTED;
|
||||
|
||||
// What the platform's connection backend supports beyond GATT operations;
|
||||
// the proxy derives its feature flags and legacy version from these.
|
||||
#if defined(USE_ESP32)
|
||||
// What the build's connection backend supports beyond GATT operations; the
|
||||
// proxy derives its feature flags and legacy version from these. Keyed on
|
||||
// the backend define, never the platform, so a second backend on one
|
||||
// platform carries its own facts.
|
||||
#if defined(USE_BLE_GATT_BACKEND_BLUEDROID)
|
||||
static constexpr bool SUPPORTS_PAIRING = true;
|
||||
static constexpr bool SUPPORTS_CACHE_CLEARING = true;
|
||||
#elif defined(USE_RP2040_BLE) && defined(USE_BLE_GATT_CLIENT)
|
||||
#elif defined(USE_BLE_GATT_BACKEND_RP2)
|
||||
// The rp2 BTstack backend pairs (just works + bonding); it has no service
|
||||
// cache to clear. Keyed on the backend, not the generic client define, so a
|
||||
// future backend without pairing keeps the stub arm below.
|
||||
// cache to clear.
|
||||
static constexpr bool SUPPORTS_PAIRING = true;
|
||||
static constexpr bool SUPPORTS_CACHE_CLEARING = false;
|
||||
#else
|
||||
@@ -65,13 +66,14 @@ static constexpr bool SUPPORTS_PAIRING = false;
|
||||
static constexpr bool SUPPORTS_CACHE_CLEARING = false;
|
||||
#endif
|
||||
|
||||
// Address-scoped (not connection-scoped) maintenance requests.
|
||||
#if (defined(USE_ESP32) || defined(USE_RP2040_BLE)) && defined(USE_BLE_GATT_CLIENT)
|
||||
// Address-scoped (not connection-scoped) maintenance requests; keyed on the
|
||||
// stack (the calls need no backend instance).
|
||||
#if (defined(USE_ESP32_BLE) || defined(USE_RP2040_BLE)) && defined(USE_BLE_GATT_CLIENT)
|
||||
conn_err_t unpair_device(uint64_t address);
|
||||
#else
|
||||
inline conn_err_t unpair_device(uint64_t) { return GATT_NOT_CONNECTED; }
|
||||
#endif
|
||||
#if defined(USE_ESP32) && defined(USE_BLE_GATT_CLIENT)
|
||||
#if defined(USE_ESP32_BLE) && defined(USE_BLE_GATT_CLIENT)
|
||||
conn_err_t clear_gatt_cache(uint64_t address);
|
||||
#else
|
||||
inline conn_err_t clear_gatt_cache(uint64_t) { return GATT_NOT_CONNECTED; }
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
|
||||
#if defined(USE_ESP32_BLE) && defined(USE_BLE_GATT_CLIENT)
|
||||
|
||||
#include "bluetooth_connection.h"
|
||||
|
||||
// The in-place streamer serves the proxy's service-discovery API; backend-only
|
||||
// builds compile without the proxy headers or the streamer.
|
||||
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
|
||||
#include "bluetooth_connection.h"
|
||||
#include "bluetooth_connection_hub.h"
|
||||
|
||||
#include "esphome/components/bluetooth_proxy/bluetooth_proxy.h"
|
||||
@@ -345,6 +346,9 @@ int BluedroidGattClient::update_connection_params(uint16_t min_interval, uint16_
|
||||
|
||||
void BluedroidGattClient::release_services() {
|
||||
this->service_total_ = 0;
|
||||
#ifdef USE_BLUEDROID_GATT_SERVICE_TABLE
|
||||
this->table_.free();
|
||||
#endif
|
||||
// Always set: terminates any in-flight stream on every cache config.
|
||||
this->services_released_ = true;
|
||||
#ifndef CONFIG_BT_GATTC_CACHE_NVS_FLASH
|
||||
@@ -357,6 +361,24 @@ void BluedroidGattClient::release_services() {
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef USE_BLUEDROID_GATT_SERVICE_TABLE
|
||||
ble_device_base::GattServiceTable BluedroidGattClient::get_service_table() {
|
||||
// Lifetime: every teardown path (CLOSE_EVT, the safety timeout, stack-down,
|
||||
// passive DISCONNECT) routes through release_services(), so a materialized
|
||||
// table cannot outlive its link.
|
||||
if (this->table_.empty() &&
|
||||
(this->services_released_ || this->service_total_ == 0 ||
|
||||
!this->table_.build(this->gattc_if_, this->conn_id_, this->service_total_, this->connection_index_))) {
|
||||
// Released / no services / failed build all collapse to empty; the
|
||||
// build failures warned above, log the quiet two.
|
||||
ESP_LOGD(TAG, "[%d] No service table (released=%d, services=%u)", this->connection_index_, this->services_released_,
|
||||
this->service_total_);
|
||||
return {};
|
||||
}
|
||||
return this->table_.view();
|
||||
}
|
||||
#endif // USE_BLUEDROID_GATT_SERVICE_TABLE
|
||||
|
||||
// ---- internals ----
|
||||
|
||||
bool BluedroidGattClient::check_addr_(const esp_bd_addr_t &addr) const {
|
||||
@@ -403,6 +425,11 @@ void BluedroidGattClient::log_gattc_warning_(const char *operation, int code) {
|
||||
// ---- service streaming ----
|
||||
|
||||
int BluedroidGattClient::handle_search_cmpl_(esp_gatt_status_t status) {
|
||||
#ifdef USE_BLUEDROID_GATT_SERVICE_TABLE
|
||||
// Re-discovery moves the counts the table view derives offsets from; free
|
||||
// the stale table.
|
||||
this->table_.free();
|
||||
#endif
|
||||
// Step down from the fast discovery params.
|
||||
this->update_conn_params_(MEDIUM_MIN_CONN_INTERVAL, MEDIUM_MAX_CONN_INTERVAL, 0, MEDIUM_CONN_TIMEOUT, "medium");
|
||||
if (status != ESP_GATT_OK) {
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
|
||||
#if defined(USE_ESP32_BLE) && defined(USE_BLE_GATT_CLIENT)
|
||||
|
||||
#include "bluetooth_connection.h"
|
||||
#include "gatt_service_table_bluedroid.h"
|
||||
|
||||
#include "esphome/components/ble_device_base/ble_gatt_client.h"
|
||||
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
|
||||
#include "esphome/core/component.h"
|
||||
@@ -76,11 +79,16 @@ class BluedroidGattClient final : public esp32_ble_tracker::ESPBTClient, public
|
||||
int notify_characteristic(uint16_t handle, bool enable);
|
||||
int pair();
|
||||
int update_connection_params(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout);
|
||||
// Contract stub: the proxy streams in place; the on-demand materializer
|
||||
// for direct consumers lands with #18205. NOTE: a direct consumer reaching
|
||||
// this stub gets an empty table indistinguishable from a service-less
|
||||
// peer - do not ship one against this backend before the materializer.
|
||||
// On-demand table for direct consumers; the proxy streams instead, so the
|
||||
// materializer compiles only under USE_BLUEDROID_GATT_SERVICE_TABLE (emitted by
|
||||
// direct-consumer codegen, never by the proxy).
|
||||
#ifdef USE_BLUEDROID_GATT_SERVICE_TABLE
|
||||
ble_device_base::GattServiceTable get_service_table();
|
||||
#else
|
||||
// A direct consumer reaching this stub misconfigured its codegen
|
||||
// (service_table=False): the empty table reads as a service-less peer.
|
||||
ble_device_base::GattServiceTable get_service_table() { return {}; }
|
||||
#endif
|
||||
void release_services();
|
||||
|
||||
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
|
||||
@@ -110,6 +118,9 @@ class BluedroidGattClient final : public esp32_ble_tracker::ESPBTClient, public
|
||||
|
||||
// Group 1: pointers / composed objects
|
||||
ble_device_base::GattClientListener *listener_{nullptr};
|
||||
#ifdef USE_BLUEDROID_GATT_SERVICE_TABLE
|
||||
BluedroidServiceTable table_;
|
||||
#endif
|
||||
// Group 2: 4-byte types
|
||||
uint32_t disconnecting_started_{0};
|
||||
|
||||
|
||||
@@ -12,10 +12,12 @@
|
||||
|
||||
#include "esphome/components/ble_device_base/ble_gatt_client.h"
|
||||
|
||||
#if defined(USE_RP2040_BLE)
|
||||
// Arms are keyed on codegen-emitted per-backend defines (_PLATFORM_BACKENDS
|
||||
// in __init__.py), so they are order-independent.
|
||||
#if defined(USE_BLE_GATT_BACKEND_RP2)
|
||||
#include "bluetooth_connection_rp2.h"
|
||||
#define ESPHOME_BLE_GATT_CONNECTION_TYPE bluetooth_connection::RP2GattClient
|
||||
#elif defined(USE_ESP32_BLE)
|
||||
#elif defined(USE_BLE_GATT_BACKEND_BLUEDROID)
|
||||
#include "bluetooth_connection_bluedroid.h"
|
||||
#define ESPHOME_BLE_GATT_CONNECTION_TYPE bluetooth_connection::BluedroidGattClient
|
||||
#elif defined(USE_BLE_GATT_CLIENT_STUB_BACKEND)
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
#include "gatt_service_table_bluedroid.h"
|
||||
|
||||
#if defined(USE_ESP32_BLE) && defined(USE_BLE_GATT_CLIENT) && defined(USE_BLUEDROID_GATT_SERVICE_TABLE)
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::bluetooth_connection {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "bluetooth_connection");
|
||||
|
||||
// A stack that never reports end-of-range would otherwise walk forever.
|
||||
static constexpr uint16_t MAX_DESCRIPTORS_PER_CHARACTERISTIC = 64;
|
||||
|
||||
// Shared enumeration for both build passes: an identical walk order is what
|
||||
// lets the counting pass size the block the filling pass fills.
|
||||
// INVALID_OFFSET/NOT_FOUND mean end-of-range; anything else is a failure.
|
||||
template<typename ServiceFn, typename CharFn, typename DescFn>
|
||||
bool BluedroidServiceTable::walk_(ServiceFn &&on_service, CharFn &&on_char, DescFn &&on_desc) {
|
||||
for (uint16_t s = 0; s < this->service_total_; s++) {
|
||||
esp_gattc_service_elem_t svc;
|
||||
uint16_t svc_count = 1;
|
||||
auto svc_status = esp_ble_gattc_get_service(this->gattc_if_, this->conn_id_, nullptr, &svc, &svc_count, s);
|
||||
if (svc_status != ESP_GATT_OK || svc_count == 0) {
|
||||
this->log_walk_warning_("esp_ble_gattc_get_service", svc_status);
|
||||
return false;
|
||||
}
|
||||
if (!on_service(s, svc)) {
|
||||
return false;
|
||||
}
|
||||
uint16_t svc_chars = 0;
|
||||
auto count_status = esp_ble_gattc_get_attr_count(this->gattc_if_, this->conn_id_, ESP_GATT_DB_CHARACTERISTIC,
|
||||
svc.start_handle, svc.end_handle, 0, &svc_chars);
|
||||
if (count_status != ESP_GATT_OK) {
|
||||
this->log_walk_warning_("esp_ble_gattc_get_attr_count", count_status);
|
||||
return false;
|
||||
}
|
||||
for (uint16_t c = 0; c < svc_chars; c++) {
|
||||
esp_gattc_char_elem_t chr;
|
||||
uint16_t char_count = 1;
|
||||
auto status = esp_ble_gattc_get_all_char(this->gattc_if_, this->conn_id_, svc.start_handle, svc.end_handle, &chr,
|
||||
&char_count, c);
|
||||
if (status != ESP_GATT_OK || char_count == 0) {
|
||||
// An early terminator contradicts svc_chars from the same cache;
|
||||
// never build a silently truncated table.
|
||||
this->log_walk_warning_("esp_ble_gattc_get_all_char", status);
|
||||
return false;
|
||||
}
|
||||
if (!on_char(svc, chr)) {
|
||||
return false;
|
||||
}
|
||||
for (uint16_t d = 0;; d++) {
|
||||
if (d == MAX_DESCRIPTORS_PER_CHARACTERISTIC) {
|
||||
// A stack that never reports end-of-range; fail like every other
|
||||
// inconsistency instead of truncating the table silently.
|
||||
ESP_LOGW(TAG, "[%d] Descriptor walk exceeded %u entries", this->log_index_,
|
||||
MAX_DESCRIPTORS_PER_CHARACTERISTIC);
|
||||
return false;
|
||||
}
|
||||
esp_gattc_descr_elem_t desc;
|
||||
uint16_t desc_count = 1;
|
||||
auto desc_status =
|
||||
esp_ble_gattc_get_all_descr(this->gattc_if_, this->conn_id_, chr.char_handle, &desc, &desc_count, d);
|
||||
if (desc_status == ESP_GATT_INVALID_OFFSET || desc_status == ESP_GATT_NOT_FOUND) {
|
||||
break;
|
||||
}
|
||||
if (desc_status != ESP_GATT_OK || desc_count == 0) {
|
||||
this->log_walk_warning_("esp_ble_gattc_get_all_descr", desc_status);
|
||||
return false;
|
||||
}
|
||||
if (!on_desc(chr, desc)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BluedroidServiceTable::count_services(esp_gatt_if_t gattc_if, uint16_t conn_id, uint16_t *total) {
|
||||
uint16_t primary = 0;
|
||||
uint16_t secondary = 0;
|
||||
if (esp_ble_gattc_get_attr_count(gattc_if, conn_id, ESP_GATT_DB_PRIMARY_SERVICE, 0x0001, 0xFFFF, 0, &primary) !=
|
||||
ESP_GATT_OK ||
|
||||
esp_ble_gattc_get_attr_count(gattc_if, conn_id, ESP_GATT_DB_SECONDARY_SERVICE, 0x0001, 0xFFFF, 0, &secondary) !=
|
||||
ESP_GATT_OK) {
|
||||
// A failed count must not read as an authoritative empty database.
|
||||
return false;
|
||||
}
|
||||
*total = primary + secondary;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BluedroidServiceTable::build(esp_gatt_if_t gattc_if, uint16_t conn_id, uint16_t service_total, uint8_t log_index) {
|
||||
this->free();
|
||||
this->gattc_if_ = gattc_if;
|
||||
this->conn_id_ = conn_id;
|
||||
this->service_total_ = service_total;
|
||||
this->log_index_ = log_index;
|
||||
|
||||
// Pass 1: count, so one exact-size block holds the whole table.
|
||||
uint16_t char_total = 0;
|
||||
uint16_t desc_total = 0;
|
||||
bool counted = this->walk_([](uint16_t, const esp_gattc_service_elem_t &) { return true; },
|
||||
[&](const esp_gattc_service_elem_t &, const esp_gattc_char_elem_t &) {
|
||||
char_total++;
|
||||
return true;
|
||||
},
|
||||
[&](const esp_gattc_char_elem_t &, const esp_gattc_descr_elem_t &) {
|
||||
desc_total++;
|
||||
return true;
|
||||
});
|
||||
if (!counted) {
|
||||
ESP_LOGW(TAG, "[%d] Service table walk failed during count", this->log_index_);
|
||||
this->free();
|
||||
return false;
|
||||
}
|
||||
|
||||
// The arrays share one block; carving stays aligned because each struct's
|
||||
// strictest member is the UUID and array sizes are multiples of it.
|
||||
static_assert(alignof(ble_device_base::GattService) >= alignof(ble_device_base::GattCharacteristic) &&
|
||||
alignof(ble_device_base::GattCharacteristic) >= alignof(ble_device_base::GattDescriptor));
|
||||
size_t svc_bytes = this->service_total_ * sizeof(ble_device_base::GattService);
|
||||
size_t char_bytes = char_total * sizeof(ble_device_base::GattCharacteristic);
|
||||
size_t total_bytes = svc_bytes + char_bytes + desc_total * sizeof(ble_device_base::GattDescriptor);
|
||||
RAMAllocator<uint8_t> allocator(RAMAllocator<uint8_t>::ALLOC_INTERNAL);
|
||||
this->storage_ = allocator.allocate(total_bytes);
|
||||
if (this->storage_ == nullptr) {
|
||||
ESP_LOGW(TAG, "[%d] Service table allocation failed (%u bytes)", this->log_index_,
|
||||
static_cast<unsigned>(total_bytes));
|
||||
this->free();
|
||||
return false;
|
||||
}
|
||||
auto *services = reinterpret_cast<ble_device_base::GattService *>(this->storage_);
|
||||
auto *characteristics = reinterpret_cast<ble_device_base::GattCharacteristic *>(this->storage_ + svc_bytes);
|
||||
auto *descriptors = reinterpret_cast<ble_device_base::GattDescriptor *>(this->storage_ + svc_bytes + char_bytes);
|
||||
|
||||
// Pass 2: fill, bounded by the pass-1 totals. A bound trip or a shortfall
|
||||
// means the cached database changed between the passes; fail the build
|
||||
// rather than serve an inconsistent table (the consumer retries).
|
||||
uint16_t char_index = 0;
|
||||
uint16_t desc_index = 0;
|
||||
ble_device_base::GattService *cur_service = nullptr;
|
||||
ble_device_base::GattCharacteristic *cur_char = nullptr;
|
||||
bool filled = this->walk_(
|
||||
[&](uint16_t s, const esp_gattc_service_elem_t &svc) {
|
||||
cur_service = &services[s];
|
||||
cur_service->uuid = ble_device_base::ESPBTUUID::from_uuid(svc.uuid);
|
||||
cur_service->start_handle = svc.start_handle;
|
||||
cur_service->end_handle = svc.end_handle;
|
||||
cur_service->first_characteristic = char_index;
|
||||
cur_service->characteristic_count = 0;
|
||||
return true;
|
||||
},
|
||||
[&](const esp_gattc_service_elem_t &svc, const esp_gattc_char_elem_t &chr) {
|
||||
if (char_index >= char_total) {
|
||||
return false;
|
||||
}
|
||||
cur_char = &characteristics[char_index++];
|
||||
cur_char->uuid = ble_device_base::ESPBTUUID::from_uuid(chr.uuid);
|
||||
cur_char->value_handle = chr.char_handle;
|
||||
// Bluedroid addresses descriptors by characteristic handle, so the
|
||||
// table's end_handle only needs the service-bounded upper bound.
|
||||
cur_char->end_handle = svc.end_handle;
|
||||
cur_char->properties = chr.properties;
|
||||
cur_char->first_descriptor = desc_index;
|
||||
cur_char->descriptor_count = 0;
|
||||
cur_service->characteristic_count++;
|
||||
return true;
|
||||
},
|
||||
[&](const esp_gattc_char_elem_t &, const esp_gattc_descr_elem_t &desc) {
|
||||
if (desc_index >= desc_total) {
|
||||
return false;
|
||||
}
|
||||
descriptors[desc_index].uuid = ble_device_base::ESPBTUUID::from_uuid(desc.uuid);
|
||||
descriptors[desc_index].handle = desc.handle;
|
||||
desc_index++;
|
||||
cur_char->descriptor_count++;
|
||||
return true;
|
||||
});
|
||||
if (!filled || char_index != char_total || desc_index != desc_total) {
|
||||
// Walk error or the database changed between passes; better an empty
|
||||
// table than a corrupt one.
|
||||
ESP_LOGW(TAG, "[%d] Service table walk mismatch, discarding", this->log_index_);
|
||||
this->free();
|
||||
return false;
|
||||
}
|
||||
this->char_total_ = char_total;
|
||||
this->desc_total_ = desc_total;
|
||||
return true;
|
||||
}
|
||||
|
||||
void BluedroidServiceTable::log_walk_warning_(const char *operation, int code) {
|
||||
ESP_LOGW(TAG, "[%d] %s failed, status=%d", this->log_index_, operation, code);
|
||||
}
|
||||
|
||||
} // namespace esphome::bluetooth_connection
|
||||
|
||||
#endif // USE_ESP32_BLE && USE_BLE_GATT_CLIENT && USE_BLUEDROID_GATT_SERVICE_TABLE
|
||||
@@ -0,0 +1,82 @@
|
||||
// Owning two-pass materializer of one Bluedroid GATT database snapshot into
|
||||
// the neutral GattServiceTable layout, shared by the BluedroidGattClient
|
||||
// backend and ble_client's esp32 engine.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
#if defined(USE_ESP32_BLE) && defined(USE_BLE_GATT_CLIENT) && defined(USE_BLUEDROID_GATT_SERVICE_TABLE)
|
||||
|
||||
#include "esphome/components/ble_device_base/ble_gatt_client.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include <esp_gattc_api.h>
|
||||
|
||||
namespace esphome::bluetooth_connection {
|
||||
|
||||
class BluedroidServiceTable {
|
||||
public:
|
||||
~BluedroidServiceTable() { this->free(); }
|
||||
// Owns storage_; a copy would double-free. The default constructor is user
|
||||
// provided, not "= default", so value-initializing the enclosing backend
|
||||
// cannot zero-fill .bss that is already zero.
|
||||
BluedroidServiceTable() {}
|
||||
BluedroidServiceTable(const BluedroidServiceTable &) = delete;
|
||||
BluedroidServiceTable &operator=(const BluedroidServiceTable &) = delete;
|
||||
|
||||
/// The service count build() requires: the stack's PRIMARY+SECONDARY
|
||||
/// attribute totals, never the SEARCH_RES event count.
|
||||
static bool count_services(esp_gatt_if_t gattc_if, uint16_t conn_id, uint16_t *total);
|
||||
|
||||
/// Two-pass build from the stack's cached database (service_total from
|
||||
/// count_services()). log_index labels warnings. Frees any previous table
|
||||
/// first; on failure the table is left empty.
|
||||
bool build(esp_gatt_if_t gattc_if, uint16_t conn_id, uint16_t service_total, uint8_t log_index);
|
||||
|
||||
// The view is carved from the storage block and the counts on each call
|
||||
// (a cold path) rather than cached, saving a per-instance table member.
|
||||
ble_device_base::GattServiceTable view() const {
|
||||
size_t svc_bytes = this->service_total_ * sizeof(ble_device_base::GattService);
|
||||
size_t char_bytes = this->char_total_ * sizeof(ble_device_base::GattCharacteristic);
|
||||
return {reinterpret_cast<const ble_device_base::GattService *>(this->storage_),
|
||||
reinterpret_cast<const ble_device_base::GattCharacteristic *>(this->storage_ + svc_bytes),
|
||||
reinterpret_cast<const ble_device_base::GattDescriptor *>(this->storage_ + svc_bytes + char_bytes),
|
||||
this->service_total_,
|
||||
this->char_total_,
|
||||
this->desc_total_};
|
||||
}
|
||||
|
||||
// Always resets the counts: a failed build must never leave a non-zero
|
||||
// service_total_ behind a null table.
|
||||
void free() {
|
||||
if (this->storage_ != nullptr) {
|
||||
RAMAllocator<uint8_t> allocator(RAMAllocator<uint8_t>::ALLOC_INTERNAL);
|
||||
allocator.deallocate(this->storage_, 0);
|
||||
this->storage_ = nullptr;
|
||||
}
|
||||
this->service_total_ = 0;
|
||||
this->char_total_ = 0;
|
||||
this->desc_total_ = 0;
|
||||
}
|
||||
|
||||
bool empty() const { return this->storage_ == nullptr; }
|
||||
|
||||
private:
|
||||
template<typename ServiceFn, typename CharFn, typename DescFn>
|
||||
bool walk_(ServiceFn &&on_service, CharFn &&on_char, DescFn &&on_desc);
|
||||
void log_walk_warning_(const char *operation, int code);
|
||||
|
||||
uint8_t *storage_{nullptr};
|
||||
uint16_t service_total_{0};
|
||||
uint16_t char_total_{0};
|
||||
uint16_t desc_total_{0};
|
||||
// Walk context, set by build().
|
||||
uint16_t conn_id_{0};
|
||||
esp_gatt_if_t gattc_if_{}; // uint8_t width
|
||||
uint8_t log_index_{0};
|
||||
};
|
||||
|
||||
} // namespace esphome::bluetooth_connection
|
||||
|
||||
#endif // USE_ESP32_BLE && USE_BLE_GATT_CLIENT && USE_BLUEDROID_GATT_SERVICE_TABLE
|
||||
@@ -94,10 +94,20 @@ def _esp32_config_schema() -> cv.All:
|
||||
CONNECTION_SCHEMA = bluetooth_connection.hub_connection_schema(PLATFORM_ESP32)
|
||||
|
||||
def validate_connections(config: ConfigType) -> ConfigType:
|
||||
if CONF_CONNECTIONS in config or not config[CONF_ACTIVE]:
|
||||
if CONF_CONNECTIONS in config:
|
||||
# Explicit entries claim slots like the generated ones; dev
|
||||
# historically skipped this, letting an explicit-connections
|
||||
# config evade the controller budget.
|
||||
bluetooth_connection.consume_gatt_slot(
|
||||
"bluetooth_proxy", len(config[CONF_CONNECTIONS])
|
||||
)(config)
|
||||
return config
|
||||
if not config[CONF_ACTIVE]:
|
||||
return config
|
||||
connection_slots: int = config[CONF_CONNECTION_SLOTS]
|
||||
esp32_ble.consume_connection_slots(connection_slots, "bluetooth_proxy")(config)
|
||||
bluetooth_connection.consume_gatt_slot("bluetooth_proxy", connection_slots)(
|
||||
config
|
||||
)
|
||||
return {
|
||||
**config,
|
||||
CONF_CONNECTIONS: [CONNECTION_SCHEMA({}) for _ in range(connection_slots)],
|
||||
@@ -149,14 +159,14 @@ def _rp2_config_schema() -> cv.All:
|
||||
connection_schema = bluetooth_connection.hub_connection_schema(PLATFORM_RP2)
|
||||
|
||||
def populate_connections(config: ConfigType) -> ConfigType:
|
||||
from esphome.components import rp2040_ble
|
||||
|
||||
# One wrapper + backend pair per slot, declared during validation so
|
||||
# their ids exist for codegen (the esp32 arm's `connections` pattern).
|
||||
if not config[CONF_ACTIVE]:
|
||||
return config
|
||||
connection_slots: int = config[CONF_CONNECTION_SLOTS]
|
||||
rp2040_ble.consume_connection_slots(connection_slots, "bluetooth_proxy")(config)
|
||||
bluetooth_connection.consume_gatt_slot("bluetooth_proxy", connection_slots)(
|
||||
config
|
||||
)
|
||||
return {
|
||||
**config,
|
||||
CONF_CONNECTIONS: [connection_schema({}) for _ in range(connection_slots)],
|
||||
@@ -206,7 +216,9 @@ async def _connections_to_code(var: cg.MockObj, config: ConfigType) -> None:
|
||||
# sends those requests and their handlers and encoders are dead.
|
||||
cg.add_define("USE_BLUETOOTH_PROXY_CONNECTIONS")
|
||||
for connection_conf in connections:
|
||||
backend = await bluetooth_connection.new_gatt_backend(connection_conf)
|
||||
backend = await bluetooth_connection.new_gatt_backend(
|
||||
connection_conf, service_table=False
|
||||
)
|
||||
connection = cg.new_Pvariable(connection_conf[CONF_ID])
|
||||
cg.add(connection.set_backend(backend))
|
||||
cg.add(var.register_connection(connection))
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_BUTTON
|
||||
|
||||
#include "bq25186.h"
|
||||
#include "esphome/components/button/button.h"
|
||||
|
||||
namespace esphome::bq25186 {
|
||||
|
||||
/// Writes one SHIP_RST field when pressed
|
||||
class BQ25186Button final : public button::Button, public Parented<BQ25186Component> {
|
||||
public:
|
||||
BQ25186Button(uint8_t mask, uint8_t value) : mask_(mask), value_(value) {}
|
||||
|
||||
protected:
|
||||
void press_action() override { this->parent_->write_ship_rst(this->mask_, this->value_); }
|
||||
|
||||
const uint8_t mask_;
|
||||
const uint8_t value_;
|
||||
};
|
||||
|
||||
} // namespace esphome::bq25186
|
||||
|
||||
#endif // USE_BUTTON
|
||||
@@ -1,4 +1,3 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import button
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
@@ -10,11 +9,10 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import BQ25186_CHILD_SCHEMA, CONF_BQ25186_ID, bq25186_ns
|
||||
from . import BQ25186_CHILD_SCHEMA, CONF_BQ25186_ID
|
||||
|
||||
DEPENDENCIES = ["bq25186"]
|
||||
|
||||
BQ25186Button = bq25186_ns.class_("BQ25186Button", button.Button)
|
||||
|
||||
# SHIP_RST: bit 7 is the software reset, bits 6:5 the shutdown, ship and hardware reset codes
|
||||
SOFTWARE_RESET_MASK = 0x80
|
||||
@@ -33,7 +31,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
BQ25186_CHILD_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(key): button.button_schema(
|
||||
BQ25186Button,
|
||||
button.ApplyButton,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
icon=icon,
|
||||
device_class=device_class,
|
||||
@@ -46,7 +44,6 @@ CONFIG_SCHEMA = cv.All(
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
buttons = button.sub_buttons(config, parent=config[CONF_BQ25186_ID])
|
||||
for key, mask, value, _, _ in _BUTTONS:
|
||||
if (conf := config.get(key)) is not None:
|
||||
var = await button.new_button(conf, mask, value)
|
||||
await cg.register_parented(var, config[CONF_BQ25186_ID])
|
||||
await buttons(key, None, apply=f"write_ship_rst({mask:#04x}, {value:#04x})")
|
||||
@@ -1,7 +1,6 @@
|
||||
from esphome import automation
|
||||
from esphome.automation import maybe_simple_id
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import mqtt, web_server
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_DEVICE_CLASS,
|
||||
@@ -10,7 +9,6 @@ from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_MQTT_ID,
|
||||
CONF_ON_PRESS,
|
||||
CONF_WEB_SERVER,
|
||||
DEVICE_CLASS_EMPTY,
|
||||
DEVICE_CLASS_IDENTIFY,
|
||||
DEVICE_CLASS_RESTART,
|
||||
@@ -21,11 +19,12 @@ from esphome.const import (
|
||||
ICON_RESTART,
|
||||
ICON_RESTART_ALERT,
|
||||
)
|
||||
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
|
||||
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority, entity_helpers
|
||||
from esphome.core.entity_helpers import (
|
||||
ApplyEntity,
|
||||
SubEntities,
|
||||
entity_duplicate_validator,
|
||||
mqtt_component_class,
|
||||
queue_entity_register,
|
||||
setup_device_class,
|
||||
setup_entity,
|
||||
@@ -53,11 +52,13 @@ validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_")
|
||||
|
||||
|
||||
_BUTTON_SCHEMA = (
|
||||
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
|
||||
cv.ENTITY_BASE_SCHEMA.extend(entity_helpers.WEBSERVER_SORTING_SCHEMA)
|
||||
.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
|
||||
.extend(
|
||||
{
|
||||
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTButtonComponent),
|
||||
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(
|
||||
mqtt_component_class("MQTTButtonComponent")
|
||||
),
|
||||
cv.Optional(
|
||||
CONF_DEVICE_CLASS, visibility=cv.Visibility.ADVANCED
|
||||
): validate_device_class,
|
||||
@@ -135,12 +136,7 @@ async def setup_button_core_(var: MockObj, config: ConfigType) -> None:
|
||||
|
||||
setup_device_class(config)
|
||||
|
||||
if mqtt_id := config.get(CONF_MQTT_ID):
|
||||
mqtt_ = cg.new_Pvariable(mqtt_id, var)
|
||||
await mqtt.register_mqtt_component(mqtt_, config)
|
||||
|
||||
if web_server_config := config.get(CONF_WEB_SERVER):
|
||||
await web_server.add_entity_config(var, web_server_config)
|
||||
await entity_helpers.setup_entity_integrations(var, config)
|
||||
|
||||
|
||||
async def register_button(var: MockObj, config: ConfigType) -> None:
|
||||
|
||||
@@ -3,7 +3,6 @@ from typing import Any
|
||||
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import mqtt, web_server
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ACTION_STATE_TOPIC,
|
||||
@@ -49,15 +48,15 @@ from esphome.const import (
|
||||
CONF_TEMPERATURE_STEP,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
CONF_VISUAL,
|
||||
CONF_WEB_SERVER,
|
||||
UNIT_KELVIN,
|
||||
)
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority, entity_helpers
|
||||
from esphome.core.entity_helpers import (
|
||||
TemperatureUnitResolver,
|
||||
convert_visual_temperatures,
|
||||
declare_fixed_temperature_unit,
|
||||
entity_duplicate_validator,
|
||||
mqtt_component_class,
|
||||
queue_entity_register,
|
||||
setup_entity,
|
||||
setup_entity_temperature_unit,
|
||||
@@ -177,11 +176,13 @@ _visual_temperatures_in_entity_unit = partial(
|
||||
|
||||
|
||||
_CLIMATE_SCHEMA = (
|
||||
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
|
||||
cv.ENTITY_BASE_SCHEMA.extend(entity_helpers.WEBSERVER_SORTING_SCHEMA)
|
||||
.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
|
||||
.extend(
|
||||
{
|
||||
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTClimateComponent),
|
||||
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(
|
||||
mqtt_component_class("MQTTClimateComponent")
|
||||
),
|
||||
cv.Optional(CONF_VISUAL, default={}): cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_MIN_TEMPERATURE): cv.temperature_with_unit,
|
||||
@@ -353,10 +354,8 @@ async def setup_climate_core_(var: MockObj, config: ConfigType) -> None:
|
||||
cg.add(var.set_visual_max_humidity_override(max_humidity))
|
||||
setup_entity_temperature_unit(var, config)
|
||||
|
||||
if (mqtt_id := config.get(CONF_MQTT_ID)) is not None:
|
||||
mqtt_ = cg.new_Pvariable(mqtt_id, var)
|
||||
await mqtt.register_mqtt_component(mqtt_, config)
|
||||
|
||||
mqtt_ = await entity_helpers.setup_entity_integrations(var, config)
|
||||
if mqtt_ is not None:
|
||||
if (action_state_topic := config.get(CONF_ACTION_STATE_TOPIC)) is not None:
|
||||
cg.add(mqtt_.set_custom_action_state_topic(action_state_topic))
|
||||
if (away_command_topic := config.get(CONF_AWAY_COMMAND_TOPIC)) is not None:
|
||||
@@ -486,9 +485,6 @@ async def setup_climate_core_(var: MockObj, config: ConfigType) -> None:
|
||||
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
if web_server_config := config.get(CONF_WEB_SERVER):
|
||||
await web_server.add_entity_config(var, web_server_config)
|
||||
|
||||
|
||||
def set_custom_fan_modes(var: MockObj, modes: list[str]) -> None:
|
||||
"""Point the climate at a shared flash table of custom fan modes."""
|
||||
|
||||
@@ -3,7 +3,6 @@ import logging
|
||||
from esphome import automation
|
||||
from esphome.automation import maybe_simple_id
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import mqtt, web_server
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_DEVICE_CLASS,
|
||||
@@ -23,7 +22,6 @@ from esphome.const import (
|
||||
CONF_TILT_COMMAND_TOPIC,
|
||||
CONF_TILT_STATE_TOPIC,
|
||||
CONF_TRIGGER_ID,
|
||||
CONF_WEB_SERVER,
|
||||
DEVICE_CLASS_AWNING,
|
||||
DEVICE_CLASS_BLIND,
|
||||
DEVICE_CLASS_CURTAIN,
|
||||
@@ -36,9 +34,10 @@ from esphome.const import (
|
||||
DEVICE_CLASS_SHUTTER,
|
||||
DEVICE_CLASS_WINDOW,
|
||||
)
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority, entity_helpers
|
||||
from esphome.core.entity_helpers import (
|
||||
entity_duplicate_validator,
|
||||
mqtt_component_class,
|
||||
queue_entity_register,
|
||||
setup_device_class,
|
||||
setup_entity,
|
||||
@@ -112,11 +111,13 @@ TRIGGERS = {
|
||||
|
||||
|
||||
_COVER_SCHEMA = (
|
||||
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
|
||||
cv.ENTITY_BASE_SCHEMA.extend(entity_helpers.WEBSERVER_SORTING_SCHEMA)
|
||||
.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
|
||||
.extend(
|
||||
{
|
||||
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTCoverComponent),
|
||||
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(
|
||||
mqtt_component_class("MQTTCoverComponent")
|
||||
),
|
||||
cv.Optional(CONF_MQTT_JSON_STATE_PAYLOAD): cv.All(
|
||||
cv.requires_component("mqtt"), cv.boolean
|
||||
),
|
||||
@@ -202,10 +203,8 @@ async def setup_cover_core_(var: MockObj, config: ConfigType) -> None:
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
|
||||
if (mqtt_id := config.get(CONF_MQTT_ID)) is not None:
|
||||
mqtt_ = cg.new_Pvariable(mqtt_id, var)
|
||||
await mqtt.register_mqtt_component(mqtt_, config)
|
||||
|
||||
mqtt_ = await entity_helpers.setup_entity_integrations(var, config)
|
||||
if mqtt_ is not None:
|
||||
if (position_state_topic := config.get(CONF_POSITION_STATE_TOPIC)) is not None:
|
||||
cg.add(mqtt_.set_custom_position_state_topic(position_state_topic))
|
||||
if (
|
||||
@@ -220,9 +219,6 @@ async def setup_cover_core_(var: MockObj, config: ConfigType) -> None:
|
||||
if (tilt_command_topic := config.get(CONF_TILT_COMMAND_TOPIC)) is not None:
|
||||
cg.add(mqtt_.set_custom_tilt_command_topic(tilt_command_topic))
|
||||
|
||||
if web_server_config := config.get(CONF_WEB_SERVER):
|
||||
await web_server.add_entity_config(var, web_server_config)
|
||||
|
||||
|
||||
async def register_cover(var: MockObj, config: ConfigType) -> None:
|
||||
if not CORE.has_id(config[CONF_ID]):
|
||||
|
||||
@@ -28,30 +28,23 @@ void CSE7766Component::loop() {
|
||||
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
// At 4800 baud (~480 bytes/sec) with ~122 Hz loop rate, typically ~4 bytes per call.
|
||||
uint8_t buf[CSE7766_RAW_DATA_SIZE];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(avail, sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
avail -= to_read;
|
||||
this->read_available<CSE7766_RAW_DATA_SIZE>(
|
||||
[this](uint8_t byte) {
|
||||
this->raw_data_[this->raw_data_index_] = byte;
|
||||
if (!this->check_byte_()) {
|
||||
this->raw_data_index_ = 0;
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < to_read; i++) {
|
||||
this->raw_data_[this->raw_data_index_] = buf[i];
|
||||
if (!this->check_byte_()) {
|
||||
this->raw_data_index_ = 0;
|
||||
this->status_set_warning();
|
||||
continue;
|
||||
}
|
||||
if (this->raw_data_index_ == CSE7766_RAW_DATA_SIZE - 1) {
|
||||
this->parse_data_();
|
||||
this->status_clear_warning();
|
||||
}
|
||||
|
||||
if (this->raw_data_index_ == CSE7766_RAW_DATA_SIZE - 1) {
|
||||
this->parse_data_();
|
||||
this->status_clear_warning();
|
||||
}
|
||||
|
||||
this->raw_data_index_ = (this->raw_data_index_ + 1) % CSE7766_RAW_DATA_SIZE;
|
||||
}
|
||||
}
|
||||
this->raw_data_index_ = (this->raw_data_index_ + 1) % CSE7766_RAW_DATA_SIZE;
|
||||
},
|
||||
avail);
|
||||
}
|
||||
|
||||
bool CSE7766Component::check_byte_() {
|
||||
|
||||
@@ -2,7 +2,6 @@ from collections.abc import Callable
|
||||
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import mqtt, time, web_server
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_DATE,
|
||||
@@ -20,12 +19,12 @@ from esphome.const import (
|
||||
CONF_TIME_ID,
|
||||
CONF_TRIGGER_ID,
|
||||
CONF_TYPE,
|
||||
CONF_WEB_SERVER,
|
||||
CONF_YEAR,
|
||||
)
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority, entity_helpers
|
||||
from esphome.core.entity_helpers import (
|
||||
entity_duplicate_validator,
|
||||
mqtt_component_class,
|
||||
queue_entity_register,
|
||||
setup_entity,
|
||||
)
|
||||
@@ -38,6 +37,10 @@ DOMAIN = "datetime"
|
||||
IS_PLATFORM_COMPONENT = True
|
||||
|
||||
datetime_ns = cg.esphome_ns.namespace("datetime")
|
||||
# Matches time.RealTimeClock by name without importing the time component
|
||||
_RealTimeClock = cg.esphome_ns.namespace("time").class_(
|
||||
"RealTimeClock", cg.PollingComponent
|
||||
)
|
||||
DateTimeBase = datetime_ns.class_("DateTimeBase", cg.EntityBase)
|
||||
DateEntity = datetime_ns.class_("DateEntity", DateTimeBase)
|
||||
TimeEntity = datetime_ns.class_("TimeEntity", DateTimeBase)
|
||||
@@ -64,7 +67,7 @@ DATETIME_MODES = [
|
||||
def _validate_time_present(config: ConfigType) -> ConfigType:
|
||||
config = config.copy()
|
||||
if CONF_ON_TIME in config and CONF_TIME_ID not in config:
|
||||
time_id = cv.use_id(time.RealTimeClock)(None)
|
||||
time_id = cv.use_id(_RealTimeClock)(None)
|
||||
config[CONF_TIME_ID] = time_id
|
||||
return config
|
||||
|
||||
@@ -73,10 +76,10 @@ _DATETIME_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_ON_VALUE): automation.validate_automation({}),
|
||||
cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock),
|
||||
cv.Optional(CONF_TIME_ID): cv.use_id(_RealTimeClock),
|
||||
}
|
||||
)
|
||||
.extend(web_server.WEBSERVER_SORTING_SCHEMA)
|
||||
.extend(entity_helpers.WEBSERVER_SORTING_SCHEMA)
|
||||
.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
|
||||
).add_extra(_validate_time_present)
|
||||
|
||||
@@ -87,7 +90,9 @@ def date_schema(class_: MockObjClass) -> cv.Schema:
|
||||
schema = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(class_),
|
||||
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTDateComponent),
|
||||
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(
|
||||
mqtt_component_class("MQTTDateComponent")
|
||||
),
|
||||
cv.Optional(CONF_TYPE, default="DATE"): cv.one_of("DATE", upper=True),
|
||||
}
|
||||
)
|
||||
@@ -98,7 +103,9 @@ def time_schema(class_: MockObjClass) -> cv.Schema:
|
||||
schema = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(class_),
|
||||
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTTimeComponent),
|
||||
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(
|
||||
mqtt_component_class("MQTTTimeComponent")
|
||||
),
|
||||
cv.Optional(CONF_TYPE, default="TIME"): cv.one_of("TIME", upper=True),
|
||||
cv.Optional(CONF_ON_TIME): automation.validate_automation(
|
||||
{
|
||||
@@ -115,7 +122,7 @@ def datetime_schema(class_: MockObjClass) -> cv.Schema:
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(class_),
|
||||
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(
|
||||
mqtt.MQTTDateTimeComponent
|
||||
mqtt_component_class("MQTTDateTimeComponent")
|
||||
),
|
||||
cv.Optional(CONF_TYPE, default="DATETIME"): cv.one_of(
|
||||
"DATETIME", upper=True
|
||||
@@ -132,11 +139,7 @@ def datetime_schema(class_: MockObjClass) -> cv.Schema:
|
||||
|
||||
@setup_entity("datetime")
|
||||
async def setup_datetime_core_(var: MockObj, config: ConfigType) -> None:
|
||||
if (mqtt_id := config.get(CONF_MQTT_ID)) is not None:
|
||||
mqtt_ = cg.new_Pvariable(mqtt_id, var)
|
||||
await mqtt.register_mqtt_component(mqtt_, config)
|
||||
if web_server_config := config.get(CONF_WEB_SERVER):
|
||||
await web_server.add_entity_config(var, web_server_config)
|
||||
await entity_helpers.setup_entity_integrations(var, config)
|
||||
# The state callback carries nothing; the automation gets the value as an ESPTime.
|
||||
for conf in config.get(CONF_ON_VALUE, []):
|
||||
await automation.build_callback_automation(
|
||||
|
||||
@@ -135,153 +135,140 @@ void DFPlayer::send_cmd_(uint8_t cmd, uint16_t argument) {
|
||||
}
|
||||
|
||||
void DFPlayer::loop() {
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
size_t avail = this->available();
|
||||
uint8_t buf[64];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(avail, sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
avail -= to_read;
|
||||
for (size_t bi = 0; bi < to_read; bi++) {
|
||||
uint8_t byte = buf[bi];
|
||||
this->read_available([this](uint8_t byte) {
|
||||
if (this->read_pos_ == DFPLAYER_READ_BUFFER_LENGTH)
|
||||
this->read_pos_ = 0;
|
||||
|
||||
if (this->read_pos_ == DFPLAYER_READ_BUFFER_LENGTH)
|
||||
this->read_pos_ = 0;
|
||||
|
||||
switch (this->read_pos_) {
|
||||
case 0: // Start mark
|
||||
if (byte != 0x7E)
|
||||
continue;
|
||||
break;
|
||||
case 1: // Version
|
||||
if (byte != 0xFF) {
|
||||
ESP_LOGW(TAG, "Expected Version 0xFF, got %#02x", byte);
|
||||
this->read_pos_ = 0;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
case 2: // Buffer length
|
||||
if (byte != 0x06) {
|
||||
ESP_LOGW(TAG, "Expected Buffer length 0x06, got %#02x", byte);
|
||||
this->read_pos_ = 0;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
case 9: // End byte
|
||||
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
|
||||
char byte_sequence[100];
|
||||
byte_sequence[0] = '\0';
|
||||
for (size_t i = 0; i < this->read_pos_ + 1; ++i) {
|
||||
snprintf(byte_sequence + strlen(byte_sequence), sizeof(byte_sequence) - strlen(byte_sequence), "%02X ",
|
||||
this->read_buffer_[i]);
|
||||
}
|
||||
ESP_LOGVV(TAG, "Received byte sequence: %s", byte_sequence);
|
||||
#endif
|
||||
if (byte != 0xEF) {
|
||||
ESP_LOGW(TAG, "Expected end byte 0xEF, got %#02x", byte);
|
||||
this->read_pos_ = 0;
|
||||
continue;
|
||||
}
|
||||
// Parse valid received command
|
||||
uint8_t cmd = this->read_buffer_[3];
|
||||
uint16_t argument = (this->read_buffer_[5] << 8) | this->read_buffer_[6];
|
||||
|
||||
ESP_LOGV(TAG, "Received message cmd: %#02x arg %#04x", cmd, argument);
|
||||
|
||||
switch (cmd) {
|
||||
case 0x3A:
|
||||
if (argument == 1) {
|
||||
ESP_LOGI(TAG, "USB loaded");
|
||||
} else if (argument == 2) {
|
||||
ESP_LOGI(TAG, "TF Card loaded");
|
||||
}
|
||||
break;
|
||||
case 0x3B:
|
||||
if (argument == 1) {
|
||||
ESP_LOGI(TAG, "USB unloaded");
|
||||
} else if (argument == 2) {
|
||||
ESP_LOGI(TAG, "TF Card unloaded");
|
||||
}
|
||||
break;
|
||||
case 0x3F:
|
||||
if (argument == 1) {
|
||||
ESP_LOGI(TAG, "USB available");
|
||||
} else if (argument == 2) {
|
||||
ESP_LOGI(TAG, "TF Card available");
|
||||
} else if (argument == 3) {
|
||||
ESP_LOGI(TAG, "USB, TF Card available");
|
||||
}
|
||||
break;
|
||||
case 0x40:
|
||||
ESP_LOGV(TAG, "Nack");
|
||||
this->ack_set_is_playing_ = false;
|
||||
this->ack_reset_is_playing_ = false;
|
||||
switch (argument) {
|
||||
case 0x01:
|
||||
ESP_LOGE(TAG, "Module is busy or uninitialized");
|
||||
break;
|
||||
case 0x02:
|
||||
ESP_LOGE(TAG, "Module is in sleep mode");
|
||||
break;
|
||||
case 0x03:
|
||||
ESP_LOGE(TAG, "Serial receive error");
|
||||
break;
|
||||
case 0x04:
|
||||
ESP_LOGE(TAG, "Checksum incorrect");
|
||||
break;
|
||||
case 0x05:
|
||||
ESP_LOGE(TAG, "Specified track is out of current track scope");
|
||||
this->is_playing_ = false;
|
||||
break;
|
||||
case 0x06:
|
||||
ESP_LOGE(TAG, "Specified track is not found");
|
||||
this->is_playing_ = false;
|
||||
break;
|
||||
case 0x07:
|
||||
ESP_LOGE(TAG,
|
||||
"Insertion error (an inserting operation only can be done when a track is being played)");
|
||||
break;
|
||||
case 0x08:
|
||||
ESP_LOGE(TAG, "SD card reading failed (SD card pulled out or damaged)");
|
||||
break;
|
||||
case 0x09:
|
||||
ESP_LOGE(TAG, "Entered into sleep mode");
|
||||
this->is_playing_ = false;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 0x41:
|
||||
ESP_LOGV(TAG, "Ack ok");
|
||||
this->is_playing_ |= this->ack_set_is_playing_;
|
||||
this->is_playing_ &= !this->ack_reset_is_playing_;
|
||||
this->ack_set_is_playing_ = false;
|
||||
this->ack_reset_is_playing_ = false;
|
||||
break;
|
||||
case 0x3C:
|
||||
ESP_LOGV(TAG, "Playback finished (USB drive)");
|
||||
this->is_playing_ = false;
|
||||
this->on_finished_playback_callback_.call();
|
||||
break;
|
||||
case 0x3D:
|
||||
ESP_LOGV(TAG, "Playback finished (SD card)");
|
||||
this->is_playing_ = false;
|
||||
this->on_finished_playback_callback_.call();
|
||||
break;
|
||||
default:
|
||||
ESP_LOGE(TAG, "Received unknown cmd %#02x arg %#04x", cmd, argument);
|
||||
}
|
||||
this->sent_cmd_ = 0;
|
||||
switch (this->read_pos_) {
|
||||
case 0: // Start mark
|
||||
if (byte != 0x7E)
|
||||
return;
|
||||
break;
|
||||
case 1: // Version
|
||||
if (byte != 0xFF) {
|
||||
ESP_LOGW(TAG, "Expected Version 0xFF, got %#02x", byte);
|
||||
this->read_pos_ = 0;
|
||||
continue;
|
||||
}
|
||||
this->read_buffer_[this->read_pos_] = byte;
|
||||
this->read_pos_++;
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case 2: // Buffer length
|
||||
if (byte != 0x06) {
|
||||
ESP_LOGW(TAG, "Expected Buffer length 0x06, got %#02x", byte);
|
||||
this->read_pos_ = 0;
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case 9: // End byte
|
||||
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
|
||||
char byte_sequence[100];
|
||||
byte_sequence[0] = '\0';
|
||||
for (size_t i = 0; i < this->read_pos_ + 1; ++i) {
|
||||
snprintf(byte_sequence + strlen(byte_sequence), sizeof(byte_sequence) - strlen(byte_sequence), "%02X ",
|
||||
this->read_buffer_[i]);
|
||||
}
|
||||
ESP_LOGVV(TAG, "Received byte sequence: %s", byte_sequence);
|
||||
#endif
|
||||
if (byte != 0xEF) {
|
||||
ESP_LOGW(TAG, "Expected end byte 0xEF, got %#02x", byte);
|
||||
this->read_pos_ = 0;
|
||||
return;
|
||||
}
|
||||
// Parse valid received command
|
||||
uint8_t cmd = this->read_buffer_[3];
|
||||
uint16_t argument = (this->read_buffer_[5] << 8) | this->read_buffer_[6];
|
||||
|
||||
ESP_LOGV(TAG, "Received message cmd: %#02x arg %#04x", cmd, argument);
|
||||
|
||||
switch (cmd) {
|
||||
case 0x3A:
|
||||
if (argument == 1) {
|
||||
ESP_LOGI(TAG, "USB loaded");
|
||||
} else if (argument == 2) {
|
||||
ESP_LOGI(TAG, "TF Card loaded");
|
||||
}
|
||||
break;
|
||||
case 0x3B:
|
||||
if (argument == 1) {
|
||||
ESP_LOGI(TAG, "USB unloaded");
|
||||
} else if (argument == 2) {
|
||||
ESP_LOGI(TAG, "TF Card unloaded");
|
||||
}
|
||||
break;
|
||||
case 0x3F:
|
||||
if (argument == 1) {
|
||||
ESP_LOGI(TAG, "USB available");
|
||||
} else if (argument == 2) {
|
||||
ESP_LOGI(TAG, "TF Card available");
|
||||
} else if (argument == 3) {
|
||||
ESP_LOGI(TAG, "USB, TF Card available");
|
||||
}
|
||||
break;
|
||||
case 0x40:
|
||||
ESP_LOGV(TAG, "Nack");
|
||||
this->ack_set_is_playing_ = false;
|
||||
this->ack_reset_is_playing_ = false;
|
||||
switch (argument) {
|
||||
case 0x01:
|
||||
ESP_LOGE(TAG, "Module is busy or uninitialized");
|
||||
break;
|
||||
case 0x02:
|
||||
ESP_LOGE(TAG, "Module is in sleep mode");
|
||||
break;
|
||||
case 0x03:
|
||||
ESP_LOGE(TAG, "Serial receive error");
|
||||
break;
|
||||
case 0x04:
|
||||
ESP_LOGE(TAG, "Checksum incorrect");
|
||||
break;
|
||||
case 0x05:
|
||||
ESP_LOGE(TAG, "Specified track is out of current track scope");
|
||||
this->is_playing_ = false;
|
||||
break;
|
||||
case 0x06:
|
||||
ESP_LOGE(TAG, "Specified track is not found");
|
||||
this->is_playing_ = false;
|
||||
break;
|
||||
case 0x07:
|
||||
ESP_LOGE(TAG, "Insertion error (an inserting operation only can be done when a track is being played)");
|
||||
break;
|
||||
case 0x08:
|
||||
ESP_LOGE(TAG, "SD card reading failed (SD card pulled out or damaged)");
|
||||
break;
|
||||
case 0x09:
|
||||
ESP_LOGE(TAG, "Entered into sleep mode");
|
||||
this->is_playing_ = false;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 0x41:
|
||||
ESP_LOGV(TAG, "Ack ok");
|
||||
this->is_playing_ |= this->ack_set_is_playing_;
|
||||
this->is_playing_ &= !this->ack_reset_is_playing_;
|
||||
this->ack_set_is_playing_ = false;
|
||||
this->ack_reset_is_playing_ = false;
|
||||
break;
|
||||
case 0x3C:
|
||||
ESP_LOGV(TAG, "Playback finished (USB drive)");
|
||||
this->is_playing_ = false;
|
||||
this->on_finished_playback_callback_.call();
|
||||
break;
|
||||
case 0x3D:
|
||||
ESP_LOGV(TAG, "Playback finished (SD card)");
|
||||
this->is_playing_ = false;
|
||||
this->on_finished_playback_callback_.call();
|
||||
break;
|
||||
default:
|
||||
ESP_LOGE(TAG, "Received unknown cmd %#02x arg %#04x", cmd, argument);
|
||||
}
|
||||
this->sent_cmd_ = 0;
|
||||
this->read_pos_ = 0;
|
||||
return;
|
||||
}
|
||||
}
|
||||
this->read_buffer_[this->read_pos_] = byte;
|
||||
this->read_pos_++;
|
||||
});
|
||||
}
|
||||
void DFPlayer::dump_config() { ESP_LOGCONFIG(TAG, "DFPlayer:"); }
|
||||
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
#include "dfrobot_sen0465_uart.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace esphome::dfrobot_sen0465_uart {
|
||||
|
||||
using dfrobot_sen0465_base::FRAME_HEAD;
|
||||
using dfrobot_sen0465_base::FRAME_SIZE;
|
||||
using dfrobot_sen0465_base::RESPONSE_TIMEOUT_ID;
|
||||
|
||||
@@ -18,30 +15,21 @@ void DFRobotSEN0465UARTComponent::setup() {
|
||||
}
|
||||
|
||||
void DFRobotSEN0465UARTComponent::loop() {
|
||||
// The tail of a reading the probe sent on its own may precede the answer; start at the next frame head.
|
||||
uint8_t byte;
|
||||
while (this->rx_count_ == 0 && this->peek_byte(&byte) && byte != FRAME_HEAD)
|
||||
this->read_byte(&byte);
|
||||
const size_t available = this->available();
|
||||
if (available == 0)
|
||||
return;
|
||||
const size_t len = std::min<size_t>(available, FRAME_SIZE - this->rx_count_);
|
||||
if (!this->read_array(this->rx_buffer_ + this->rx_count_, len))
|
||||
return;
|
||||
this->rx_count_ += len;
|
||||
if (this->rx_count_ < FRAME_SIZE)
|
||||
// The tail of a reading the probe sent on its own may precede the answer; the reader skips to the frame head.
|
||||
const uint8_t *frame = nullptr;
|
||||
this->read_available<FRAME_SIZE>([&](uint8_t c) { return (frame = this->reader_.feed(c)) == nullptr; });
|
||||
if (frame == nullptr)
|
||||
return;
|
||||
|
||||
this->rx_count_ = 0;
|
||||
this->cancel_timeout(RESPONSE_TIMEOUT_ID);
|
||||
this->disable_loop();
|
||||
this->handle_response_(this->rx_buffer_);
|
||||
this->handle_response_(frame);
|
||||
}
|
||||
|
||||
bool DFRobotSEN0465UARTComponent::write_frame(const uint8_t *frame) {
|
||||
// Drop anything left over, such as the answer to the mode change or readings sent before passive mode was set
|
||||
this->discard_available();
|
||||
this->rx_count_ = 0;
|
||||
this->reader_.reset();
|
||||
this->write_array(frame, FRAME_SIZE);
|
||||
return true;
|
||||
}
|
||||
@@ -50,7 +38,7 @@ void DFRobotSEN0465UARTComponent::read_response() {
|
||||
this->enable_loop();
|
||||
this->set_timeout(RESPONSE_TIMEOUT_ID, RESPONSE_TIMEOUT_MS, [this]() {
|
||||
this->disable_loop();
|
||||
this->rx_count_ = 0;
|
||||
this->reader_.reset();
|
||||
this->handle_no_response_();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "esphome/components/dfrobot_sen0465_base/dfrobot_sen0465_base.h"
|
||||
#include "esphome/components/uart/uart.h"
|
||||
#include "esphome/core/frame_reader.h"
|
||||
|
||||
namespace esphome::dfrobot_sen0465_uart {
|
||||
|
||||
@@ -17,8 +18,7 @@ class DFRobotSEN0465UARTComponent final : public dfrobot_sen0465_base::DFRobotSE
|
||||
bool write_frame(const uint8_t *frame) override;
|
||||
void read_response() override;
|
||||
|
||||
uint8_t rx_buffer_[dfrobot_sen0465_base::FRAME_SIZE];
|
||||
uint8_t rx_count_{0};
|
||||
FrameReader<dfrobot_sen0465_base::FRAME_SIZE, dfrobot_sen0465_base::FRAME_HEAD> reader_;
|
||||
};
|
||||
|
||||
} // namespace esphome::dfrobot_sen0465_uart
|
||||
@@ -5,19 +5,16 @@ import esphome.config_validation as cv
|
||||
from esphome.const import CONF_FACTORY_RESET, CONF_ID, CONF_RESTART
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import CONF_DFROBOT_SEN0609_ID, DFRobotSEN0609Component, dfrobot_sen0609_ns
|
||||
|
||||
DFRobotSEN0609Button = dfrobot_sen0609_ns.class_("DFRobotSEN0609Button", button.Button)
|
||||
ButtonAction = dfrobot_sen0609_ns.enum("ButtonAction")
|
||||
from .. import CONF_DFROBOT_SEN0609_ID, DFRobotSEN0609Component
|
||||
|
||||
CONFIG_SCHEMA = {
|
||||
cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase),
|
||||
cv.GenerateID(CONF_DFROBOT_SEN0609_ID): cv.use_id(DFRobotSEN0609Component),
|
||||
cv.Optional(CONF_FACTORY_RESET): button.factory_reset_button_schema(
|
||||
DFRobotSEN0609Button
|
||||
button.ApplyButton
|
||||
),
|
||||
cv.Optional(CONF_QUERY_PARAMS): button.query_button_schema(DFRobotSEN0609Button),
|
||||
cv.Optional(CONF_RESTART): button.restart_button_schema(DFRobotSEN0609Button),
|
||||
cv.Optional(CONF_QUERY_PARAMS): button.query_button_schema(button.ApplyButton),
|
||||
cv.Optional(CONF_RESTART): button.restart_button_schema(button.ApplyButton),
|
||||
}
|
||||
|
||||
|
||||
@@ -25,11 +22,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
hub = await cg.get_variable(config[CONF_DFROBOT_SEN0609_ID])
|
||||
buttons = button.sub_buttons(config, parent=hub)
|
||||
await buttons(
|
||||
CONF_FACTORY_RESET,
|
||||
hub.set_factory_reset_button,
|
||||
ButtonAction.BUTTON_FACTORY_RESET,
|
||||
CONF_FACTORY_RESET, hub.set_factory_reset_button, apply="factory_reset()"
|
||||
)
|
||||
await buttons(
|
||||
CONF_QUERY_PARAMS, hub.set_query_button, ButtonAction.BUTTON_QUERY_PARAMS
|
||||
)
|
||||
await buttons(CONF_RESTART, hub.set_restart_button, ButtonAction.BUTTON_RESTART)
|
||||
await buttons(CONF_QUERY_PARAMS, hub.set_query_button, apply="query_params()")
|
||||
await buttons(CONF_RESTART, hub.set_restart_button, apply="restart()")
|
||||
@@ -1,30 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/button/button.h"
|
||||
#include "../dfrobot_sen0609.h"
|
||||
|
||||
namespace esphome::dfrobot_sen0609 {
|
||||
|
||||
class DFRobotSEN0609Button final : public button::Button, public Parented<DFRobotSEN0609Component> {
|
||||
public:
|
||||
explicit DFRobotSEN0609Button(ButtonAction action) : action_(action) {}
|
||||
|
||||
protected:
|
||||
void press_action() override {
|
||||
switch (this->action_) {
|
||||
case BUTTON_RESTART:
|
||||
this->parent_->restart();
|
||||
break;
|
||||
case BUTTON_FACTORY_RESET:
|
||||
this->parent_->factory_reset();
|
||||
break;
|
||||
case BUTTON_QUERY_PARAMS:
|
||||
this->parent_->query_params();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ButtonAction action_;
|
||||
};
|
||||
|
||||
} // namespace esphome::dfrobot_sen0609
|
||||
@@ -5,7 +5,6 @@
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
@@ -175,21 +174,11 @@ void DFRobotSEN0609Component::setup() {
|
||||
}
|
||||
|
||||
void DFRobotSEN0609Component::loop() {
|
||||
uint8_t buf[MAX_LINE_LENGTH];
|
||||
size_t avail = this->available();
|
||||
while (avail > 0) {
|
||||
const size_t to_read = std::min(avail, sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
if (!this->read_failed_) {
|
||||
ESP_LOGW(TAG, "UART read failed");
|
||||
}
|
||||
this->read_failed_ = true;
|
||||
break;
|
||||
}
|
||||
if (this->read_available<MAX_LINE_LENGTH>([this](uint8_t b) { this->readline_(b); })) {
|
||||
this->read_failed_ = false;
|
||||
avail -= to_read;
|
||||
for (size_t i = 0; i < to_read; i++)
|
||||
this->readline_(buf[i]);
|
||||
} else if (!this->read_failed_) {
|
||||
ESP_LOGW(TAG, "UART read failed");
|
||||
this->read_failed_ = true;
|
||||
}
|
||||
this->run_sequencer_();
|
||||
}
|
||||
@@ -454,6 +443,15 @@ void DFRobotSEN0609Component::control_number(NumberSetting setting, float value)
|
||||
this->request_write_(static_cast<Param>(progmem_read_byte(&NUMBER_PARAMS[setting])));
|
||||
}
|
||||
|
||||
void DFRobotSEN0609Component::control_number(number::Number *number, float value) {
|
||||
for (uint8_t i = 0; i < NUMBER_COUNT; i++) {
|
||||
if (this->numbers_[i] == number) {
|
||||
this->control_number(static_cast<NumberSetting>(i), value);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DFRobotSEN0609Component::forget_number_changes_(Param param, bool unknown) {
|
||||
for (uint8_t i = 0; i < NUMBER_COUNT; i++) {
|
||||
if (progmem_read_byte(&NUMBER_PARAMS[i]) != param)
|
||||
|
||||
@@ -62,12 +62,6 @@ enum SwitchSetting : uint8_t {
|
||||
SWITCH_COUNT,
|
||||
};
|
||||
|
||||
enum ButtonAction : uint8_t {
|
||||
BUTTON_RESTART,
|
||||
BUTTON_FACTORY_RESET,
|
||||
BUTTON_QUERY_PARAMS,
|
||||
};
|
||||
|
||||
/// Module parameters; a write or read of each is one command. Pending work runs in this order.
|
||||
enum Param : uint8_t {
|
||||
PARAM_RUN_APP,
|
||||
@@ -130,6 +124,8 @@ class DFRobotSEN0609Component final : public Component, public uart::UARTDevice
|
||||
#ifdef USE_NUMBER
|
||||
void set_number(NumberSetting setting, number::Number *number) { this->numbers_[setting] = number; }
|
||||
void control_number(NumberSetting setting, float value);
|
||||
// Finds the setting by pointer so every number shares one Apply function
|
||||
void control_number(number::Number *number, float value);
|
||||
#endif
|
||||
#ifdef USE_SWITCH
|
||||
void set_switch(SwitchSetting setting, switch_::Switch *sw) { this->switches_[setting] = sw; }
|
||||
|
||||
@@ -31,7 +31,6 @@ from .. import (
|
||||
final_validate_mode,
|
||||
)
|
||||
|
||||
DFRobotSEN0609Number = dfrobot_sen0609_ns.class_("DFRobotSEN0609Number", number.Number)
|
||||
NumberSetting = dfrobot_sen0609_ns.enum("NumberSetting")
|
||||
|
||||
CONF_HOLD_SENSITIVITY = "hold_sensitivity"
|
||||
@@ -55,7 +54,7 @@ class Setting(NamedTuple):
|
||||
|
||||
def _distance(icon: str = ICON_RULER) -> cv.Schema:
|
||||
return number.number_schema(
|
||||
DFRobotSEN0609Number,
|
||||
number.ApplyNumber,
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
icon=icon,
|
||||
@@ -65,7 +64,7 @@ def _distance(icon: str = ICON_RULER) -> cv.Schema:
|
||||
|
||||
def _duration() -> cv.Schema:
|
||||
return number.number_schema(
|
||||
DFRobotSEN0609Number,
|
||||
number.ApplyNumber,
|
||||
device_class=DEVICE_CLASS_DURATION,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
icon=ICON_TIMELAPSE,
|
||||
@@ -75,7 +74,7 @@ def _duration() -> cv.Schema:
|
||||
|
||||
def _plain() -> cv.Schema:
|
||||
return number.number_schema(
|
||||
DFRobotSEN0609Number,
|
||||
number.ApplyNumber,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
icon=ICON_MOTION_SENSOR,
|
||||
)
|
||||
@@ -187,8 +186,8 @@ async def to_code(config: ConfigType) -> None:
|
||||
await numbers(
|
||||
s.key,
|
||||
lambda n, setting=s.setting: hub.set_number(setting, n),
|
||||
s.setting,
|
||||
min_value=s.min_value,
|
||||
max_value=max_range if s.max_value is None else s.max_value,
|
||||
step=s.step,
|
||||
apply="control_number(entity, value)",
|
||||
)
|
||||
@@ -1,18 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/number/number.h"
|
||||
#include "../dfrobot_sen0609.h"
|
||||
|
||||
namespace esphome::dfrobot_sen0609 {
|
||||
|
||||
class DFRobotSEN0609Number final : public number::Number, public Parented<DFRobotSEN0609Component> {
|
||||
public:
|
||||
explicit DFRobotSEN0609Number(NumberSetting setting) : setting_(setting) {}
|
||||
|
||||
protected:
|
||||
void control(float value) override { this->parent_->control_number(this->setting_, value); }
|
||||
|
||||
NumberSetting setting_;
|
||||
};
|
||||
|
||||
} // namespace esphome::dfrobot_sen0609
|
||||
@@ -1,7 +1,5 @@
|
||||
#include "ds1603l.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
@@ -10,35 +8,28 @@ namespace esphome::ds1603l {
|
||||
ESPHOME_LOG_TAG(TAG, "ds1603l.sensor");
|
||||
|
||||
void DS1603L::loop() {
|
||||
// Assemble frames one byte at a time so a stream that starts mid-frame can realign
|
||||
uint8_t byte;
|
||||
while (this->available() > 0 && this->read_byte(&byte)) {
|
||||
if (this->rx_count_ == 0 && byte != HEADER_BYTE) {
|
||||
ESP_LOGV(TAG, "Skipping byte 0x%02X while looking for header", byte);
|
||||
continue;
|
||||
this->read_available([this](uint8_t byte) {
|
||||
const uint8_t *frame = this->reader_.feed(byte);
|
||||
if (frame == nullptr) {
|
||||
if (this->reader_.length() == 0) {
|
||||
ESP_LOGV(TAG, "Skipping byte 0x%02X while looking for header", byte);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this->rx_buffer_[this->rx_count_++] = byte;
|
||||
if (this->rx_count_ < FRAME_SIZE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this->parse_data_()) {
|
||||
this->rx_count_ = 0;
|
||||
} else {
|
||||
if (!this->parse_data_(frame)) {
|
||||
// The header byte was part of the payload of a misaligned frame, so realign instead of dropping everything
|
||||
this->resync_();
|
||||
this->reader_.resync();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void DS1603L::dump_config() { LOG_SENSOR("", "DS1603L", this); }
|
||||
|
||||
bool DS1603L::parse_data_() {
|
||||
uint8_t header = this->rx_buffer_[0];
|
||||
uint8_t data_h = this->rx_buffer_[1];
|
||||
uint8_t data_l = this->rx_buffer_[2];
|
||||
uint8_t checksum = this->rx_buffer_[3];
|
||||
bool DS1603L::parse_data_(const uint8_t *frame) {
|
||||
uint8_t header = frame[0];
|
||||
uint8_t data_h = frame[1];
|
||||
uint8_t data_l = frame[2];
|
||||
uint8_t checksum = frame[3];
|
||||
|
||||
uint8_t computed_checksum = (header + data_h + data_l) & 0xFF;
|
||||
|
||||
@@ -53,16 +44,4 @@ bool DS1603L::parse_data_() {
|
||||
return true;
|
||||
}
|
||||
|
||||
void DS1603L::resync_() {
|
||||
// Drop the byte that was treated as the header, then look for the next candidate header in what is left
|
||||
size_t start = 1;
|
||||
while (start < this->rx_count_ && this->rx_buffer_[start] != HEADER_BYTE) {
|
||||
start++;
|
||||
}
|
||||
this->rx_count_ -= start;
|
||||
if (this->rx_count_ > 0) {
|
||||
memmove(this->rx_buffer_, this->rx_buffer_ + start, this->rx_count_);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::ds1603l
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/uart/uart.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/frame_reader.h"
|
||||
|
||||
namespace esphome::ds1603l {
|
||||
|
||||
@@ -18,13 +19,10 @@ class DS1603L final : public sensor::Sensor, public Component, public uart::UART
|
||||
static constexpr uint8_t HEADER_BYTE = 0xFF;
|
||||
static constexpr size_t FRAME_SIZE = 4;
|
||||
|
||||
// Validates the checksum of the frame in rx_buffer_ and publishes it. Returns false if the frame is invalid.
|
||||
bool parse_data_();
|
||||
// Drops the first buffered byte and realigns the buffer on the next possible header byte.
|
||||
void resync_();
|
||||
// Validates the checksum of the frame and publishes it. Returns false if the frame is invalid.
|
||||
bool parse_data_(const uint8_t *frame);
|
||||
|
||||
uint8_t rx_buffer_[FRAME_SIZE]; // Buffer for the frame being assembled
|
||||
size_t rx_count_{0}; // Number of bytes currently in rx_buffer_
|
||||
FrameReader<FRAME_SIZE, HEADER_BYTE> reader_;
|
||||
};
|
||||
|
||||
} // namespace esphome::ds1603l
|
||||
@@ -1,20 +1,18 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import button
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import ENTITY_CATEGORY_CONFIG
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import CONF_ESPECTRE_ID, ESPectreComponent, espectre_ns
|
||||
from .. import CONF_ESPECTRE_ID, ESPectreComponent
|
||||
|
||||
DEPENDENCIES = ["espectre"]
|
||||
|
||||
RecalibrateButton = espectre_ns.class_("RecalibrateButton", button.Button)
|
||||
|
||||
CONFIG_SCHEMA = button.button_schema(
|
||||
RecalibrateButton, entity_category=ENTITY_CATEGORY_CONFIG
|
||||
button.ApplyButton, entity_category=ENTITY_CATEGORY_CONFIG
|
||||
).extend({cv.GenerateID(CONF_ESPECTRE_ID): cv.use_id(ESPectreComponent)})
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await button.new_button(config)
|
||||
await cg.register_parented(var, config[CONF_ESPECTRE_ID])
|
||||
buttons = button.sub_buttons(config, parent=config[CONF_ESPECTRE_ID])
|
||||
await buttons.create(config, None, apply="recalibrate()")
|
||||
@@ -1,19 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
#ifdef USE_ESPECTRE
|
||||
|
||||
#include "esphome/components/button/button.h"
|
||||
#include "../espectre.h"
|
||||
|
||||
namespace esphome::espectre {
|
||||
|
||||
class RecalibrateButton final : public button::Button, public Parented<ESPectreComponent> {
|
||||
protected:
|
||||
void press_action() override { this->parent_->recalibrate(); }
|
||||
};
|
||||
|
||||
} // namespace esphome::espectre
|
||||
|
||||
#endif // USE_ESPECTRE
|
||||
@@ -1,6 +1,5 @@
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import mqtt, web_server
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_DEVICE_CLASS,
|
||||
@@ -10,15 +9,15 @@ from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_MQTT_ID,
|
||||
CONF_ON_EVENT,
|
||||
CONF_WEB_SERVER,
|
||||
DEVICE_CLASS_BUTTON,
|
||||
DEVICE_CLASS_DOORBELL,
|
||||
DEVICE_CLASS_EMPTY,
|
||||
DEVICE_CLASS_MOTION,
|
||||
)
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority, entity_helpers
|
||||
from esphome.core.entity_helpers import (
|
||||
entity_duplicate_validator,
|
||||
mqtt_component_class,
|
||||
queue_entity_register,
|
||||
setup_device_class,
|
||||
setup_entity,
|
||||
@@ -44,11 +43,13 @@ EventPtr = Event.operator("ptr")
|
||||
validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_")
|
||||
|
||||
_EVENT_SCHEMA = (
|
||||
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
|
||||
cv.ENTITY_BASE_SCHEMA.extend(entity_helpers.WEBSERVER_SORTING_SCHEMA)
|
||||
.extend(cv.MQTT_COMPONENT_SCHEMA)
|
||||
.extend(
|
||||
{
|
||||
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTEventComponent),
|
||||
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(
|
||||
mqtt_component_class("MQTTEventComponent")
|
||||
),
|
||||
cv.GenerateID(): cv.declare_id(Event),
|
||||
cv.Optional(
|
||||
CONF_DEVICE_CLASS, visibility=cv.Visibility.ADVANCED
|
||||
@@ -104,12 +105,7 @@ async def setup_event_core_(
|
||||
|
||||
setup_device_class(config)
|
||||
|
||||
if mqtt_id := config.get(CONF_MQTT_ID):
|
||||
mqtt_ = cg.new_Pvariable(mqtt_id, var)
|
||||
await mqtt.register_mqtt_component(mqtt_, config)
|
||||
|
||||
if web_server_config := config.get(CONF_WEB_SERVER):
|
||||
await web_server.add_entity_config(var, web_server_config)
|
||||
await entity_helpers.setup_entity_integrations(var, config)
|
||||
|
||||
|
||||
async def register_event(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "ezo_pmp.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
namespace esphome::ezo_pmp {
|
||||
|
||||
@@ -31,11 +32,24 @@ static const uint16_t EZO_PMP_COMMAND_READ_ABSOLUTE_TOTAL_VOLUME_DOSED = 65;
|
||||
static const uint16_t EZO_PMP_COMMAND_READ_CALIBRATION_STATUS = 129;
|
||||
static const uint16_t EZO_PMP_COMMAND_READ_PUMP_VOLTAGE = 257;
|
||||
|
||||
static const std::string DOSING_MODE_NONE = "None";
|
||||
static const std::string DOSING_MODE_VOLUME = "Volume";
|
||||
static const std::string DOSING_MODE_VOLUME_OVER_TIME = "Volume/Time";
|
||||
static const std::string DOSING_MODE_CONSTANT_FLOW_RATE = "Constant Flow Rate";
|
||||
static const std::string DOSING_MODE_CONTINUOUS = "Continuous";
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
enum DosingMode : uint8_t {
|
||||
DOSING_MODE_NONE,
|
||||
DOSING_MODE_VOLUME,
|
||||
DOSING_MODE_VOLUME_OVER_TIME,
|
||||
DOSING_MODE_CONSTANT_FLOW_RATE,
|
||||
DOSING_MODE_CONTINUOUS,
|
||||
};
|
||||
PROGMEM_STRING_TABLE(DosingModeStrings, "None", "Volume", "Volume/Time", "Constant Flow Rate", "Continuous");
|
||||
|
||||
static void publish_dosing_mode(text_sensor::TextSensor *sensor, DosingMode mode) {
|
||||
if (sensor == nullptr)
|
||||
return;
|
||||
const ProgmemStr name = DosingModeStrings::get_progmem_str(mode, DOSING_MODE_NONE);
|
||||
if (ESPHOME_strcmp_P(sensor->state.c_str(), reinterpret_cast<ESPHOME_PGM_P>(name)) != 0)
|
||||
sensor->publish_state(name);
|
||||
}
|
||||
#endif
|
||||
|
||||
void EzoPMP::dump_config() {
|
||||
LOG_I2C_DEVICE(this);
|
||||
@@ -212,8 +226,7 @@ void EzoPMP::read_command_result_() {
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
if (!this->is_dosing_flag_ && !this->is_paused_flag_) {
|
||||
// If pump is not paused and not dispensing
|
||||
if (this->dosing_mode_ && this->dosing_mode_->state != DOSING_MODE_NONE)
|
||||
this->dosing_mode_->publish_state(DOSING_MODE_NONE);
|
||||
publish_dosing_mode(this->dosing_mode_, DOSING_MODE_NONE);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -285,29 +298,25 @@ void EzoPMP::read_command_result_() {
|
||||
|
||||
case EZO_PMP_COMMAND_DOSE_VOLUME: // Volume Dispensing (page 55)
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
if (this->dosing_mode_ && this->dosing_mode_->state != DOSING_MODE_VOLUME)
|
||||
this->dosing_mode_->publish_state(DOSING_MODE_VOLUME);
|
||||
publish_dosing_mode(this->dosing_mode_, DOSING_MODE_VOLUME);
|
||||
#endif
|
||||
break;
|
||||
|
||||
case EZO_PMP_COMMAND_DOSE_VOLUME_OVER_TIME: // Dose over time (page 56)
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
if (this->dosing_mode_ && this->dosing_mode_->state != DOSING_MODE_VOLUME_OVER_TIME)
|
||||
this->dosing_mode_->publish_state(DOSING_MODE_VOLUME_OVER_TIME);
|
||||
publish_dosing_mode(this->dosing_mode_, DOSING_MODE_VOLUME_OVER_TIME);
|
||||
#endif
|
||||
break;
|
||||
|
||||
case EZO_PMP_COMMAND_DOSE_WITH_CONSTANT_FLOW_RATE: // Constant Flow Rate (page 57)
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
if (this->dosing_mode_ && this->dosing_mode_->state != DOSING_MODE_CONSTANT_FLOW_RATE)
|
||||
this->dosing_mode_->publish_state(DOSING_MODE_CONSTANT_FLOW_RATE);
|
||||
publish_dosing_mode(this->dosing_mode_, DOSING_MODE_CONSTANT_FLOW_RATE);
|
||||
#endif
|
||||
break;
|
||||
|
||||
case EZO_PMP_COMMAND_DOSE_CONTINUOUSLY: // Continuous Dispensing (page 54)
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
if (this->dosing_mode_ && this->dosing_mode_->state != DOSING_MODE_CONTINUOUS)
|
||||
this->dosing_mode_->publish_state(DOSING_MODE_CONTINUOUS);
|
||||
publish_dosing_mode(this->dosing_mode_, DOSING_MODE_CONTINUOUS);
|
||||
#endif
|
||||
break;
|
||||
|
||||
@@ -318,8 +327,7 @@ void EzoPMP::read_command_result_() {
|
||||
this->is_paused_->publish_state(this->is_paused_flag_);
|
||||
#endif
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
if (this->dosing_mode_ && this->dosing_mode_->state != DOSING_MODE_NONE)
|
||||
this->dosing_mode_->publish_state(DOSING_MODE_NONE);
|
||||
publish_dosing_mode(this->dosing_mode_, DOSING_MODE_NONE);
|
||||
#endif
|
||||
break;
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from esphome import automation
|
||||
from esphome.automation import maybe_simple_id
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import mqtt, web_server
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_DIRECTION,
|
||||
@@ -29,11 +28,11 @@ from esphome.const import (
|
||||
CONF_SPEED_LEVEL_STATE_TOPIC,
|
||||
CONF_SPEED_STATE_TOPIC,
|
||||
CONF_TRIGGER_ID,
|
||||
CONF_WEB_SERVER,
|
||||
)
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority, entity_helpers
|
||||
from esphome.core.entity_helpers import (
|
||||
entity_duplicate_validator,
|
||||
mqtt_component_class,
|
||||
queue_entity_register,
|
||||
setup_entity,
|
||||
)
|
||||
@@ -78,14 +77,16 @@ FanPresetSetTrigger = fan_ns.class_(
|
||||
)
|
||||
|
||||
_FAN_SCHEMA = (
|
||||
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
|
||||
cv.ENTITY_BASE_SCHEMA.extend(entity_helpers.WEBSERVER_SORTING_SCHEMA)
|
||||
.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
|
||||
.extend(
|
||||
{
|
||||
cv.Optional(CONF_RESTORE_MODE, default="ALWAYS_OFF"): cv.enum(
|
||||
RESTORE_MODES, upper=True, space="_"
|
||||
),
|
||||
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTFanComponent),
|
||||
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(
|
||||
mqtt_component_class("MQTTFanComponent")
|
||||
),
|
||||
cv.Optional(CONF_DIRECTION_STATE_TOPIC): cv.All(
|
||||
cv.requires_component("mqtt"), cv.publish_topic
|
||||
),
|
||||
@@ -240,10 +241,8 @@ _TRIGGER_AUTOMATIONS = (
|
||||
async def setup_fan_core_(var, config):
|
||||
cg.add(var.set_restore_mode(config[CONF_RESTORE_MODE]))
|
||||
|
||||
if (mqtt_id := config.get(CONF_MQTT_ID)) is not None:
|
||||
mqtt_ = cg.new_Pvariable(mqtt_id, var)
|
||||
await mqtt.register_mqtt_component(mqtt_, config)
|
||||
|
||||
mqtt_ = await entity_helpers.setup_entity_integrations(var, config)
|
||||
if mqtt_ is not None:
|
||||
if (
|
||||
direction_state_topic := config.get(CONF_DIRECTION_STATE_TOPIC)
|
||||
) is not None:
|
||||
@@ -277,9 +276,6 @@ async def setup_fan_core_(var, config):
|
||||
if (speed_command_topic := config.get(CONF_SPEED_COMMAND_TOPIC)) is not None:
|
||||
cg.add(mqtt_.set_custom_speed_command_topic(speed_command_topic))
|
||||
|
||||
if web_server_config := config.get(CONF_WEB_SERVER):
|
||||
await web_server.add_entity_config(var, web_server_config)
|
||||
|
||||
for conf in config.get(CONF_ON_STATE, []):
|
||||
await automation.build_parent_callback_automation(
|
||||
var, "add_on_state_callback", (Fan.operator("ptr"), "x"), conf
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import button
|
||||
import esphome.config_validation as cv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from ..climate import CONF_HAIER_ID, HonClimate, haier_ns
|
||||
from ..climate import CONF_HAIER_ID, HonClimate
|
||||
|
||||
CODEOWNERS = ["@paveldn"]
|
||||
SelfCleaningButton = haier_ns.class_("SelfCleaningButton", button.Button)
|
||||
SteriCleaningButton = haier_ns.class_("SteriCleaningButton", button.Button)
|
||||
|
||||
|
||||
# Haier buttons
|
||||
@@ -20,19 +18,18 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_HAIER_ID): cv.use_id(HonClimate),
|
||||
cv.Optional(CONF_SELF_CLEANING): button.button_schema(
|
||||
SelfCleaningButton,
|
||||
button.ApplyButton,
|
||||
icon=ICON_SPRAY_BOTTLE,
|
||||
),
|
||||
cv.Optional(CONF_STERI_CLEANING): button.button_schema(
|
||||
SteriCleaningButton,
|
||||
button.ApplyButton,
|
||||
icon=ICON_SPRAY_BOTTLE,
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
for button_type in [CONF_SELF_CLEANING, CONF_STERI_CLEANING]:
|
||||
if conf := config.get(button_type):
|
||||
btn = await button.new_button(conf)
|
||||
await cg.register_parented(btn, config[CONF_HAIER_ID])
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
buttons = button.sub_buttons(config, parent=config[CONF_HAIER_ID])
|
||||
await buttons(CONF_SELF_CLEANING, None, apply="start_self_cleaning()")
|
||||
await buttons(CONF_STERI_CLEANING, None, apply="start_steri_cleaning()")
|
||||
@@ -1,7 +0,0 @@
|
||||
#include "self_cleaning.h"
|
||||
|
||||
namespace esphome::haier {
|
||||
|
||||
void SelfCleaningButton::press_action() { this->parent_->start_self_cleaning(); }
|
||||
|
||||
} // namespace esphome::haier
|
||||
@@ -1,16 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/button/button.h"
|
||||
#include "../hon_climate.h"
|
||||
|
||||
namespace esphome::haier {
|
||||
|
||||
class SelfCleaningButton final : public button::Button, public Parented<HonClimate> {
|
||||
public:
|
||||
SelfCleaningButton() = default;
|
||||
|
||||
protected:
|
||||
void press_action() override;
|
||||
};
|
||||
|
||||
} // namespace esphome::haier
|
||||
@@ -1,7 +0,0 @@
|
||||
#include "steri_cleaning.h"
|
||||
|
||||
namespace esphome::haier {
|
||||
|
||||
void SteriCleaningButton::press_action() { this->parent_->start_steri_cleaning(); }
|
||||
|
||||
} // namespace esphome::haier
|
||||
@@ -1,16 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/button/button.h"
|
||||
#include "../hon_climate.h"
|
||||
|
||||
namespace esphome::haier {
|
||||
|
||||
class SteriCleaningButton final : public button::Button, public Parented<HonClimate> {
|
||||
public:
|
||||
SteriCleaningButton() = default;
|
||||
|
||||
protected:
|
||||
void press_action() override;
|
||||
};
|
||||
|
||||
} // namespace esphome::haier
|
||||
@@ -1,10 +1,9 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import button
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import ICON_AIR_FILTER
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import CONF_HOERMANN_HCP_ID, HoermannHcp, hoermann_hcp_ns
|
||||
from .. import CONF_HOERMANN_HCP_ID, HoermannHcp
|
||||
|
||||
DEPENDENCIES = ["hoermann_hcp"]
|
||||
|
||||
@@ -13,10 +12,6 @@ CONF_VENT = "vent"
|
||||
|
||||
ICON_GARAGE_OPEN_VARIANT = "mdi:garage-open-variant"
|
||||
|
||||
HoermannHcpVentButton = hoermann_hcp_ns.class_("HoermannHcpVentButton", button.Button)
|
||||
HoermannHcpHalfOpenButton = hoermann_hcp_ns.class_(
|
||||
"HoermannHcpHalfOpenButton", button.Button
|
||||
)
|
||||
|
||||
BUTTON_KEYS = (CONF_VENT, CONF_HALF_OPEN)
|
||||
|
||||
@@ -25,10 +20,10 @@ CONFIG_SCHEMA = cv.All(
|
||||
{
|
||||
cv.GenerateID(CONF_HOERMANN_HCP_ID): cv.use_id(HoermannHcp),
|
||||
cv.Optional(CONF_VENT): button.button_schema(
|
||||
HoermannHcpVentButton, icon=ICON_AIR_FILTER
|
||||
button.ApplyButton, icon=ICON_AIR_FILTER
|
||||
),
|
||||
cv.Optional(CONF_HALF_OPEN): button.button_schema(
|
||||
HoermannHcpHalfOpenButton, icon=ICON_GARAGE_OPEN_VARIANT
|
||||
button.ApplyButton, icon=ICON_GARAGE_OPEN_VARIANT
|
||||
),
|
||||
}
|
||||
),
|
||||
@@ -37,7 +32,6 @@ CONFIG_SCHEMA = cv.All(
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
parent = await cg.get_variable(config[CONF_HOERMANN_HCP_ID])
|
||||
for key in BUTTON_KEYS:
|
||||
if (conf := config.get(key)) is not None:
|
||||
await button.new_button(conf, parent)
|
||||
buttons = button.sub_buttons(config, parent=config[CONF_HOERMANN_HCP_ID])
|
||||
await buttons(CONF_VENT, None, apply="vent_door()")
|
||||
await buttons(CONF_HALF_OPEN, None, apply="half_open_door()")
|
||||
@@ -1,34 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/button/button.h"
|
||||
#include "../hoermann_hcp.h"
|
||||
|
||||
namespace esphome::hoermann_hcp {
|
||||
|
||||
// The door commands the cover has no equivalent for. A refused command is already reported by the hub and
|
||||
// leaves nothing to correct here, because a button carries no state of its own.
|
||||
class HoermannHcpButton : public button::Button {
|
||||
public:
|
||||
explicit HoermannHcpButton(HoermannHcp *parent) : parent_(parent) {}
|
||||
|
||||
protected:
|
||||
HoermannHcp *const parent_;
|
||||
};
|
||||
|
||||
class HoermannHcpVentButton final : public HoermannHcpButton {
|
||||
public:
|
||||
using HoermannHcpButton::HoermannHcpButton;
|
||||
|
||||
protected:
|
||||
void press_action() override { this->parent_->vent_door(); }
|
||||
};
|
||||
|
||||
class HoermannHcpHalfOpenButton final : public HoermannHcpButton {
|
||||
public:
|
||||
using HoermannHcpButton::HoermannHcpButton;
|
||||
|
||||
protected:
|
||||
void press_action() override { this->parent_->half_open_door(); }
|
||||
};
|
||||
|
||||
} // namespace esphome::hoermann_hcp
|
||||
@@ -41,11 +41,13 @@ from esphome.const import (
|
||||
CONF_FREQUENCY,
|
||||
CONF_I2C_ID,
|
||||
CONF_ID,
|
||||
CONF_INVERTED,
|
||||
CONF_LOW_POWER_MODE,
|
||||
CONF_SCAN,
|
||||
CONF_SCL,
|
||||
CONF_SDA,
|
||||
CONF_TIMEOUT,
|
||||
PLATFORM_BK72XX,
|
||||
PLATFORM_ESP32,
|
||||
PLATFORM_ESP8266,
|
||||
PLATFORM_HOST,
|
||||
@@ -68,6 +70,7 @@ ArduinoI2CBus = i2c_ns.class_("ArduinoI2CBus", InternalI2CBus, cg.Component)
|
||||
IDFI2CBus = i2c_ns.class_("IDFI2CBus", InternalI2CBus, cg.Component)
|
||||
ZephyrI2CBus = i2c_ns.class_("ZephyrI2CBus", I2CBus, cg.Component)
|
||||
HostI2CBus = i2c_ns.class_("HostI2CBus", I2CBus, cg.Component)
|
||||
SoftwareI2CBus = i2c_ns.class_("SoftwareI2CBus", I2CBus, cg.Component)
|
||||
I2CDevice = i2c_ns.class_("I2CDevice")
|
||||
|
||||
ESP32_I2C_CAPABILITIES = {
|
||||
@@ -108,6 +111,8 @@ def validate_device(value: str) -> str:
|
||||
def _bus_declare_type(value: Any) -> ID:
|
||||
if CORE.is_esp32:
|
||||
return cv.declare_id(IDFI2CBus)(value)
|
||||
if CORE.is_libretiny:
|
||||
return cv.declare_id(SoftwareI2CBus)(value)
|
||||
if CORE.using_arduino:
|
||||
return cv.declare_id(ArduinoI2CBus)(value)
|
||||
if CORE.using_zephyr:
|
||||
@@ -128,7 +133,28 @@ def _rp2040_i2c_controller(pin: int) -> int:
|
||||
return (pin // 2) % 2
|
||||
|
||||
|
||||
def _i2c_pin_number(value: Any) -> Any:
|
||||
# The software bus on LibreTiny drives the pins through the GPIO pin objects and
|
||||
# switches them between input with pull-up and output at runtime.
|
||||
if CORE.is_libretiny:
|
||||
return pins.internal_gpio_input_pullup_pin_schema(value)
|
||||
return pins.internal_gpio_pin_number(value)
|
||||
|
||||
|
||||
def validate_config(config: ConfigType) -> ConfigType:
|
||||
if CORE.is_libretiny:
|
||||
if int(config[CONF_FREQUENCY]) < 1:
|
||||
raise cv.Invalid("The frequency must be at least 1Hz", [CONF_FREQUENCY])
|
||||
# no default pins on LibreTiny, the bus is bit-banged on any two GPIOs
|
||||
for key in (CONF_SDA, CONF_SCL):
|
||||
if key not in config:
|
||||
raise cv.Invalid(f"'{key}' is required on this platform", [key])
|
||||
if config[key].get(CONF_INVERTED):
|
||||
# the software bus pulls a line low by writing a low level to the pin
|
||||
raise cv.Invalid(
|
||||
f"'{CONF_INVERTED}' is not supported on the I2C pins",
|
||||
[key, CONF_INVERTED],
|
||||
)
|
||||
if CORE.is_esp32:
|
||||
return cv.require_framework_version(
|
||||
esp_idf=cv.Version(5, 4, 2), esp32_arduino=cv.Version(3, 2, 1)
|
||||
@@ -181,7 +207,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
esp8266="SDA",
|
||||
rp2="SDA",
|
||||
nrf52="SDA",
|
||||
): pins.internal_gpio_pin_number,
|
||||
): _i2c_pin_number,
|
||||
cv.SplitDefault(CONF_SDA_PULLUP_ENABLED, esp32=True): cv.All(
|
||||
cv.only_on_esp32, cv.boolean
|
||||
),
|
||||
@@ -191,7 +217,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
esp8266="SCL",
|
||||
rp2="SCL",
|
||||
nrf52="SCL",
|
||||
): pins.internal_gpio_pin_number,
|
||||
): _i2c_pin_number,
|
||||
cv.SplitDefault(CONF_SCL_PULLUP_ENABLED, esp32=True): cv.All(
|
||||
cv.only_on_esp32, cv.boolean
|
||||
),
|
||||
@@ -202,6 +228,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
rp2="50kHz",
|
||||
nrf52="100kHz",
|
||||
host="50kHz",
|
||||
bk72xx="100kHz",
|
||||
): cv.All(
|
||||
cv.frequency,
|
||||
cv.float_range(min=0, min_included=False),
|
||||
@@ -225,6 +252,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
cv.only_on(
|
||||
[
|
||||
PLATFORM_BK72XX,
|
||||
PLATFORM_ESP32,
|
||||
PLATFORM_ESP8266,
|
||||
PLATFORM_RP2,
|
||||
@@ -343,6 +371,16 @@ async def to_code(config: ConfigType) -> None:
|
||||
cg.add(var.set_timeout(int(config[CONF_TIMEOUT].total_microseconds)))
|
||||
if CONF_LOW_POWER_MODE in config:
|
||||
cg.add(var.set_lp_mode(bool(config[CONF_LOW_POWER_MODE])))
|
||||
elif CORE.is_libretiny:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
|
||||
cg.add(var.set_sda_pin(await cg.gpio_pin_expression(config[CONF_SDA])))
|
||||
cg.add(var.set_scl_pin(await cg.gpio_pin_expression(config[CONF_SCL])))
|
||||
cg.add(var.set_frequency(int(config[CONF_FREQUENCY])))
|
||||
cg.add(var.set_scan(config[CONF_SCAN]))
|
||||
if CONF_TIMEOUT in config:
|
||||
cg.add(var.set_timeout(int(config[CONF_TIMEOUT].total_microseconds)))
|
||||
else:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
@@ -462,7 +500,6 @@ _platform_filter = filter_source_files_from_platform(
|
||||
"i2c_bus_arduino.cpp": {
|
||||
PlatformFramework.ESP8266_ARDUINO,
|
||||
PlatformFramework.RP2_ARDUINO,
|
||||
PlatformFramework.BK72XX_ARDUINO,
|
||||
PlatformFramework.RTL87XX_ARDUINO,
|
||||
PlatformFramework.LN882X_ARDUINO,
|
||||
},
|
||||
@@ -470,6 +507,7 @@ _platform_filter = filter_source_files_from_platform(
|
||||
PlatformFramework.ESP32_ARDUINO,
|
||||
PlatformFramework.ESP32_IDF,
|
||||
},
|
||||
"i2c_bus_software.cpp": {PlatformFramework.BK72XX_ARDUINO},
|
||||
"i2c_bus_zephyr.cpp": {PlatformFramework.NRF52_ZEPHYR},
|
||||
"i2c_bus_host.cpp": {PlatformFramework.HOST_NATIVE},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
#ifdef USE_LIBRETINY
|
||||
|
||||
#include "i2c_bus_software.h"
|
||||
#include <cinttypes>
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::i2c {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "i2c.software");
|
||||
|
||||
// Maximum bytes to log in hex format (truncates larger transfers)
|
||||
static constexpr size_t I2C_MAX_LOG_BYTES = 32;
|
||||
// Clock pulses used to free a device that is holding SDA low
|
||||
static constexpr uint8_t RECOVERY_CLOCK_PULSES = 9;
|
||||
|
||||
// A line is released by switching the pin to input with pull-up and pulled low by driving it as output.
|
||||
static inline void release_line(InternalGPIOPin *pin) { pin->pin_mode(gpio::FLAG_INPUT | gpio::FLAG_PULLUP); }
|
||||
static inline void pull_line_low(InternalGPIOPin *pin) {
|
||||
pin->digital_write(false);
|
||||
pin->pin_mode(gpio::FLAG_OUTPUT);
|
||||
}
|
||||
|
||||
void SoftwareI2CBus::setup() {
|
||||
this->update_half_period_();
|
||||
this->sda_pin_->setup();
|
||||
this->scl_pin_->setup();
|
||||
release_line(this->sda_pin_);
|
||||
release_line(this->scl_pin_);
|
||||
if (!this->recover_()) {
|
||||
// A line that stays low would be read as an ACK for every address and as zero data
|
||||
ESP_LOGE(TAG, "SDA or SCL is held low, the bus cannot be used");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
this->initialized_ = true;
|
||||
if (this->scan_) {
|
||||
ESP_LOGV(TAG, "Scanning bus for active devices");
|
||||
this->i2c_scan_();
|
||||
}
|
||||
}
|
||||
|
||||
void SoftwareI2CBus::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "I2C Bus (software):");
|
||||
LOG_PIN(" SDA Pin: ", this->sda_pin_);
|
||||
LOG_PIN(" SCL Pin: ", this->scl_pin_);
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Frequency: %" PRIu32 " Hz\n"
|
||||
" Clock stretch timeout: %" PRIu32 " us",
|
||||
this->frequency_, this->timeout_);
|
||||
if (this->recovered_) {
|
||||
ESP_LOGCONFIG(TAG, " Recovery: bus successfully recovered");
|
||||
}
|
||||
if (this->scan_) {
|
||||
ESP_LOGI(TAG, "Results from bus scan:");
|
||||
if (this->scan_results_.empty()) {
|
||||
ESP_LOGI(TAG, "Found no devices");
|
||||
} else {
|
||||
for (const auto &s : this->scan_results_) {
|
||||
if (s.second) {
|
||||
ESP_LOGI(TAG, "Found device at address 0x%02X", s.first);
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Unknown error at address 0x%02X", s.first);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Half of one clock period in microseconds, rounded up. The frequency is validated to be non-zero.
|
||||
void SoftwareI2CBus::update_half_period_() {
|
||||
this->half_period_us_ = (500000 + this->frequency_ - 1) / this->frequency_;
|
||||
}
|
||||
|
||||
#ifdef I2C_PORT_FREQUENCY_COUNT
|
||||
ErrorCode SoftwareI2CBus::switch_frequency(uint32_t frequency) {
|
||||
if (frequency == 0)
|
||||
return ERROR_INVALID_ARGUMENT;
|
||||
this->frequency_ = frequency;
|
||||
this->update_half_period_();
|
||||
return ERROR_OK;
|
||||
}
|
||||
#endif
|
||||
|
||||
bool SoftwareI2CBus::recover_() {
|
||||
this->timed_out_ = false;
|
||||
// A device may still be stretching the clock, give SCL the configured time to rise before looking at SDA
|
||||
this->release_scl_();
|
||||
// A device that was interrupted in the middle of a transfer can hold SDA low; clock it out.
|
||||
bool clocked = false;
|
||||
for (uint8_t i = 0; i < RECOVERY_CLOCK_PULSES && !this->sda_pin_->digital_read(); i++) {
|
||||
pull_line_low(this->scl_pin_);
|
||||
delayMicroseconds(this->half_period_us_);
|
||||
// wait for the rising edge, the device may stretch the clock
|
||||
if (!this->release_scl_())
|
||||
break;
|
||||
delayMicroseconds(this->half_period_us_);
|
||||
clocked = true;
|
||||
}
|
||||
bool ok = this->sda_pin_->digital_read() && this->scl_pin_->digital_read();
|
||||
if (ok && clocked) {
|
||||
// end the interrupted transfer with a STOP so every device returns to idle
|
||||
this->recovered_ = true;
|
||||
pull_line_low(this->sda_pin_);
|
||||
delayMicroseconds(this->half_period_us_);
|
||||
release_line(this->scl_pin_);
|
||||
delayMicroseconds(this->half_period_us_);
|
||||
release_line(this->sda_pin_);
|
||||
}
|
||||
if (!ok) {
|
||||
ESP_LOGW(TAG, "Bus recovery failed, a line is held low");
|
||||
}
|
||||
this->timed_out_ = false;
|
||||
return ok;
|
||||
}
|
||||
|
||||
// Release SCL and wait for it to go high, a device may stretch the clock.
|
||||
bool SoftwareI2CBus::release_scl_() {
|
||||
release_line(this->scl_pin_);
|
||||
uint32_t start = micros();
|
||||
while (!this->scl_pin_->digital_read()) {
|
||||
if (micros() - start > this->timeout_) {
|
||||
this->timed_out_ = true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void SoftwareI2CBus::start_() {
|
||||
// Also generates a repeated start when SCL is low and SDA is released
|
||||
release_line(this->sda_pin_);
|
||||
// After a byte the device may still hold SDA low for its ACK. Without SDA being high before SCL rises there is
|
||||
// no falling edge, so the (repeated) start would not be seen. Wait until the device lets go.
|
||||
uint32_t start = micros();
|
||||
while (!this->sda_pin_->digital_read()) {
|
||||
if (micros() - start > this->timeout_) {
|
||||
this->timed_out_ = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
delayMicroseconds(this->half_period_us_);
|
||||
this->release_scl_();
|
||||
delayMicroseconds(this->half_period_us_);
|
||||
pull_line_low(this->sda_pin_);
|
||||
delayMicroseconds(this->half_period_us_);
|
||||
pull_line_low(this->scl_pin_);
|
||||
}
|
||||
|
||||
void SoftwareI2CBus::stop_() {
|
||||
if (this->timed_out_) {
|
||||
// SCL is stuck low, a STOP cannot be generated; just release both lines
|
||||
release_line(this->scl_pin_);
|
||||
release_line(this->sda_pin_);
|
||||
return;
|
||||
}
|
||||
pull_line_low(this->sda_pin_);
|
||||
delayMicroseconds(this->half_period_us_);
|
||||
this->release_scl_();
|
||||
delayMicroseconds(this->half_period_us_);
|
||||
release_line(this->sda_pin_);
|
||||
delayMicroseconds(this->half_period_us_);
|
||||
}
|
||||
|
||||
/// Shift out one byte, returns true if the device acknowledged it.
|
||||
bool SoftwareI2CBus::write_byte_(uint8_t data) {
|
||||
for (uint8_t mask = 0x80; mask != 0 && !this->timed_out_; mask >>= 1) {
|
||||
if (data & mask) {
|
||||
release_line(this->sda_pin_);
|
||||
} else {
|
||||
pull_line_low(this->sda_pin_);
|
||||
}
|
||||
delayMicroseconds(this->half_period_us_);
|
||||
this->release_scl_();
|
||||
delayMicroseconds(this->half_period_us_);
|
||||
pull_line_low(this->scl_pin_);
|
||||
}
|
||||
if (this->timed_out_)
|
||||
return false;
|
||||
release_line(this->sda_pin_);
|
||||
delayMicroseconds(this->half_period_us_);
|
||||
if (!this->release_scl_())
|
||||
return false;
|
||||
delayMicroseconds(this->half_period_us_);
|
||||
bool ack = !this->sda_pin_->digital_read();
|
||||
pull_line_low(this->scl_pin_);
|
||||
return ack;
|
||||
}
|
||||
|
||||
uint8_t SoftwareI2CBus::read_byte_(bool ack) {
|
||||
uint8_t data = 0;
|
||||
release_line(this->sda_pin_);
|
||||
for (uint8_t i = 0; i < 8 && !this->timed_out_; i++) {
|
||||
delayMicroseconds(this->half_period_us_);
|
||||
this->release_scl_();
|
||||
delayMicroseconds(this->half_period_us_);
|
||||
data = (data << 1) | (this->sda_pin_->digital_read() ? 1 : 0);
|
||||
pull_line_low(this->scl_pin_);
|
||||
}
|
||||
if (this->timed_out_)
|
||||
return data;
|
||||
if (ack) {
|
||||
pull_line_low(this->sda_pin_);
|
||||
}
|
||||
delayMicroseconds(this->half_period_us_);
|
||||
this->release_scl_();
|
||||
delayMicroseconds(this->half_period_us_);
|
||||
pull_line_low(this->scl_pin_);
|
||||
release_line(this->sda_pin_);
|
||||
return data;
|
||||
}
|
||||
|
||||
ErrorCode SoftwareI2CBus::write_readv(uint8_t address, const uint8_t *write_buffer, size_t write_count,
|
||||
uint8_t *read_buffer, size_t read_count) {
|
||||
if (!this->initialized_) {
|
||||
ESP_LOGD(TAG, "i2c bus not initialized!");
|
||||
return ERROR_NOT_INITIALIZED;
|
||||
}
|
||||
// After a failed recovery the bus stays blocked until a later attempt frees both lines
|
||||
if (this->bus_stuck_ && !this->recover_()) {
|
||||
return ERROR_TIMEOUT;
|
||||
}
|
||||
this->bus_stuck_ = false;
|
||||
|
||||
ESP_LOGV_HEX(TAG, write_buffer, write_count, I2C_MAX_LOG_BYTES, "0x%02X TX %s", address);
|
||||
|
||||
this->timed_out_ = false;
|
||||
ErrorCode err = ERROR_OK;
|
||||
this->start_();
|
||||
// An empty transfer only addresses the device (used by the bus scan)
|
||||
if (write_count != 0 || read_count == 0) {
|
||||
if (!this->write_byte_(address << 1)) {
|
||||
err = ERROR_NOT_ACKNOWLEDGED;
|
||||
}
|
||||
for (size_t i = 0; err == ERROR_OK && i < write_count; i++) {
|
||||
if (!this->write_byte_(write_buffer[i])) {
|
||||
ESP_LOGVV(TAG, "TX of byte %u to 0x%02X not acknowledged", (unsigned) i, address);
|
||||
err = ERROR_NOT_ACKNOWLEDGED;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (err == ERROR_OK && read_count != 0) {
|
||||
if (write_count != 0) {
|
||||
this->start_(); // repeated start
|
||||
}
|
||||
if (!this->write_byte_((address << 1) | 1)) {
|
||||
err = ERROR_NOT_ACKNOWLEDGED;
|
||||
}
|
||||
for (size_t i = 0; err == ERROR_OK && !this->timed_out_ && i < read_count; i++) {
|
||||
read_buffer[i] = this->read_byte_(i + 1 != read_count);
|
||||
}
|
||||
}
|
||||
this->stop_();
|
||||
|
||||
if (this->timed_out_) {
|
||||
ESP_LOGVV(TAG, "Bus line held low during transfer to 0x%02X for more than %" PRIu32 " us", address, this->timeout_);
|
||||
this->bus_stuck_ = !this->recover_();
|
||||
return ERROR_TIMEOUT;
|
||||
}
|
||||
return err;
|
||||
}
|
||||
|
||||
} // namespace esphome::i2c
|
||||
|
||||
#endif // USE_LIBRETINY
|
||||
@@ -0,0 +1,55 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_LIBRETINY
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/gpio.h"
|
||||
#include "i2c_bus.h"
|
||||
|
||||
namespace esphome::i2c {
|
||||
|
||||
/// Bit-banged I2C controller for LibreTiny chips without an Arduino Wire implementation (BK72xx).
|
||||
/// SDA and SCL can be any two GPIO pins; both lines are driven open-drain style (pulled low or released),
|
||||
/// so the bus needs pull-ups (the internal pull-up is enabled while released).
|
||||
class SoftwareI2CBus final : public I2CBus, public Component {
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
ErrorCode write_readv(uint8_t address, const uint8_t *write_buffer, size_t write_count, uint8_t *read_buffer,
|
||||
size_t read_count) override;
|
||||
float get_setup_priority() const override { return setup_priority::BUS; }
|
||||
|
||||
void set_scan(bool scan) { this->scan_ = scan; }
|
||||
void set_sda_pin(InternalGPIOPin *sda_pin) { this->sda_pin_ = sda_pin; }
|
||||
void set_scl_pin(InternalGPIOPin *scl_pin) { this->scl_pin_ = scl_pin; }
|
||||
void set_frequency(uint32_t frequency) { this->frequency_ = frequency; }
|
||||
#ifdef I2C_PORT_FREQUENCY_COUNT
|
||||
ErrorCode switch_frequency(uint32_t frequency) override;
|
||||
uint32_t get_frequency() const override { return this->frequency_; }
|
||||
#endif
|
||||
/// Maximum time in microseconds a device may hold SCL low (clock stretching).
|
||||
void set_timeout(uint32_t timeout) { this->timeout_ = timeout; }
|
||||
|
||||
protected:
|
||||
bool recover_();
|
||||
void update_half_period_();
|
||||
void start_();
|
||||
void stop_();
|
||||
bool release_scl_();
|
||||
bool write_byte_(uint8_t data);
|
||||
uint8_t read_byte_(bool ack);
|
||||
|
||||
InternalGPIOPin *sda_pin_{nullptr};
|
||||
InternalGPIOPin *scl_pin_{nullptr};
|
||||
uint32_t frequency_{100000};
|
||||
uint32_t timeout_{1000};
|
||||
uint32_t half_period_us_{5};
|
||||
bool timed_out_{false};
|
||||
bool recovered_{false};
|
||||
bool initialized_{false};
|
||||
bool bus_stuck_{false};
|
||||
};
|
||||
|
||||
} // namespace esphome::i2c
|
||||
|
||||
#endif // USE_LIBRETINY
|
||||
@@ -5,26 +5,26 @@ import esphome.config_validation as cv
|
||||
from esphome.const import CONF_FACTORY_RESET, CONF_ID, CONF_RESTART
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import CONF_LD2410_ID, LD2410Component, ld2410_ns
|
||||
|
||||
FactoryResetButton = ld2410_ns.class_("FactoryResetButton", button.Button)
|
||||
QueryButton = ld2410_ns.class_("QueryButton", button.Button)
|
||||
RestartButton = ld2410_ns.class_("RestartButton", button.Button)
|
||||
from .. import CONF_LD2410_ID, LD2410Component
|
||||
|
||||
CONFIG_SCHEMA = {
|
||||
cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase),
|
||||
cv.GenerateID(CONF_LD2410_ID): cv.use_id(LD2410Component),
|
||||
cv.Optional(CONF_FACTORY_RESET): button.factory_reset_button_schema(
|
||||
FactoryResetButton
|
||||
button.ApplyButton
|
||||
),
|
||||
cv.Optional(CONF_RESTART): button.restart_button_schema(RestartButton),
|
||||
cv.Optional(CONF_QUERY_PARAMS): button.query_button_schema(QueryButton),
|
||||
cv.Optional(CONF_RESTART): button.restart_button_schema(button.ApplyButton),
|
||||
cv.Optional(CONF_QUERY_PARAMS): button.query_button_schema(button.ApplyButton),
|
||||
}
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
hub = await cg.get_variable(config[CONF_LD2410_ID])
|
||||
buttons = button.sub_buttons(config, parent=hub)
|
||||
await buttons(CONF_FACTORY_RESET, hub.set_factory_reset_button)
|
||||
await buttons(CONF_RESTART, hub.set_restart_button)
|
||||
await buttons(CONF_QUERY_PARAMS, hub.set_query_button)
|
||||
await buttons(
|
||||
CONF_FACTORY_RESET, hub.set_factory_reset_button, apply="factory_reset()"
|
||||
)
|
||||
await buttons(
|
||||
CONF_RESTART, hub.set_restart_button, apply="restart_and_read_all_info()"
|
||||
)
|
||||
await buttons(CONF_QUERY_PARAMS, hub.set_query_button, apply="read_all_info()")
|
||||
@@ -1,7 +0,0 @@
|
||||
#include "factory_reset_button.h"
|
||||
|
||||
namespace esphome::ld2410 {
|
||||
|
||||
void FactoryResetButton::press_action() { this->parent_->factory_reset(); }
|
||||
|
||||
} // namespace esphome::ld2410
|
||||
@@ -1,16 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/button/button.h"
|
||||
#include "../ld2410.h"
|
||||
|
||||
namespace esphome::ld2410 {
|
||||
|
||||
class FactoryResetButton final : public button::Button, public Parented<LD2410Component> {
|
||||
public:
|
||||
FactoryResetButton() = default;
|
||||
|
||||
protected:
|
||||
void press_action() override;
|
||||
};
|
||||
|
||||
} // namespace esphome::ld2410
|
||||
@@ -1,7 +0,0 @@
|
||||
#include "query_button.h"
|
||||
|
||||
namespace esphome::ld2410 {
|
||||
|
||||
void QueryButton::press_action() { this->parent_->read_all_info(); }
|
||||
|
||||
} // namespace esphome::ld2410
|
||||
@@ -1,16 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/button/button.h"
|
||||
#include "../ld2410.h"
|
||||
|
||||
namespace esphome::ld2410 {
|
||||
|
||||
class QueryButton final : public button::Button, public Parented<LD2410Component> {
|
||||
public:
|
||||
QueryButton() = default;
|
||||
|
||||
protected:
|
||||
void press_action() override;
|
||||
};
|
||||
|
||||
} // namespace esphome::ld2410
|
||||
@@ -1,7 +0,0 @@
|
||||
#include "restart_button.h"
|
||||
|
||||
namespace esphome::ld2410 {
|
||||
|
||||
void RestartButton::press_action() { this->parent_->restart_and_read_all_info(); }
|
||||
|
||||
} // namespace esphome::ld2410
|
||||
@@ -1,16 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/button/button.h"
|
||||
#include "../ld2410.h"
|
||||
|
||||
namespace esphome::ld2410 {
|
||||
|
||||
class RestartButton final : public button::Button, public Parented<LD2410Component> {
|
||||
public:
|
||||
RestartButton() = default;
|
||||
|
||||
protected:
|
||||
void press_action() override;
|
||||
};
|
||||
|
||||
} // namespace esphome::ld2410
|
||||
@@ -9,27 +9,12 @@
|
||||
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
namespace esphome::ld2410 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "ld2410");
|
||||
|
||||
enum BaudRate : 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,
|
||||
};
|
||||
|
||||
enum DistanceResolution : uint8_t {
|
||||
DISTANCE_RESOLUTION_0_2 = 0x01,
|
||||
DISTANCE_RESOLUTION_0_75 = 0x00,
|
||||
};
|
||||
|
||||
enum LightFunction : uint8_t {
|
||||
LIGHT_FUNCTION_OFF = 0x00,
|
||||
LIGHT_FUNCTION_BELOW = 0x01,
|
||||
@@ -69,73 +54,17 @@ enum AckData : uint8_t {
|
||||
COMMAND_STATUS = 7,
|
||||
};
|
||||
|
||||
// Memory-efficient lookup tables
|
||||
struct StringToUint8 {
|
||||
const char *str;
|
||||
const uint8_t value;
|
||||
};
|
||||
// Index + 1 is the BaudRate value; the select options use the same order
|
||||
PROGMEM_STRING_TABLE(BaudRateStrings, "9600", "19200", "38400", "57600", "115200", "230400", "256000", "460800");
|
||||
static_assert(BAUD_RATE_9600 == 1 && BAUD_RATE_460800 == BaudRateStrings::COUNT, "BaudRate is index + 1");
|
||||
// Same order as DISTANCE_RESOLUTIONS
|
||||
PROGMEM_STRING_TABLE(DistanceResolutionStrings, "0.2m", "0.75m");
|
||||
// The light function and out pin level select options are in enum order
|
||||
static_assert(LIGHT_FUNCTION_OFF == 0 && LIGHT_FUNCTION_BELOW == 1 && LIGHT_FUNCTION_ABOVE == 2,
|
||||
"LightFunction is the select option index");
|
||||
static_assert(OUT_PIN_LEVEL_LOW == 0 && OUT_PIN_LEVEL_HIGH == 1, "OutPinLevel is the select option index");
|
||||
|
||||
struct Uint8ToString {
|
||||
const uint8_t value;
|
||||
const char *str;
|
||||
};
|
||||
|
||||
constexpr StringToUint8 BAUD_RATES_BY_STR[] = {
|
||||
{"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},
|
||||
};
|
||||
|
||||
constexpr StringToUint8 DISTANCE_RESOLUTIONS_BY_STR[] = {
|
||||
{"0.2m", DISTANCE_RESOLUTION_0_2},
|
||||
{"0.75m", DISTANCE_RESOLUTION_0_75},
|
||||
};
|
||||
|
||||
constexpr Uint8ToString DISTANCE_RESOLUTIONS_BY_UINT[] = {
|
||||
{DISTANCE_RESOLUTION_0_2, "0.2m"},
|
||||
{DISTANCE_RESOLUTION_0_75, "0.75m"},
|
||||
};
|
||||
|
||||
constexpr StringToUint8 LIGHT_FUNCTIONS_BY_STR[] = {
|
||||
{"off", LIGHT_FUNCTION_OFF},
|
||||
{"below", LIGHT_FUNCTION_BELOW},
|
||||
{"above", LIGHT_FUNCTION_ABOVE},
|
||||
};
|
||||
|
||||
constexpr Uint8ToString LIGHT_FUNCTIONS_BY_UINT[] = {
|
||||
{LIGHT_FUNCTION_OFF, "off"},
|
||||
{LIGHT_FUNCTION_BELOW, "below"},
|
||||
{LIGHT_FUNCTION_ABOVE, "above"},
|
||||
};
|
||||
|
||||
constexpr StringToUint8 OUT_PIN_LEVELS_BY_STR[] = {
|
||||
{"low", OUT_PIN_LEVEL_LOW},
|
||||
{"high", OUT_PIN_LEVEL_HIGH},
|
||||
};
|
||||
|
||||
constexpr Uint8ToString OUT_PIN_LEVELS_BY_UINT[] = {
|
||||
{OUT_PIN_LEVEL_LOW, "low"},
|
||||
{OUT_PIN_LEVEL_HIGH, "high"},
|
||||
};
|
||||
|
||||
constexpr uint32_t BAUD_RATES[] = {9600, 19200, 38400, 57600, 115200, 230400, 256000, 460800};
|
||||
|
||||
// Helper functions for lookups
|
||||
template<size_t N> uint8_t find_uint8(const StringToUint8 (&arr)[N], const char *str) {
|
||||
for (const auto &entry : arr) {
|
||||
if (strcmp(str, entry.str) == 0)
|
||||
return entry.value;
|
||||
}
|
||||
return 0xFF; // Not found
|
||||
}
|
||||
|
||||
template<size_t N> const char *find_str(const Uint8ToString (&arr)[N], uint8_t value) {
|
||||
for (const auto &entry : arr) {
|
||||
if (value == entry.value)
|
||||
return entry.str;
|
||||
}
|
||||
return ""; // Not found
|
||||
}
|
||||
static constexpr uint32_t BAUD_RATES[] PROGMEM = {9600, 19200, 38400, 57600, 115200, 230400, 256000, 460800};
|
||||
|
||||
// Commands
|
||||
static constexpr uint8_t CMD_ENABLE_CONF = 0xFF;
|
||||
@@ -272,20 +201,7 @@ void LD2410Component::restart_and_read_all_info() {
|
||||
}
|
||||
|
||||
void LD2410Component::loop() {
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
size_t avail = this->available();
|
||||
uint8_t buf[MAX_LINE_LENGTH];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(avail, sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
avail -= to_read;
|
||||
|
||||
for (size_t i = 0; i < to_read; i++) {
|
||||
this->readline_(buf[i]);
|
||||
}
|
||||
}
|
||||
this->read_available<MAX_LINE_LENGTH>([this](uint8_t b) { this->readline_(b); });
|
||||
}
|
||||
|
||||
void LD2410Component::send_command_(uint8_t command, const uint8_t *command_value, uint8_t command_value_len) {
|
||||
@@ -465,11 +381,16 @@ void LD2410Component::handle_ack_data_() {
|
||||
if (this->buffer_pos_ < 11 + HEADER_FOOTER_SIZE) {
|
||||
break; // too short to hold the resolution
|
||||
}
|
||||
const auto *distance_resolution = find_str(DISTANCE_RESOLUTIONS_BY_UINT, this->buffer_data_[10]);
|
||||
ESP_LOGV(TAG, "Distance resolution: %s", distance_resolution);
|
||||
const auto index = ld24xx::find_index(DISTANCE_RESOLUTIONS, this->buffer_data_[10]);
|
||||
if (!index) {
|
||||
ESP_LOGW(TAG, "Unknown distance resolution %u", this->buffer_data_[10]);
|
||||
break;
|
||||
}
|
||||
ESP_LOGV(TAG, "Distance resolution: %s",
|
||||
LOG_STR_ARG(DistanceResolutionStrings::get_log_str(*index, DistanceResolutionStrings::LAST_INDEX)));
|
||||
#ifdef USE_SELECT
|
||||
if (this->distance_resolution_select_ != nullptr) {
|
||||
this->distance_resolution_select_->publish_state(distance_resolution);
|
||||
this->distance_resolution_select_->publish_state(*index);
|
||||
}
|
||||
#endif
|
||||
break;
|
||||
@@ -482,16 +403,14 @@ void LD2410Component::handle_ack_data_() {
|
||||
this->light_function_ = this->buffer_data_[10];
|
||||
this->light_threshold_ = this->buffer_data_[11];
|
||||
this->out_pin_level_ = this->buffer_data_[12];
|
||||
const auto *light_function_str = find_str(LIGHT_FUNCTIONS_BY_UINT, this->light_function_);
|
||||
const auto *out_pin_level_str = find_str(OUT_PIN_LEVELS_BY_UINT, this->out_pin_level_);
|
||||
ESP_LOGV(TAG, "Light function: %s, threshold: %u, out pin level: %s", light_function_str, this->light_threshold_,
|
||||
out_pin_level_str);
|
||||
ESP_LOGV(TAG, "Light function: %u, threshold: %u, out pin level: %u", this->light_function_,
|
||||
this->light_threshold_, this->out_pin_level_);
|
||||
#ifdef USE_SELECT
|
||||
if (this->light_function_select_ != nullptr) {
|
||||
this->light_function_select_->publish_state(light_function_str);
|
||||
this->light_function_select_->publish_state(static_cast<size_t>(this->light_function_));
|
||||
}
|
||||
if (this->out_pin_level_select_ != nullptr) {
|
||||
this->out_pin_level_select_->publish_state(out_pin_level_str);
|
||||
this->out_pin_level_select_->publish_state(static_cast<size_t>(this->out_pin_level_));
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_NUMBER
|
||||
@@ -628,15 +547,29 @@ void LD2410Component::set_bluetooth(bool enable) {
|
||||
}
|
||||
|
||||
void LD2410Component::set_distance_resolution(const char *state) {
|
||||
const int16_t index = progmem_string_table_find<DistanceResolutionStrings>(state, strlen(state));
|
||||
if (index >= 0) {
|
||||
this->set_distance_resolution(static_cast<DistanceResolution>(progmem_read_byte(&DISTANCE_RESOLUTIONS[index])));
|
||||
}
|
||||
}
|
||||
|
||||
void LD2410Component::set_distance_resolution(DistanceResolution resolution) {
|
||||
this->set_config_mode_(true);
|
||||
const uint8_t cmd_value[2] = {find_uint8(DISTANCE_RESOLUTIONS_BY_STR, state), 0x00};
|
||||
const uint8_t cmd_value[2] = {resolution, 0x00};
|
||||
this->send_command_(CMD_SET_DISTANCE_RESOLUTION, cmd_value, sizeof(cmd_value));
|
||||
this->set_timeout(200, [this]() { this->restart_and_read_all_info(); });
|
||||
}
|
||||
|
||||
void LD2410Component::set_baud_rate(const char *state) {
|
||||
const int16_t index = progmem_string_table_find<BaudRateStrings>(state, strlen(state));
|
||||
if (index >= 0) {
|
||||
this->set_baud_rate(static_cast<BaudRate>(index + 1));
|
||||
}
|
||||
}
|
||||
|
||||
void LD2410Component::set_baud_rate(BaudRate rate) {
|
||||
this->set_config_mode_(true);
|
||||
const uint8_t cmd_value[2] = {find_uint8(BAUD_RATES_BY_STR, state), 0x00};
|
||||
const uint8_t cmd_value[2] = {rate, 0x00};
|
||||
this->send_command_(CMD_SET_BAUD_RATE, cmd_value, sizeof(cmd_value));
|
||||
this->set_timeout(200, [this]() { this->restart_(); });
|
||||
}
|
||||
@@ -715,7 +648,16 @@ void LD2410Component::set_max_distances_timeout() {
|
||||
this->set_config_mode_(false);
|
||||
}
|
||||
|
||||
void LD2410Component::set_gate_threshold(uint8_t gate) {
|
||||
void LD2410Component::set_gate_threshold(number::Number *n) {
|
||||
// Found by pointer: up to 18 compares per write beat storing the gate in each number
|
||||
uint8_t gate = 0;
|
||||
while (gate < TOTAL_GATES && this->gate_move_threshold_numbers_[gate] != n &&
|
||||
this->gate_still_threshold_numbers_[gate] != n)
|
||||
gate++;
|
||||
if (gate == TOTAL_GATES) {
|
||||
ESP_LOGW(TAG, "Threshold number is not a known gate");
|
||||
return;
|
||||
}
|
||||
number::Number *motionsens = this->gate_move_threshold_numbers_[gate];
|
||||
number::Number *stillsens = this->gate_still_threshold_numbers_[gate];
|
||||
|
||||
@@ -759,11 +701,15 @@ void LD2410Component::set_light_out_control() {
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
if (this->light_function_select_ != nullptr && this->light_function_select_->has_state()) {
|
||||
this->light_function_ = find_uint8(LIGHT_FUNCTIONS_BY_STR, this->light_function_select_->current_option().c_str());
|
||||
if (this->light_function_select_ != nullptr) {
|
||||
if (const auto index = this->light_function_select_->active_index()) {
|
||||
this->light_function_ = *index;
|
||||
}
|
||||
}
|
||||
if (this->out_pin_level_select_ != nullptr && this->out_pin_level_select_->has_state()) {
|
||||
this->out_pin_level_ = find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_select_->current_option().c_str());
|
||||
if (this->out_pin_level_select_ != nullptr) {
|
||||
if (const auto index = this->out_pin_level_select_->active_index()) {
|
||||
this->out_pin_level_ = *index;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
this->set_config_mode_(true);
|
||||
|
||||
@@ -37,6 +37,25 @@ using namespace ld24xx;
|
||||
static constexpr uint8_t MAX_LINE_LENGTH = 50;
|
||||
static constexpr uint8_t TOTAL_GATES = 9; // Total number of gates supported by the LD2410
|
||||
|
||||
enum BaudRate : 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,
|
||||
};
|
||||
|
||||
enum DistanceResolution : uint8_t {
|
||||
DISTANCE_RESOLUTION_0_2 = 0x01,
|
||||
DISTANCE_RESOLUTION_0_75 = 0x00,
|
||||
};
|
||||
|
||||
// Device values of the distance resolution select options, in option order; in flash on ESP8266
|
||||
static constexpr uint8_t DISTANCE_RESOLUTIONS[] PROGMEM = {DISTANCE_RESOLUTION_0_2, DISTANCE_RESOLUTION_0_75};
|
||||
|
||||
class LD2410Component final : public Component, public uart::UARTDevice {
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
SUB_BINARY_SENSOR(out_pin_presence_status)
|
||||
@@ -87,7 +106,7 @@ class LD2410Component final : public Component, public uart::UARTDevice {
|
||||
void set_gate_still_threshold_number(uint8_t gate, number::Number *n);
|
||||
void set_gate_move_threshold_number(uint8_t gate, number::Number *n);
|
||||
void set_max_distances_timeout();
|
||||
void set_gate_threshold(uint8_t gate);
|
||||
void set_gate_threshold(number::Number *n);
|
||||
#endif
|
||||
#ifdef USE_SENSOR
|
||||
void set_gate_move_sensor(uint8_t gate, sensor::Sensor *s);
|
||||
@@ -99,7 +118,9 @@ class LD2410Component final : public Component, public uart::UARTDevice {
|
||||
void restart_and_read_all_info();
|
||||
void set_bluetooth(bool enable);
|
||||
void set_distance_resolution(const char *state);
|
||||
void set_distance_resolution(DistanceResolution resolution);
|
||||
void set_baud_rate(const char *state);
|
||||
void set_baud_rate(BaudRate rate);
|
||||
void factory_reset();
|
||||
|
||||
protected:
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from functools import partial
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import number
|
||||
import esphome.config_validation as cv
|
||||
@@ -18,11 +20,7 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import CONF_LD2410_ID, LD2410Component, ld2410_ns
|
||||
|
||||
GateThresholdNumber = ld2410_ns.class_("GateThresholdNumber", number.Number)
|
||||
LightThresholdNumber = ld2410_ns.class_("LightThresholdNumber", number.Number)
|
||||
MaxDistanceTimeoutNumber = ld2410_ns.class_("MaxDistanceTimeoutNumber", number.Number)
|
||||
from .. import CONF_LD2410_ID, LD2410Component
|
||||
|
||||
CONF_MAX_MOVE_DISTANCE_GATE = "max_move_distance_gate"
|
||||
CONF_MAX_STILL_DISTANCE_GATE = "max_still_distance_gate"
|
||||
@@ -35,25 +33,25 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase),
|
||||
cv.GenerateID(CONF_LD2410_ID): cv.use_id(LD2410Component),
|
||||
cv.Inclusive(CONF_TIMEOUT, TIMEOUT_GROUP): number.number_schema(
|
||||
MaxDistanceTimeoutNumber,
|
||||
number.ApplyNumber,
|
||||
unit_of_measurement=UNIT_SECOND,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
icon=ICON_TIMELAPSE,
|
||||
),
|
||||
cv.Inclusive(CONF_MAX_MOVE_DISTANCE_GATE, TIMEOUT_GROUP): number.number_schema(
|
||||
MaxDistanceTimeoutNumber,
|
||||
number.ApplyNumber,
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
icon=ICON_MOTION_SENSOR,
|
||||
),
|
||||
cv.Inclusive(CONF_MAX_STILL_DISTANCE_GATE, TIMEOUT_GROUP): number.number_schema(
|
||||
MaxDistanceTimeoutNumber,
|
||||
number.ApplyNumber,
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
icon=ICON_MOTION_SENSOR,
|
||||
),
|
||||
cv.Optional(CONF_LIGHT_THRESHOLD): number.number_schema(
|
||||
LightThresholdNumber,
|
||||
number.ApplyNumber,
|
||||
device_class=DEVICE_CLASS_ILLUMINANCE,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
icon=ICON_LIGHTBULB,
|
||||
@@ -66,14 +64,14 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
|
||||
cv.Optional(f"g{x}"): cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_MOVE_THRESHOLD): number.number_schema(
|
||||
GateThresholdNumber,
|
||||
number.ApplyNumber,
|
||||
device_class=DEVICE_CLASS_SIGNAL_STRENGTH,
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
icon=ICON_MOTION_SENSOR,
|
||||
),
|
||||
cv.Required(CONF_STILL_THRESHOLD): number.number_schema(
|
||||
GateThresholdNumber,
|
||||
number.ApplyNumber,
|
||||
device_class=DEVICE_CLASS_SIGNAL_STRENGTH,
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
@@ -89,8 +87,14 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
hub = await cg.get_variable(config[CONF_LD2410_ID])
|
||||
numbers = number.sub_numbers(config, parent=hub)
|
||||
timeouts = {"apply": "set_max_distances_timeout()", "publish": "before"}
|
||||
await numbers(
|
||||
CONF_TIMEOUT, hub.set_timeout_number, min_value=0, max_value=65535, step=1
|
||||
CONF_TIMEOUT,
|
||||
hub.set_timeout_number,
|
||||
min_value=0,
|
||||
max_value=65535,
|
||||
step=1,
|
||||
**timeouts,
|
||||
)
|
||||
await numbers(
|
||||
CONF_MAX_MOVE_DISTANCE_GATE,
|
||||
@@ -98,6 +102,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
min_value=2,
|
||||
max_value=8,
|
||||
step=1,
|
||||
**timeouts,
|
||||
)
|
||||
await numbers(
|
||||
CONF_MAX_STILL_DISTANCE_GATE,
|
||||
@@ -105,6 +110,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
min_value=2,
|
||||
max_value=8,
|
||||
step=1,
|
||||
**timeouts,
|
||||
)
|
||||
await numbers(
|
||||
CONF_LIGHT_THRESHOLD,
|
||||
@@ -112,21 +118,25 @@ async def to_code(config: ConfigType) -> None:
|
||||
min_value=0,
|
||||
max_value=255,
|
||||
step=1,
|
||||
apply="set_light_out_control()",
|
||||
publish="before",
|
||||
)
|
||||
threshold = {
|
||||
"min_value": 0,
|
||||
"max_value": 100,
|
||||
"step": 1,
|
||||
"apply": "set_gate_threshold(entity)",
|
||||
"publish": "before",
|
||||
}
|
||||
for x in range(9):
|
||||
if gate_conf := config.get(f"g{x}"):
|
||||
move_config = gate_conf[CONF_MOVE_THRESHOLD]
|
||||
n = cg.new_Pvariable(move_config[CONF_ID], x)
|
||||
await number.register_number(
|
||||
n, move_config, min_value=0, max_value=100, step=1
|
||||
await numbers.create(
|
||||
gate_conf[CONF_MOVE_THRESHOLD],
|
||||
partial(hub.set_gate_move_threshold_number, x),
|
||||
**threshold,
|
||||
)
|
||||
await cg.register_parented(n, hub)
|
||||
cg.add(hub.set_gate_move_threshold_number(x, n))
|
||||
|
||||
still_config = gate_conf[CONF_STILL_THRESHOLD]
|
||||
n = cg.new_Pvariable(still_config[CONF_ID], x)
|
||||
await number.register_number(
|
||||
n, still_config, min_value=0, max_value=100, step=1
|
||||
await numbers.create(
|
||||
gate_conf[CONF_STILL_THRESHOLD],
|
||||
partial(hub.set_gate_still_threshold_number, x),
|
||||
**threshold,
|
||||
)
|
||||
await cg.register_parented(n, hub)
|
||||
cg.add(hub.set_gate_still_threshold_number(x, n))
|
||||
@@ -1,12 +0,0 @@
|
||||
#include "gate_threshold_number.h"
|
||||
|
||||
namespace esphome::ld2410 {
|
||||
|
||||
GateThresholdNumber::GateThresholdNumber(uint8_t gate) : gate_(gate) {}
|
||||
|
||||
void GateThresholdNumber::control(float value) {
|
||||
this->publish_state(value);
|
||||
this->parent_->set_gate_threshold(this->gate_);
|
||||
}
|
||||
|
||||
} // namespace esphome::ld2410
|
||||
@@ -1,17 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/number/number.h"
|
||||
#include "../ld2410.h"
|
||||
|
||||
namespace esphome::ld2410 {
|
||||
|
||||
class GateThresholdNumber final : public number::Number, public Parented<LD2410Component> {
|
||||
public:
|
||||
GateThresholdNumber(uint8_t gate);
|
||||
|
||||
protected:
|
||||
uint8_t gate_;
|
||||
void control(float value) override;
|
||||
};
|
||||
|
||||
} // namespace esphome::ld2410
|
||||
@@ -1,10 +0,0 @@
|
||||
#include "light_threshold_number.h"
|
||||
|
||||
namespace esphome::ld2410 {
|
||||
|
||||
void LightThresholdNumber::control(float value) {
|
||||
this->publish_state(value);
|
||||
this->parent_->set_light_out_control();
|
||||
}
|
||||
|
||||
} // namespace esphome::ld2410
|
||||
@@ -1,16 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/number/number.h"
|
||||
#include "../ld2410.h"
|
||||
|
||||
namespace esphome::ld2410 {
|
||||
|
||||
class LightThresholdNumber final : public number::Number, public Parented<LD2410Component> {
|
||||
public:
|
||||
LightThresholdNumber() = default;
|
||||
|
||||
protected:
|
||||
void control(float value) override;
|
||||
};
|
||||
|
||||
} // namespace esphome::ld2410
|
||||
@@ -1,10 +0,0 @@
|
||||
#include "max_distance_timeout_number.h"
|
||||
|
||||
namespace esphome::ld2410 {
|
||||
|
||||
void MaxDistanceTimeoutNumber::control(float value) {
|
||||
this->publish_state(value);
|
||||
this->parent_->set_max_distances_timeout();
|
||||
}
|
||||
|
||||
} // namespace esphome::ld2410
|
||||
@@ -1,16 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/number/number.h"
|
||||
#include "../ld2410.h"
|
||||
|
||||
namespace esphome::ld2410 {
|
||||
|
||||
class MaxDistanceTimeoutNumber final : public number::Number, public Parented<LD2410Component> {
|
||||
public:
|
||||
MaxDistanceTimeoutNumber() = default;
|
||||
|
||||
protected:
|
||||
void control(float value) override;
|
||||
};
|
||||
|
||||
} // namespace esphome::ld2410
|
||||
@@ -52,6 +52,7 @@ CONFIG_SCHEMA = {
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
hub = await cg.get_variable(config[CONF_LD2410_ID])
|
||||
selects = select.sub_selects(config, parent=hub)
|
||||
# Option lists follow the order of the C++ enums and tables; the hub maps by index
|
||||
await selects(
|
||||
CONF_DISTANCE_RESOLUTION,
|
||||
hub.set_distance_resolution_select,
|
||||
|
||||
@@ -4,7 +4,7 @@ namespace esphome::ld2410 {
|
||||
|
||||
void BaudRateSelect::control(size_t index) {
|
||||
this->publish_state(index);
|
||||
this->parent_->set_baud_rate(this->option_at(index));
|
||||
this->parent_->set_baud_rate(static_cast<BaudRate>(index + 1));
|
||||
}
|
||||
|
||||
} // namespace esphome::ld2410
|
||||
@@ -4,7 +4,8 @@ namespace esphome::ld2410 {
|
||||
|
||||
void DistanceResolutionSelect::control(size_t index) {
|
||||
this->publish_state(index);
|
||||
this->parent_->set_distance_resolution(this->option_at(index));
|
||||
this->parent_->set_distance_resolution(
|
||||
static_cast<DistanceResolution>(progmem_read_byte(&DISTANCE_RESOLUTIONS[index])));
|
||||
}
|
||||
|
||||
} // namespace esphome::ld2410
|
||||
@@ -11,10 +11,7 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import CONF_LD2410_ID, LD2410Component, ld2410_ns
|
||||
|
||||
BluetoothSwitch = ld2410_ns.class_("BluetoothSwitch", switch.Switch)
|
||||
EngineeringModeSwitch = ld2410_ns.class_("EngineeringModeSwitch", switch.Switch)
|
||||
from .. import CONF_LD2410_ID, LD2410Component
|
||||
|
||||
CONF_ENGINEERING_MODE = "engineering_mode"
|
||||
|
||||
@@ -22,13 +19,13 @@ CONFIG_SCHEMA = {
|
||||
cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase),
|
||||
cv.GenerateID(CONF_LD2410_ID): cv.use_id(LD2410Component),
|
||||
cv.Optional(CONF_ENGINEERING_MODE): switch.switch_schema(
|
||||
EngineeringModeSwitch,
|
||||
switch.ApplySwitch,
|
||||
device_class=DEVICE_CLASS_SWITCH,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
icon=ICON_PULSE,
|
||||
),
|
||||
cv.Optional(CONF_BLUETOOTH): switch.switch_schema(
|
||||
BluetoothSwitch,
|
||||
switch.ApplySwitch,
|
||||
device_class=DEVICE_CLASS_SWITCH,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
icon=ICON_BLUETOOTH,
|
||||
@@ -39,5 +36,15 @@ CONFIG_SCHEMA = {
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
hub = await cg.get_variable(config[CONF_LD2410_ID])
|
||||
switches = switch.sub_switches(config, parent=hub)
|
||||
await switches(CONF_ENGINEERING_MODE, hub.set_engineering_mode_switch)
|
||||
await switches(CONF_BLUETOOTH, hub.set_bluetooth_switch)
|
||||
await switches(
|
||||
CONF_ENGINEERING_MODE,
|
||||
hub.set_engineering_mode_switch,
|
||||
apply="set_engineering_mode(state)",
|
||||
publish="before",
|
||||
)
|
||||
await switches(
|
||||
CONF_BLUETOOTH,
|
||||
hub.set_bluetooth_switch,
|
||||
apply="set_bluetooth(state)",
|
||||
publish="before",
|
||||
)
|
||||
@@ -1,10 +0,0 @@
|
||||
#include "bluetooth_switch.h"
|
||||
|
||||
namespace esphome::ld2410 {
|
||||
|
||||
void BluetoothSwitch::write_state(bool state) {
|
||||
this->publish_state(state);
|
||||
this->parent_->set_bluetooth(state);
|
||||
}
|
||||
|
||||
} // namespace esphome::ld2410
|
||||
@@ -1,16 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/switch/switch.h"
|
||||
#include "../ld2410.h"
|
||||
|
||||
namespace esphome::ld2410 {
|
||||
|
||||
class BluetoothSwitch final : public switch_::Switch, public Parented<LD2410Component> {
|
||||
public:
|
||||
BluetoothSwitch() = default;
|
||||
|
||||
protected:
|
||||
void write_state(bool state) override;
|
||||
};
|
||||
|
||||
} // namespace esphome::ld2410
|
||||
@@ -1,10 +0,0 @@
|
||||
#include "engineering_mode_switch.h"
|
||||
|
||||
namespace esphome::ld2410 {
|
||||
|
||||
void EngineeringModeSwitch::write_state(bool state) {
|
||||
this->publish_state(state);
|
||||
this->parent_->set_engineering_mode(state);
|
||||
}
|
||||
|
||||
} // namespace esphome::ld2410
|
||||
@@ -1,16 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/switch/switch.h"
|
||||
#include "../ld2410.h"
|
||||
|
||||
namespace esphome::ld2410 {
|
||||
|
||||
class EngineeringModeSwitch final : public switch_::Switch, public Parented<LD2410Component> {
|
||||
public:
|
||||
EngineeringModeSwitch() = default;
|
||||
|
||||
protected:
|
||||
void write_state(bool state) override;
|
||||
};
|
||||
|
||||
} // namespace esphome::ld2410
|
||||
@@ -1,6 +1,5 @@
|
||||
#include "ld2410s.h"
|
||||
#include "ld2410s_number.h"
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/hal.h"
|
||||
@@ -95,19 +94,8 @@ void LD2410SComponent::dump_config() {
|
||||
}
|
||||
|
||||
void LD2410SComponent::loop() {
|
||||
uint8_t chunk[16];
|
||||
// Take the count once so one pass handles a bounded number of bytes
|
||||
size_t avail = this->available();
|
||||
while (avail > 0) {
|
||||
const size_t len = std::min(avail, sizeof(chunk));
|
||||
if (!this->read_array(chunk, len)) {
|
||||
ESP_LOGV(TAG, "UART read of %u bytes failed", static_cast<unsigned>(len));
|
||||
break;
|
||||
}
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
this->receive_byte_(chunk[i]);
|
||||
}
|
||||
avail -= len;
|
||||
if (!this->read_available<16>([this](uint8_t b) { this->receive_byte_(b); })) {
|
||||
ESP_LOGV(TAG, "UART read failed");
|
||||
}
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
if (this->sequence_ == nullptr) {
|
||||
|
||||
@@ -11,26 +11,20 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import CONF_LD2412_ID, LD2412_ns, LD2412Component
|
||||
from .. import CONF_LD2412_ID, LD2412Component
|
||||
|
||||
FactoryResetButton = LD2412_ns.class_("FactoryResetButton", button.Button)
|
||||
QueryButton = LD2412_ns.class_("QueryButton", button.Button)
|
||||
RestartButton = LD2412_ns.class_("RestartButton", button.Button)
|
||||
StartDynamicBackgroundCorrectionButton = LD2412_ns.class_(
|
||||
"StartDynamicBackgroundCorrectionButton", button.Button
|
||||
)
|
||||
CONF_START_DYNAMIC_BACKGROUND_CORRECTION = "start_dynamic_background_correction"
|
||||
|
||||
CONFIG_SCHEMA = {
|
||||
cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase),
|
||||
cv.GenerateID(CONF_LD2412_ID): cv.use_id(LD2412Component),
|
||||
cv.Optional(CONF_FACTORY_RESET): button.factory_reset_button_schema(
|
||||
FactoryResetButton
|
||||
button.ApplyButton
|
||||
),
|
||||
cv.Optional(CONF_QUERY_PARAMS): button.query_button_schema(QueryButton),
|
||||
cv.Optional(CONF_RESTART): button.restart_button_schema(RestartButton),
|
||||
cv.Optional(CONF_QUERY_PARAMS): button.query_button_schema(button.ApplyButton),
|
||||
cv.Optional(CONF_RESTART): button.restart_button_schema(button.ApplyButton),
|
||||
cv.Optional(CONF_START_DYNAMIC_BACKGROUND_CORRECTION): button.button_schema(
|
||||
StartDynamicBackgroundCorrectionButton,
|
||||
button.ApplyButton,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
icon=ICON_PULSE,
|
||||
),
|
||||
@@ -40,10 +34,15 @@ CONFIG_SCHEMA = {
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
hub = await cg.get_variable(config[CONF_LD2412_ID])
|
||||
buttons = button.sub_buttons(config, parent=hub)
|
||||
await buttons(CONF_FACTORY_RESET, hub.set_factory_reset_button)
|
||||
await buttons(CONF_QUERY_PARAMS, hub.set_query_button)
|
||||
await buttons(CONF_RESTART, hub.set_restart_button)
|
||||
await buttons(
|
||||
CONF_FACTORY_RESET, hub.set_factory_reset_button, apply="factory_reset()"
|
||||
)
|
||||
await buttons(CONF_QUERY_PARAMS, hub.set_query_button, apply="read_all_info()")
|
||||
await buttons(
|
||||
CONF_RESTART, hub.set_restart_button, apply="restart_and_read_all_info()"
|
||||
)
|
||||
await buttons(
|
||||
CONF_START_DYNAMIC_BACKGROUND_CORRECTION,
|
||||
hub.set_start_dynamic_background_correction_button,
|
||||
apply="start_dynamic_background_correction()",
|
||||
)
|
||||
@@ -1,7 +0,0 @@
|
||||
#include "factory_reset_button.h"
|
||||
|
||||
namespace esphome::ld2412 {
|
||||
|
||||
void FactoryResetButton::press_action() { this->parent_->factory_reset(); }
|
||||
|
||||
} // namespace esphome::ld2412
|
||||
@@ -1,16 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/button/button.h"
|
||||
#include "../ld2412.h"
|
||||
|
||||
namespace esphome::ld2412 {
|
||||
|
||||
class FactoryResetButton final : public button::Button, public Parented<LD2412Component> {
|
||||
public:
|
||||
FactoryResetButton() = default;
|
||||
|
||||
protected:
|
||||
void press_action() override;
|
||||
};
|
||||
|
||||
} // namespace esphome::ld2412
|
||||
@@ -1,7 +0,0 @@
|
||||
#include "query_button.h"
|
||||
|
||||
namespace esphome::ld2412 {
|
||||
|
||||
void QueryButton::press_action() { this->parent_->read_all_info(); }
|
||||
|
||||
} // namespace esphome::ld2412
|
||||
@@ -1,16 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/button/button.h"
|
||||
#include "../ld2412.h"
|
||||
|
||||
namespace esphome::ld2412 {
|
||||
|
||||
class QueryButton final : public button::Button, public Parented<LD2412Component> {
|
||||
public:
|
||||
QueryButton() = default;
|
||||
|
||||
protected:
|
||||
void press_action() override;
|
||||
};
|
||||
|
||||
} // namespace esphome::ld2412
|
||||
@@ -1,7 +0,0 @@
|
||||
#include "restart_button.h"
|
||||
|
||||
namespace esphome::ld2412 {
|
||||
|
||||
void RestartButton::press_action() { this->parent_->restart_and_read_all_info(); }
|
||||
|
||||
} // namespace esphome::ld2412
|
||||
@@ -1,16 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/button/button.h"
|
||||
#include "../ld2412.h"
|
||||
|
||||
namespace esphome::ld2412 {
|
||||
|
||||
class RestartButton final : public button::Button, public Parented<LD2412Component> {
|
||||
public:
|
||||
RestartButton() = default;
|
||||
|
||||
protected:
|
||||
void press_action() override;
|
||||
};
|
||||
|
||||
} // namespace esphome::ld2412
|
||||
@@ -1,9 +0,0 @@
|
||||
#include "start_dynamic_background_correction_button.h"
|
||||
|
||||
#include "restart_button.h"
|
||||
|
||||
namespace esphome::ld2412 {
|
||||
|
||||
void StartDynamicBackgroundCorrectionButton::press_action() { this->parent_->start_dynamic_background_correction(); }
|
||||
|
||||
} // namespace esphome::ld2412
|
||||
@@ -1,16 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/button/button.h"
|
||||
#include "../ld2412.h"
|
||||
|
||||
namespace esphome::ld2412 {
|
||||
|
||||
class StartDynamicBackgroundCorrectionButton final : public button::Button, public Parented<LD2412Component> {
|
||||
public:
|
||||
StartDynamicBackgroundCorrectionButton() = default;
|
||||
|
||||
protected:
|
||||
void press_action() override;
|
||||
};
|
||||
|
||||
} // namespace esphome::ld2412
|
||||
@@ -79,7 +79,6 @@ static constexpr uint8_t OUT_PIN_LEVELS[] PROGMEM = {OUT_PIN_LEVEL_LOW, OUT_PIN_
|
||||
|
||||
static constexpr uint32_t BAUD_RATES[] PROGMEM = {9600, 19200, 38400, 57600, 115200, 230400, 256000, 460800};
|
||||
|
||||
static constexpr uint8_t DEFAULT_PRESENCE_TIMEOUT = 5; // Default used when number component is not defined
|
||||
// Commands
|
||||
static constexpr uint8_t CMD_ENABLE_CONF = 0xFF;
|
||||
static constexpr uint8_t CMD_DISABLE_CONF = 0xFE;
|
||||
@@ -237,20 +236,7 @@ void LD2412Component::restart_and_read_all_info() {
|
||||
}
|
||||
|
||||
void LD2412Component::loop() {
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
size_t avail = this->available();
|
||||
uint8_t buf[MAX_LINE_LENGTH];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(avail, sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
avail -= to_read;
|
||||
|
||||
for (size_t i = 0; i < to_read; i++) {
|
||||
this->readline_(buf[i]);
|
||||
}
|
||||
}
|
||||
this->read_available<MAX_LINE_LENGTH>([this](uint8_t b) { this->readline_(b); });
|
||||
}
|
||||
|
||||
void LD2412Component::send_command_(uint8_t command, const uint8_t *command_value, uint8_t command_value_len) {
|
||||
@@ -397,8 +383,10 @@ static inline bool has_value(number::Number *n) { return n != nullptr && n->has_
|
||||
|
||||
// Timeout ids 0 (still) and 1 (motion) for a write waiting on a re-query
|
||||
static constexpr uint32_t GATE_QUERY_TIMEOUT_ID = 0;
|
||||
static constexpr uint32_t GATE_QUERY_TIMEOUT_MS = 2000;
|
||||
#endif
|
||||
// Timeout id for a basic configuration write waiting on a re-query, and how long any such write waits
|
||||
static constexpr uint32_t BASIC_CONF_TIMEOUT_ID = 2;
|
||||
static constexpr uint32_t QUERY_TIMEOUT_MS = 2000;
|
||||
|
||||
void LD2412Component::handle_ack_data_() {
|
||||
ESP_LOGV(TAG, "Handling ACK DATA for COMMAND %02X", this->buffer_data_[COMMAND]);
|
||||
@@ -566,38 +554,44 @@ void LD2412Component::handle_ack_data_() {
|
||||
break;
|
||||
}
|
||||
|
||||
case CMD_QUERY_BASIC_CONF: // Query parameters response
|
||||
{
|
||||
#ifdef USE_NUMBER
|
||||
case CMD_QUERY_BASIC_CONF: { // Query parameters response
|
||||
if (this->buffer_pos_ < 15 + HEADER_FOOTER_SIZE) {
|
||||
break; // too short to hold the configuration
|
||||
}
|
||||
/*
|
||||
Moving distance range: 9th byte
|
||||
Still distance range: 10th byte
|
||||
*/
|
||||
set_number_value(this->min_distance_gate_number_, this->buffer_data_[10]);
|
||||
set_number_value(this->max_distance_gate_number_, this->buffer_data_[11] - 1);
|
||||
ESP_LOGV(TAG, "min_distance_gate_number_: %u, max_distance_gate_number_ %u", this->buffer_data_[10],
|
||||
this->buffer_data_[11]);
|
||||
/*
|
||||
None Duration: 11~12th bytes
|
||||
*/
|
||||
set_number_value(this->timeout_number_, encode_uint16(this->buffer_data_[13], this->buffer_data_[12]));
|
||||
ESP_LOGV(TAG, "timeout_number_: %u", encode_uint16(this->buffer_data_[13], this->buffer_data_[12]));
|
||||
/*
|
||||
Output pin configuration: 13th bytes
|
||||
*/
|
||||
// True only while a held-back write waits for this answer
|
||||
const bool pending = this->cancel_timeout(BASIC_CONF_TIMEOUT_ID);
|
||||
this->basic_conf_read_ = true;
|
||||
// Min gate: 9th byte, max gate: 10th byte, timeout: 11~12th bytes, output pin level: 13th byte
|
||||
this->min_gate_ = this->buffer_data_[10];
|
||||
this->max_gate_ = this->buffer_data_[11];
|
||||
this->timeout_ = encode_uint16(this->buffer_data_[13], this->buffer_data_[12]);
|
||||
this->out_pin_level_ = this->buffer_data_[14];
|
||||
ESP_LOGV(TAG, "min_distance_gate_number_: %u, max_distance_gate_number_ %u", this->min_gate_, this->max_gate_);
|
||||
ESP_LOGV(TAG, "timeout_number_: %u", this->timeout_);
|
||||
// The pending write's user values must survive the answer
|
||||
#ifdef USE_NUMBER
|
||||
if (!pending || !has_value(this->min_distance_gate_number_)) {
|
||||
set_number_value(this->min_distance_gate_number_, this->min_gate_);
|
||||
}
|
||||
if (!pending || !has_value(this->max_distance_gate_number_)) {
|
||||
set_number_value(this->max_distance_gate_number_, this->max_gate_ - 1);
|
||||
}
|
||||
if (!pending || !has_value(this->timeout_number_)) {
|
||||
set_number_value(this->timeout_number_, this->timeout_);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
if (this->out_pin_level_select_ != nullptr) {
|
||||
if (this->out_pin_level_select_ != nullptr && !(pending && this->out_pin_level_select_->has_state())) {
|
||||
if (const auto index = ld24xx::find_index(OUT_PIN_LEVELS, this->out_pin_level_)) {
|
||||
this->out_pin_level_select_->publish_state(*index);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
} break;
|
||||
if (pending) {
|
||||
this->set_basic_config();
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -703,8 +697,9 @@ void LD2412Component::set_engineering_mode(bool enable) {
|
||||
void LD2412Component::factory_reset() {
|
||||
this->set_config_mode_(true);
|
||||
this->send_command_(CMD_FACTORY_RESET, nullptr, 0);
|
||||
// Reset discards the settings; the next write must wait for the re-read
|
||||
this->basic_conf_read_ = false;
|
||||
#ifdef USE_NUMBER
|
||||
// Reset discards the thresholds; the next write must wait for the re-read
|
||||
this->gate_move_thresholds_.read = false;
|
||||
this->gate_still_thresholds_.read = false;
|
||||
#endif
|
||||
@@ -727,10 +722,22 @@ void LD2412Component::get_distance_resolution_() { this->send_command_(CMD_QUERY
|
||||
void LD2412Component::query_light_control_() { this->send_command_(CMD_QUERY_LIGHT_CONTROL, nullptr, 0); }
|
||||
|
||||
void LD2412Component::set_basic_config() {
|
||||
uint8_t min_gate = 1;
|
||||
uint8_t max_gate = TOTAL_GATES;
|
||||
uint16_t timeout = DEFAULT_PRESENCE_TIMEOUT;
|
||||
uint8_t out_pin_level = 0x01;
|
||||
// Entities that are not configured keep what the module last reported
|
||||
uint8_t min_gate = this->min_gate_;
|
||||
uint8_t max_gate = this->max_gate_;
|
||||
uint16_t timeout = this->timeout_;
|
||||
uint8_t out_pin_level = this->out_pin_level_;
|
||||
bool from_module = false;
|
||||
|
||||
#ifdef USE_NUMBER
|
||||
from_module = this->min_distance_gate_number_ == nullptr || this->max_distance_gate_number_ == nullptr ||
|
||||
this->timeout_number_ == nullptr;
|
||||
#else
|
||||
from_module = true;
|
||||
#endif
|
||||
#ifndef USE_SELECT
|
||||
from_module = true;
|
||||
#endif
|
||||
|
||||
#ifdef USE_NUMBER
|
||||
if (this->min_distance_gate_number_ != nullptr) {
|
||||
@@ -755,8 +762,19 @@ void LD2412Component::set_basic_config() {
|
||||
if (!index)
|
||||
return;
|
||||
out_pin_level = progmem_read_byte(&OUT_PIN_LEVELS[*index]);
|
||||
} else {
|
||||
from_module = true;
|
||||
}
|
||||
#endif
|
||||
if (from_module && !this->basic_conf_read_) {
|
||||
// The module never reported, so the values on hand are the built in defaults
|
||||
ESP_LOGW(TAG, "Command %02X held back until the module reports its settings", CMD_BASIC_CONF);
|
||||
this->set_config_mode_(true);
|
||||
this->query_parameters_();
|
||||
this->set_config_mode_(false);
|
||||
this->set_timeout(BASIC_CONF_TIMEOUT_ID, QUERY_TIMEOUT_MS, [this]() { this->basic_conf_timeout_(); });
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t value[5] = {
|
||||
lowbyte(min_gate), lowbyte(max_gate), lowbyte(timeout), highbyte(timeout), out_pin_level,
|
||||
@@ -766,6 +784,26 @@ void LD2412Component::set_basic_config() {
|
||||
this->set_config_mode_(false);
|
||||
}
|
||||
|
||||
void LD2412Component::basic_conf_timeout_() {
|
||||
ESP_LOGE(TAG, "Command %02X not sent, the module did not report its settings", CMD_BASIC_CONF);
|
||||
#ifdef USE_NUMBER
|
||||
// The values on hand never reached the module, so forgetting them loses nothing
|
||||
for (number::Number *n : {this->min_distance_gate_number_, this->max_distance_gate_number_, this->timeout_number_}) {
|
||||
if (n != nullptr) {
|
||||
n->publish_state(NAN);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
// Show the last known or default level until the module reports one
|
||||
if (this->out_pin_level_select_ != nullptr) {
|
||||
if (const auto index = ld24xx::find_index(OUT_PIN_LEVELS, this->out_pin_level_)) {
|
||||
this->out_pin_level_select_->publish_state(*index);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef USE_NUMBER
|
||||
void LD2412Component::send_gate_thresholds_(bool motion) {
|
||||
GateThresholds &group = this->gate_thresholds_(motion);
|
||||
@@ -783,9 +821,9 @@ void LD2412Component::send_gate_thresholds_(bool motion) {
|
||||
}
|
||||
if (configured < TOTAL_GATES && !group.read) {
|
||||
// Unread gates would go out as zero, which is maximum sensitivity
|
||||
ESP_LOGW(TAG, "Command %02X held back until the module reports its gate thresholds", command);
|
||||
ESP_LOGW(TAG, "Command %02X held back until the module reports its settings", command);
|
||||
this->send_command_(motion ? CMD_QUERY_MOTION_GATE_SENS : CMD_QUERY_STATIC_GATE_SENS, nullptr, 0);
|
||||
this->set_timeout(GATE_QUERY_TIMEOUT_ID + motion, GATE_QUERY_TIMEOUT_MS,
|
||||
this->set_timeout(GATE_QUERY_TIMEOUT_ID + motion, QUERY_TIMEOUT_MS,
|
||||
[this, motion]() { this->gate_query_timeout_(motion); });
|
||||
return;
|
||||
}
|
||||
@@ -793,7 +831,7 @@ void LD2412Component::send_gate_thresholds_(bool motion) {
|
||||
}
|
||||
|
||||
void LD2412Component::gate_query_timeout_(bool motion) {
|
||||
ESP_LOGE(TAG, "Command %02X not sent, the module did not report its gate thresholds",
|
||||
ESP_LOGE(TAG, "Command %02X not sent, the module did not report its settings",
|
||||
motion ? CMD_MOTION_GATE_SENS : CMD_STATIC_GATE_SENS);
|
||||
// The values on hand never reached the module or are stale, so forgetting them loses nothing
|
||||
for (number::Number *n : this->gate_thresholds_(motion).numbers) {
|
||||
|
||||
@@ -35,6 +35,7 @@ using namespace ld24xx;
|
||||
|
||||
static constexpr uint8_t MAX_LINE_LENGTH = 54; // Max characters for serial buffer
|
||||
static constexpr uint8_t TOTAL_GATES = 14; // Total number of gates supported by the LD2412
|
||||
static constexpr uint16_t DEFAULT_PRESENCE_TIMEOUT = 5;
|
||||
|
||||
enum BaudRate : uint8_t {
|
||||
BAUD_RATE_9600 = 1,
|
||||
@@ -136,6 +137,7 @@ class LD2412Component final : public Component, public uart::UARTDevice {
|
||||
void handle_periodic_data_();
|
||||
void handle_ack_data_();
|
||||
bool handle_frame_(uint8_t frame_len);
|
||||
void basic_conf_timeout_();
|
||||
void readline_(int readch);
|
||||
void query_parameters_();
|
||||
void get_version_();
|
||||
@@ -160,7 +162,12 @@ class LD2412Component final : public Component, public uart::UARTDevice {
|
||||
|
||||
uint8_t light_function_ = 0;
|
||||
uint8_t light_threshold_ = 0;
|
||||
uint8_t out_pin_level_ = 0;
|
||||
// Basic configuration as last reported by the module; resent for entities that are not configured
|
||||
bool basic_conf_read_{false}; // answered since boot or the last factory reset
|
||||
uint16_t timeout_ = DEFAULT_PRESENCE_TIMEOUT;
|
||||
uint8_t min_gate_ = 1;
|
||||
uint8_t max_gate_ = TOTAL_GATES;
|
||||
uint8_t out_pin_level_ = 1;
|
||||
uint8_t buffer_pos_ = 0; // where to resume processing/populating buffer
|
||||
uint8_t buffer_data_[MAX_LINE_LENGTH];
|
||||
uint8_t mac_address_[MAC_ADDRESS_SIZE] = {0, 0, 0, 0, 0, 0};
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from functools import partial
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import number
|
||||
import esphome.config_validation as cv
|
||||
@@ -18,11 +20,7 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import CONF_LD2412_ID, LD2412_ns, LD2412Component
|
||||
|
||||
GateThresholdNumber = LD2412_ns.class_("GateThresholdNumber", number.Number)
|
||||
LightThresholdNumber = LD2412_ns.class_("LightThresholdNumber", number.Number)
|
||||
MaxDistanceTimeoutNumber = LD2412_ns.class_("MaxDistanceTimeoutNumber", number.Number)
|
||||
from .. import CONF_LD2412_ID, LD2412Component
|
||||
|
||||
CONF_LIGHT_THRESHOLD = "light_threshold"
|
||||
CONF_MAX_DISTANCE_GATE = "max_distance_gate"
|
||||
@@ -35,25 +33,25 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase),
|
||||
cv.GenerateID(CONF_LD2412_ID): cv.use_id(LD2412Component),
|
||||
cv.Optional(CONF_LIGHT_THRESHOLD): number.number_schema(
|
||||
LightThresholdNumber,
|
||||
number.ApplyNumber,
|
||||
device_class=DEVICE_CLASS_ILLUMINANCE,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
icon=ICON_LIGHTBULB,
|
||||
),
|
||||
cv.Optional(CONF_MAX_DISTANCE_GATE): number.number_schema(
|
||||
MaxDistanceTimeoutNumber,
|
||||
number.ApplyNumber,
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
icon=ICON_MOTION_SENSOR,
|
||||
),
|
||||
cv.Optional(CONF_MIN_DISTANCE_GATE): number.number_schema(
|
||||
MaxDistanceTimeoutNumber,
|
||||
number.ApplyNumber,
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
icon=ICON_MOTION_SENSOR,
|
||||
),
|
||||
cv.Optional(CONF_TIMEOUT): number.number_schema(
|
||||
MaxDistanceTimeoutNumber,
|
||||
number.ApplyNumber,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
icon=ICON_TIMELAPSE,
|
||||
unit_of_measurement=UNIT_SECOND,
|
||||
@@ -66,14 +64,14 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
|
||||
cv.Optional(f"gate_{x}"): (
|
||||
{
|
||||
cv.Required(CONF_MOVE_THRESHOLD): number.number_schema(
|
||||
GateThresholdNumber,
|
||||
number.ApplyNumber,
|
||||
device_class=DEVICE_CLASS_SIGNAL_STRENGTH,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
icon=ICON_MOTION_SENSOR,
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
),
|
||||
cv.Required(CONF_STILL_THRESHOLD): number.number_schema(
|
||||
GateThresholdNumber,
|
||||
number.ApplyNumber,
|
||||
device_class=DEVICE_CLASS_SIGNAL_STRENGTH,
|
||||
entity_category=ENTITY_CATEGORY_CONFIG,
|
||||
icon=ICON_MOTION_SENSOR,
|
||||
@@ -95,13 +93,17 @@ async def to_code(config: ConfigType) -> None:
|
||||
min_value=0,
|
||||
max_value=255,
|
||||
step=1,
|
||||
apply="set_light_out_control()",
|
||||
publish="before",
|
||||
)
|
||||
basic = {"apply": "set_basic_config()", "publish": "before"}
|
||||
await numbers(
|
||||
CONF_MAX_DISTANCE_GATE,
|
||||
hub.set_max_distance_gate_number,
|
||||
min_value=2,
|
||||
max_value=13,
|
||||
step=1,
|
||||
**basic,
|
||||
)
|
||||
await numbers(
|
||||
CONF_MIN_DISTANCE_GATE,
|
||||
@@ -109,23 +111,32 @@ async def to_code(config: ConfigType) -> None:
|
||||
min_value=1,
|
||||
max_value=12,
|
||||
step=1,
|
||||
**basic,
|
||||
)
|
||||
threshold = {
|
||||
"min_value": 0,
|
||||
"max_value": 100,
|
||||
"step": 1,
|
||||
"apply": "set_gate_threshold(entity)",
|
||||
"publish": "before",
|
||||
}
|
||||
for x in range(14):
|
||||
if gate_conf := config.get(f"gate_{x}"):
|
||||
move_config = gate_conf[CONF_MOVE_THRESHOLD]
|
||||
n = await number.new_number(move_config, min_value=0, max_value=100, step=1)
|
||||
await cg.register_parented(n, hub)
|
||||
cg.add(hub.set_gate_move_threshold_number(x, n))
|
||||
still_config = gate_conf[CONF_STILL_THRESHOLD]
|
||||
n = await number.new_number(
|
||||
still_config, min_value=0, max_value=100, step=1
|
||||
await numbers.create(
|
||||
gate_conf[CONF_MOVE_THRESHOLD],
|
||||
partial(hub.set_gate_move_threshold_number, x),
|
||||
**threshold,
|
||||
)
|
||||
await numbers.create(
|
||||
gate_conf[CONF_STILL_THRESHOLD],
|
||||
partial(hub.set_gate_still_threshold_number, x),
|
||||
**threshold,
|
||||
)
|
||||
await cg.register_parented(n, hub)
|
||||
cg.add(hub.set_gate_still_threshold_number(x, n))
|
||||
await numbers(
|
||||
CONF_TIMEOUT,
|
||||
hub.set_timeout_number,
|
||||
min_value=0,
|
||||
max_value=900,
|
||||
step=1,
|
||||
**basic,
|
||||
)
|
||||
@@ -1,10 +0,0 @@
|
||||
#include "gate_threshold_number.h"
|
||||
|
||||
namespace esphome::ld2412 {
|
||||
|
||||
void GateThresholdNumber::control(float value) {
|
||||
this->publish_state(value);
|
||||
this->parent_->set_gate_threshold(this);
|
||||
}
|
||||
|
||||
} // namespace esphome::ld2412
|
||||
@@ -1,17 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/number/number.h"
|
||||
#include "../ld2412.h"
|
||||
|
||||
namespace esphome::ld2412 {
|
||||
|
||||
class GateThresholdNumber final : public number::Number, public Parented<LD2412Component> {
|
||||
public:
|
||||
// Not "= default": that makes new(p) T() zero-fill the object at every codegen site before the ctor runs.
|
||||
GateThresholdNumber() {}
|
||||
|
||||
protected:
|
||||
void control(float value) override;
|
||||
};
|
||||
|
||||
} // namespace esphome::ld2412
|
||||
Loaded 100 of 241 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user