[ble_client] Reject descriptor_uuid combined with notify at validation time (#18109)

This commit is contained in:
J. Nick Koston
2026-08-05 19:09:45 -05:00
committed by GitHub
parent f971e404c7
commit 2dfcde477f
5 changed files with 141 additions and 9 deletions
+36
View File
@@ -9,6 +9,7 @@ from esphome.const import (
CONF_ID,
CONF_MAC_ADDRESS,
CONF_NAME,
CONF_NOTIFY,
CONF_ON_CONNECT,
CONF_ON_DISCONNECT,
CONF_SERVICE_UUID,
@@ -16,11 +17,46 @@ from esphome.const import (
CONF_VALUE,
)
from esphome.core import ID
from esphome.types import ConfigType
AUTO_LOAD = ["esp32_ble_client"]
CODEOWNERS = ["@buxtronix", "@clydebarrow"]
DEPENDENCIES = ["esp32_ble_tracker"]
CONF_DESCRIPTOR_UUID = "descriptor_uuid"
CONF_ON_NOTIFY = "on_notify"
def validate_descriptor_not_notify(config: ConfigType) -> ConfigType:
"""Reject descriptor_uuid combined with notify or on_notify.
BLE descriptors cannot send notifications; only characteristics can, and
ESP-IDF has no descriptor variant of esp_ble_gattc_register_for_notify.
"""
if CONF_DESCRIPTOR_UUID in config and (
config.get(CONF_NOTIFY) or CONF_ON_NOTIFY in config
):
raise cv.Invalid(
f"'{CONF_DESCRIPTOR_UUID}' cannot be used with '{CONF_NOTIFY}' or "
f"'{CONF_ON_NOTIFY}': BLE descriptors cannot send notifications; remove "
f"'{CONF_DESCRIPTOR_UUID}' to receive characteristic notifications, or "
f"remove '{CONF_NOTIFY}' and '{CONF_ON_NOTIFY}' to poll the descriptor"
)
return config
def notify_from_on_notify(config: ConfigType) -> ConfigType:
"""Enable notifications when an on_notify automation is configured.
The triggers have no registration path of their own; without notify the
automation would validate but never fire.
"""
if CONF_ON_NOTIFY in config and not config[CONF_NOTIFY]:
config = config.copy()
config[CONF_NOTIFY] = True
return config
ble_client_ns = cg.esphome_ns.namespace("ble_client")
BLEClient = ble_client_ns.class_("BLEClient", esp32_ble_client.BLEClientBase)
BLEClientNode = ble_client_ns.class_("BLEClientNode")
@@ -14,13 +14,16 @@ from esphome.const import (
UNIT_DECIBEL_MILLIWATT,
)
from .. import ble_client_ns
from .. import (
CONF_DESCRIPTOR_UUID,
CONF_ON_NOTIFY,
ble_client_ns,
notify_from_on_notify,
validate_descriptor_not_notify,
)
DEPENDENCIES = ["ble_client"]
CONF_DESCRIPTOR_UUID = "descriptor_uuid"
CONF_ON_NOTIFY = "on_notify"
TYPE_CHARACTERISTIC = "characteristic"
TYPE_RSSI = "rssi"
@@ -85,6 +88,8 @@ CONFIG_SCHEMA = cv.All(
},
lower=True,
),
validate_descriptor_not_notify,
notify_from_on_notify,
)
@@ -9,13 +9,16 @@ from esphome.const import (
CONF_TRIGGER_ID,
)
from .. import ble_client_ns
from .. import (
CONF_DESCRIPTOR_UUID,
CONF_ON_NOTIFY,
ble_client_ns,
notify_from_on_notify,
validate_descriptor_not_notify,
)
DEPENDENCIES = ["ble_client"]
CONF_DESCRIPTOR_UUID = "descriptor_uuid"
CONF_ON_NOTIFY = "on_notify"
adv_data_t = cg.std_vector.template(cg.uint8)
adv_data_t_const_ref = adv_data_t.operator("ref").operator("const")
@@ -48,7 +51,9 @@ CONFIG_SCHEMA = cv.All(
}
)
.extend(cv.polling_component_schema("60s"))
.extend(ble_client.BLE_CLIENT_SCHEMA)
.extend(ble_client.BLE_CLIENT_SCHEMA),
validate_descriptor_not_notify,
notify_from_on_notify,
)
@@ -0,0 +1,86 @@
"""Tests for ble_client config validation."""
import pytest
from esphome import config_validation as cv
from esphome.components.ble_client import (
CONF_DESCRIPTOR_UUID,
CONF_ON_NOTIFY,
notify_from_on_notify,
validate_descriptor_not_notify,
)
from esphome.components.ble_client.sensor import CONFIG_SCHEMA as SENSOR_SCHEMA
from esphome.components.ble_client.text_sensor import (
CONFIG_SCHEMA as TEXT_SENSOR_SCHEMA,
)
from esphome.const import (
CONF_CHARACTERISTIC_UUID,
CONF_NAME,
CONF_NOTIFY,
CONF_SERVICE_UUID,
CONF_TYPE,
)
from esphome.types import ConfigType
DESCRIPTOR_CONFIG: ConfigType = {
CONF_NAME: "test",
CONF_SERVICE_UUID: "6E400001-B5A3-F393-E0A9-E50E24DCCA9E",
CONF_CHARACTERISTIC_UUID: "6E400003-B5A3-F393-E0A9-E50E24DCCA9E",
CONF_DESCRIPTOR_UUID: "2902",
}
def test_notify_with_descriptor_uuid_rejected() -> None:
config: ConfigType = {CONF_NOTIFY: True, CONF_DESCRIPTOR_UUID: "2902"}
with pytest.raises(cv.Invalid, match="cannot send notifications"):
validate_descriptor_not_notify(config)
def test_on_notify_with_descriptor_uuid_rejected() -> None:
config: ConfigType = {
CONF_NOTIFY: False,
CONF_ON_NOTIFY: [{}],
CONF_DESCRIPTOR_UUID: "2902",
}
with pytest.raises(cv.Invalid, match="cannot send notifications"):
validate_descriptor_not_notify(config)
def test_descriptor_uuid_without_notify_allowed() -> None:
config: ConfigType = {CONF_NOTIFY: False, CONF_DESCRIPTOR_UUID: "2902"}
assert validate_descriptor_not_notify(config) is config
def test_notify_without_descriptor_uuid_allowed() -> None:
config: ConfigType = {CONF_NOTIFY: True}
assert validate_descriptor_not_notify(config) is config
def test_sensor_schema_rejects_notify_with_descriptor() -> None:
config = {**DESCRIPTOR_CONFIG, CONF_TYPE: "characteristic", CONF_NOTIFY: True}
with pytest.raises(cv.Invalid, match="cannot send notifications"):
SENSOR_SCHEMA(config)
def test_text_sensor_schema_rejects_notify_with_descriptor() -> None:
config = {**DESCRIPTOR_CONFIG, CONF_NOTIFY: True}
with pytest.raises(cv.Invalid, match="cannot send notifications"):
TEXT_SENSOR_SCHEMA(config)
def test_sensor_schema_allows_descriptor_polling() -> None:
assert SENSOR_SCHEMA({**DESCRIPTOR_CONFIG, CONF_TYPE: "characteristic"})
def test_text_sensor_schema_allows_descriptor_polling() -> None:
assert TEXT_SENSOR_SCHEMA(dict(DESCRIPTOR_CONFIG))
def test_on_notify_implies_notify() -> None:
config: ConfigType = {CONF_NOTIFY: False, CONF_ON_NOTIFY: [{}]}
assert notify_from_on_notify(config)[CONF_NOTIFY] is True
def test_notify_unchanged_without_on_notify() -> None:
config: ConfigType = {CONF_NOTIFY: False}
assert notify_from_on_notify(config)[CONF_NOTIFY] is False