diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index 75cf9d707d..26458d9853 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -1,3 +1,5 @@ +import re + from esphome import automation import esphome.codegen as cg from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant @@ -61,6 +63,43 @@ CONFIG_SCHEMA = cv.Schema( ).extend(cv.COMPONENT_SCHEMA) +bt_uuid16_format = "XXXX" +bt_uuid32_format = "XXXXXXXX" +bt_uuid128_format = "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" + + +def bt_uuid(value): + in_value = cv.string_strict(value) + value = in_value.upper() + + if len(value) == len(bt_uuid16_format): + pattern = re.compile("^[A-F|0-9]{4,}$") + if not pattern.match(value): + raise cv.Invalid( + f"Invalid hexadecimal value for 16 bit UUID format: '{in_value}'" + ) + return value + if len(value) == len(bt_uuid32_format): + pattern = re.compile("^[A-F|0-9]{8,}$") + if not pattern.match(value): + raise cv.Invalid( + f"Invalid hexadecimal value for 32 bit UUID format: '{in_value}'" + ) + return value + if len(value) == len(bt_uuid128_format): + pattern = re.compile( + "^[A-F|0-9]{8,}-[A-F|0-9]{4,}-[A-F|0-9]{4,}-[A-F|0-9]{4,}-[A-F|0-9]{12,}$" + ) + if not pattern.match(value): + raise cv.Invalid( + f"Invalid hexadecimal value for 128 UUID format: '{in_value}'" + ) + return value + raise cv.Invalid( + f"Service UUID must be in 16 bit '{bt_uuid16_format}', 32 bit '{bt_uuid32_format}', or 128 bit '{bt_uuid128_format}' format" + ) + + def validate_variant(_): variant = get_esp32_variant() if variant in NO_BLUETOOTH_VARIANTS: diff --git a/esphome/components/esp32_ble_server/__init__.py b/esphome/components/esp32_ble_server/__init__.py index b4b7bbbf12..0ec0621fad 100644 --- a/esphome/components/esp32_ble_server/__init__.py +++ b/esphome/components/esp32_ble_server/__init__.py @@ -2,6 +2,7 @@ from esphome import automation import esphome.codegen as cg from esphome.components import esp32_ble from esphome.components.esp32 import add_idf_sdkconfig_option +from esphome.components.esp32_ble import bt_uuid, bt_uuid16_format, bt_uuid32_format, bt_uuid128_format import esphome.config_validation as cv from esphome.const import ( CONF_ID, @@ -70,12 +71,6 @@ PROPERTY_MAP = { } -def validate_uuid(value): - if len(value) != 36: - raise cv.Invalid("UUID must be exactly 36 characters long") - return value - - def validate_on_write(char_config): if CONF_ON_WRITE in char_config: if not char_config[CONF_WRITE] and not char_config[CONF_WRITE_NO_RESPONSE]: @@ -113,8 +108,6 @@ def validate_notify_action(action_char_id): return action_char_id -UUID_SCHEMA = cv.Any(cv.All(cv.string, validate_uuid), cv.uint32_t) - DESCRIPTOR_VALUE_SCHEMA = cv.Any( cv.boolean, cv.float_, @@ -140,7 +133,7 @@ CHARACTERISTIC_VALUE_SCHEMA = cv.Any( DESCRIPTOR_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(BLEDescriptor), - cv.Required(CONF_UUID): UUID_SCHEMA, + cv.Required(CONF_UUID): bt_uuid, cv.Required(CONF_VALUE): DESCRIPTOR_VALUE_SCHEMA, } ) @@ -148,7 +141,7 @@ DESCRIPTOR_SCHEMA = cv.Schema( SERVICE_CHARACTERISTIC_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(BLECharacteristic), - cv.Required(CONF_UUID): UUID_SCHEMA, + cv.Required(CONF_UUID): bt_uuid, cv.Optional(CONF_WRITE_NO_RESPONSE, default=False): cv.boolean, cv.Optional(CONF_VALUE): CHARACTERISTIC_VALUE_SCHEMA, cv.GenerateID(CONF_VALUE_ACTION_ID_): cv.declare_id( @@ -165,7 +158,7 @@ SERVICE_CHARACTERISTIC_SCHEMA = cv.Schema( SERVICE_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(BLEService), - cv.Required(CONF_UUID): UUID_SCHEMA, + cv.Required(CONF_UUID): bt_uuid, cv.Optional(CONF_ADVERTISE, default=False): cv.boolean, cv.Optional(CONF_CHARACTERISTICS, default=[]): cv.ensure_list( SERVICE_CHARACTERISTIC_SCHEMA @@ -192,14 +185,6 @@ def parse_properties(char_conf): ) -def parse_uuid(uuid): - # If the UUID is a string, use from_raw - if isinstance(uuid, str): - return ESPBTUUID_ns.from_raw(uuid) - # Otherwise, use from_uint32 - return ESPBTUUID_ns.from_uint32(uuid) - - def parse_descriptor_value(value): # Compute the maximum length of the descriptor value # Also parse the value for byte arrays @@ -288,7 +273,7 @@ async def to_code(config): service_var = cg.Pvariable( service_config[CONF_ID], var.create_service( - parse_uuid(service_config[CONF_UUID]), + ESPBTUUID_ns.from_raw(service_config[CONF_UUID]), service_config[CONF_ADVERTISE], num_handles, ), @@ -297,7 +282,7 @@ async def to_code(config): char_var = cg.Pvariable( char_conf[CONF_ID], service_var.create_characteristic( - parse_uuid(char_conf[CONF_UUID]), + ESPBTUUID_ns.from_raw(char_conf[CONF_UUID]), parse_properties(char_conf), ), ) @@ -326,7 +311,7 @@ async def to_code(config): ) desc_var = cg.new_Pvariable( descriptor_conf[CONF_ID], - parse_uuid(descriptor_conf[CONF_UUID]), + ESPBTUUID_ns.from_raw(descriptor_conf[CONF_UUID]), max_length, ) if CONF_VALUE in descriptor_conf: diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index 0aa8eadd0a..ff13d8366b 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -1,9 +1,8 @@ -import re - from esphome import automation import esphome.codegen as cg from esphome.components import esp32_ble from esphome.components.esp32 import add_idf_sdkconfig_option +from esphome.components.esp32_ble import bt_uuid, bt_uuid16_format, bt_uuid32_format, bt_uuid128_format import esphome.config_validation as cv from esphome.const import ( CONF_ACTIVE, @@ -86,43 +85,6 @@ def validate_scan_parameters(config): return config -bt_uuid16_format = "XXXX" -bt_uuid32_format = "XXXXXXXX" -bt_uuid128_format = "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" - - -def bt_uuid(value): - in_value = cv.string_strict(value) - value = in_value.upper() - - if len(value) == len(bt_uuid16_format): - pattern = re.compile("^[A-F|0-9]{4,}$") - if not pattern.match(value): - raise cv.Invalid( - f"Invalid hexadecimal value for 16 bit UUID format: '{in_value}'" - ) - return value - if len(value) == len(bt_uuid32_format): - pattern = re.compile("^[A-F|0-9]{8,}$") - if not pattern.match(value): - raise cv.Invalid( - f"Invalid hexadecimal value for 32 bit UUID format: '{in_value}'" - ) - return value - if len(value) == len(bt_uuid128_format): - pattern = re.compile( - "^[A-F|0-9]{8,}-[A-F|0-9]{4,}-[A-F|0-9]{4,}-[A-F|0-9]{4,}-[A-F|0-9]{12,}$" - ) - if not pattern.match(value): - raise cv.Invalid( - f"Invalid hexadecimal value for 128 UUID format: '{in_value}'" - ) - return value - raise cv.Invalid( - f"Service UUID must be in 16 bit '{bt_uuid16_format}', 32 bit '{bt_uuid32_format}', or 128 bit '{bt_uuid128_format}' format" - ) - - def as_hex(value): return cg.RawExpression(f"0x{value}ULL")