diff --git a/esphome/components/ble_client/__init__.py b/esphome/components/ble_client/__init__.py index 03e8f0b0b2..8f70ad3417 100644 --- a/esphome/components/ble_client/__init__.py +++ b/esphome/components/ble_client/__init__.py @@ -29,8 +29,35 @@ BLEClientConnectTrigger = ble_client_ns.class_( BLEClientDisconnectTrigger = ble_client_ns.class_( "BLEClientDisconnectTrigger", automation.Trigger.template(BLEClientNodeConstRef) ) +BLEClientPasskeyRequestTrigger = ble_client_ns.class_( + "BLEClientPasskeyRequestTrigger", automation.Trigger.template(BLEClientNodeConstRef) +) +BLEClientPasskeyNotificationTrigger = ble_client_ns.class_( + "BLEClientPasskeyNotificationTrigger", + automation.Trigger.template(BLEClientNodeConstRef, cg.uint32), +) +BLEClientNumericComparisonRequestTrigger = ble_client_ns.class_( + "BLEClientNumericComparisonRequestTrigger", + automation.Trigger.template(BLEClientNodeConstRef, cg.uint32), +) + # Actions BLEWriteAction = ble_client_ns.class_("BLEClientWriteAction", automation.Action) +BLEPasskeyReplyAction = ble_client_ns.class_( + "BLEClientPasskeyReplyAction", automation.Action +) +BLENumericComparisonReplyAction = ble_client_ns.class_( + "BLEClientNumericComparisonReplyAction", automation.Action +) +BLERemoveBondAction = ble_client_ns.class_( + "BLEClientRemoveBondAction", automation.Action +) + +CONF_PASSKEY = "passkey" +CONF_ACCEPT = "accept" +CONF_ON_PASSKEY_REQUEST = "on_passkey_request" +CONF_ON_PASSKEY_NOTIFICATION = "on_passkey_notification" +CONF_ON_NUMERIC_COMPARISON_REQUEST = "on_numeric_comparison_request" # Espressif platformio framework is built with MAX_BLE_CONN to 3, so # enforce this in yaml checks. @@ -56,6 +83,29 @@ CONFIG_SCHEMA = ( ), } ), + 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) @@ -85,13 +135,34 @@ BLE_WRITE_ACTION_SCHEMA = cv.Schema( } ) +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_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_REMOVE_BOND_ACTION_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_ID): cv.use_id(BLEClient), + } +) + @automation.register_action( "ble_client.ble_write", BLEWriteAction, BLE_WRITE_ACTION_SCHEMA ) async def ble_write_to_code(config, action_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_arg, paren) + parent = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, parent) value = config[CONF_VALUE] if cg.is_template(value): @@ -137,6 +208,54 @@ async def ble_write_to_code(config, action_id, template_arg, args): return var +@automation.register_action( + "ble_client.numeric_comparison_reply", + BLENumericComparisonReplyAction, + BLE_NUMERIC_COMPARISON_REPLY_ACTION_SCHEMA, +) +async def numeric_comparison_reply_to_code(config, action_id, template_arg, args): + parent = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, parent) + + accept = config[CONF_ACCEPT] + if cg.is_template(accept): + templ = await cg.templatable(accept, args, cg.bool_) + cg.add(var.set_value_template(templ)) + else: + cg.add(var.set_value_simple(accept)) + + return var + + +@automation.register_action( + "ble_client.passkey_reply", BLEPasskeyReplyAction, BLE_PASSKEY_REPLY_ACTION_SCHEMA +) +async def passkey_reply_to_code(config, action_id, template_arg, args): + parent = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, parent) + + passkey = config[CONF_PASSKEY] + if cg.is_template(passkey): + templ = await cg.templatable(passkey, args, cg.uint32) + cg.add(var.set_value_template(templ)) + else: + cg.add(var.set_value_simple(passkey)) + + return var + + +@automation.register_action( + "ble_client.remove_bond", + BLERemoveBondAction, + BLE_REMOVE_BOND_ACTION_SCHEMA, +) +async def remove_bond_to_code(config, action_id, template_arg, args): + parent = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, parent) + + return var + + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) @@ -148,3 +267,12 @@ async def to_code(config): for conf in config.get(CONF_ON_DISCONNECT, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation(trigger, [], conf) + for conf in config.get(CONF_ON_PASSKEY_REQUEST, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + for conf in config.get(CONF_ON_PASSKEY_NOTIFICATION, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(cg.uint32, "passkey")], conf) + for conf in config.get(CONF_ON_NUMERIC_COMPARISON_REQUEST, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [(cg.uint32, "passkey")], conf) diff --git a/esphome/components/ble_client/automation.h b/esphome/components/ble_client/automation.h index 45ddba9782..423f74b85a 100644 --- a/esphome/components/ble_client/automation.h +++ b/esphome/components/ble_client/automation.h @@ -37,6 +37,44 @@ class BLEClientDisconnectTrigger : public Trigger<>, public BLEClientNode { } }; +class BLEClientPasskeyRequestTrigger : public Trigger<>, public BLEClientNode { + public: + explicit BLEClientPasskeyRequestTrigger(BLEClient *parent) { parent->register_ble_node(this); } + void loop() override {} + void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override { + if (event == ESP_GAP_BLE_PASSKEY_REQ_EVT && + memcmp(param->ble_security.auth_cmpl.bd_addr, this->parent_->get_remote_bda(), 6) == 0) { + this->trigger(); + } + } +}; + +class BLEClientPasskeyNotificationTrigger : public Trigger, public BLEClientNode { + public: + explicit BLEClientPasskeyNotificationTrigger(BLEClient *parent) { parent->register_ble_node(this); } + void loop() override {} + void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override { + if (event == ESP_GAP_BLE_PASSKEY_NOTIF_EVT && + memcmp(param->ble_security.auth_cmpl.bd_addr, this->parent_->get_remote_bda(), 6) == 0) { + uint32_t passkey = param->ble_security.key_notif.passkey; + this->trigger(passkey); + } + } +}; + +class BLEClientNumericComparisonRequestTrigger : public Trigger, public BLEClientNode { + public: + explicit BLEClientNumericComparisonRequestTrigger(BLEClient *parent) { parent->register_ble_node(this); } + void loop() override {} + void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override { + if (event == ESP_GAP_BLE_NC_REQ_EVT && + memcmp(param->ble_security.auth_cmpl.bd_addr, this->parent_->get_remote_bda(), 6) == 0) { + uint32_t passkey = param->ble_security.key_notif.passkey; + this->trigger(passkey); + } + } +}; + class BLEWriterClientNode : public BLEClientNode { public: BLEWriterClientNode(BLEClient *ble_client) { @@ -94,6 +132,86 @@ template class BLEClientWriteAction : public Action, publ std::function(Ts...)> value_template_{}; }; +template class BLEClientPasskeyReplyAction : public Action { + public: + BLEClientPasskeyReplyAction(BLEClient *ble_client) { parent_ = ble_client; } + + void play(Ts... x) override { + uint32_t passkey; + if (has_simple_value_) { + passkey = this->value_simple_; + } else { + passkey = this->value_template_(x...); + } + if (passkey > 999999) + return; + esp_bd_addr_t remote_bda; + memcpy(remote_bda, parent_->get_remote_bda(), sizeof(esp_bd_addr_t)); + esp_ble_passkey_reply(remote_bda, true, passkey); + } + + void set_value_template(std::function func) { + this->value_template_ = std::move(func); + has_simple_value_ = false; + } + + void set_value_simple(const uint32_t &value) { + this->value_simple_ = value; + has_simple_value_ = true; + } + + private: + BLEClient *parent_{nullptr}; + bool has_simple_value_ = true; + uint32_t value_simple_{0}; + std::function value_template_{}; +}; + +template class BLEClientNumericComparisonReplyAction : public Action { + public: + BLEClientNumericComparisonReplyAction(BLEClient *ble_client) { parent_ = ble_client; } + + void play(Ts... x) override { + esp_bd_addr_t remote_bda; + memcpy(remote_bda, parent_->get_remote_bda(), sizeof(esp_bd_addr_t)); + if (has_simple_value_) { + esp_ble_confirm_reply(remote_bda, this->value_simple_); + } else { + esp_ble_confirm_reply(remote_bda, this->value_template_(x...)); + } + } + + void set_value_template(std::function func) { + this->value_template_ = std::move(func); + has_simple_value_ = false; + } + + void set_value_simple(const bool &value) { + this->value_simple_ = value; + has_simple_value_ = true; + } + + private: + BLEClient *parent_{nullptr}; + bool has_simple_value_ = true; + bool value_simple_{false}; + std::function value_template_{}; +}; + +template class BLEClientRemoveBondAction : public Action { + public: + BLEClientRemoveBondAction(BLEClient *ble_client) { parent_ = ble_client; } + + void play(Ts... x) override { + esp_bd_addr_t remote_bda; + memcpy(remote_bda, parent_->get_remote_bda(), sizeof(esp_bd_addr_t)); + esp_ble_remove_bond_device(remote_bda); + } + + private: + BLEClient *parent_{nullptr}; +}; + } // namespace ble_client } // namespace esphome diff --git a/esphome/components/ble_client/ble_client.h b/esphome/components/ble_client/ble_client.h index ceca94c86a..e04f4a8042 100644 --- a/esphome/components/ble_client/ble_client.h +++ b/esphome/components/ble_client/ble_client.h @@ -27,7 +27,7 @@ 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) = 0; + 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; } diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index 7db6fff6b9..f508cecb87 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -9,6 +9,7 @@ CODEOWNERS = ["@jesserockz"] CONFLICTS_WITH = ["esp32_ble_beacon"] CONF_BLE_ID = "ble_id" +CONF_IO_CAPABILITY = "io_capability" NO_BLUETOOTH_VARIANTS = [const.VARIANT_ESP32S2] @@ -19,10 +20,21 @@ GAPEventHandler = esp32_ble_ns.class_("GAPEventHandler") GATTcEventHandler = esp32_ble_ns.class_("GATTcEventHandler") GATTsEventHandler = esp32_ble_ns.class_("GATTsEventHandler") +IoCapability = esp32_ble_ns.enum("IoCapability") +IO_CAPABILITY = { + "none": IoCapability.IO_CAP_NONE, + "keyboard_only": IoCapability.IO_CAP_IN, + "keyboard_display": IoCapability.IO_CAP_KBDISP, + "display_only": IoCapability.IO_CAP_OUT, + "display_yes_no": IoCapability.IO_CAP_IO, +} CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(ESP32BLE), + cv.Optional(CONF_IO_CAPABILITY, default="none"): cv.enum( + IO_CAPABILITY, lower=True + ), } ).extend(cv.COMPONENT_SCHEMA) @@ -39,6 +51,7 @@ FINAL_VALIDATE_SCHEMA = validate_variant async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) + cg.add(var.set_io_capability(config[CONF_IO_CAPABILITY])) if CORE.using_esp_idf: add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index 502399f97a..21ec005e07 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -134,8 +134,7 @@ bool ESP32BLE::ble_setup_() { return false; } - esp_ble_io_cap_t iocap = ESP_IO_CAP_NONE; - err = esp_ble_gap_set_security_param(ESP_BLE_SM_IOCAP_MODE, &iocap, sizeof(uint8_t)); + err = esp_ble_gap_set_security_param(ESP_BLE_SM_IOCAP_MODE, &(this->io_cap_), sizeof(uint8_t)); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_ble_gap_set_security_param failed: %d", err); return false; @@ -215,9 +214,31 @@ float ESP32BLE::get_setup_priority() const { return setup_priority::BLUETOOTH; } void ESP32BLE::dump_config() { const uint8_t *mac_address = esp_bt_dev_get_address(); if (mac_address) { + const char *io_capability_s; + switch (this->io_cap_) { + case ESP_IO_CAP_OUT: + io_capability_s = "display_only"; + break; + case ESP_IO_CAP_IO: + io_capability_s = "display_yes_no"; + break; + case ESP_IO_CAP_IN: + io_capability_s = "keyboard_only"; + break; + case ESP_IO_CAP_NONE: + io_capability_s = "none"; + break; + case ESP_IO_CAP_KBDISP: + io_capability_s = "keyboard_display"; + break; + default: + io_capability_s = "invalid"; + break; + } ESP_LOGCONFIG(TAG, "ESP32 BLE:"); ESP_LOGCONFIG(TAG, " MAC address: %02X:%02X:%02X:%02X:%02X:%02X", mac_address[0], mac_address[1], mac_address[2], mac_address[3], mac_address[4], mac_address[5]); + ESP_LOGCONFIG(TAG, " IO Capability: %s", io_capability_s); } else { ESP_LOGCONFIG(TAG, "ESP32 BLE: bluetooth stack is not enabled"); } diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index 5970b43688..11ae826544 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -25,6 +25,14 @@ typedef struct { uint16_t mtu; } conn_status_t; +enum IoCapability { + IO_CAP_OUT = ESP_IO_CAP_OUT, + IO_CAP_IO = ESP_IO_CAP_IO, + IO_CAP_IN = ESP_IO_CAP_IN, + IO_CAP_NONE = ESP_IO_CAP_NONE, + IO_CAP_KBDISP = ESP_IO_CAP_KBDISP, +}; + class GAPEventHandler { public: virtual void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) = 0; @@ -44,6 +52,8 @@ class GATTsEventHandler { class ESP32BLE : public Component { public: + void set_io_capability(IoCapability io_capability) { this->io_cap_ = (esp_ble_io_cap_t) io_capability; } + void setup() override; void loop() override; void dump_config() override; @@ -72,6 +82,7 @@ class ESP32BLE : public Component { Queue ble_events_; BLEAdvertising *advertising_; + esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE}; }; // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/tests/test1.yaml b/tests/test1.yaml index 56b7c8595a..46c6bb80c6 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -294,6 +294,9 @@ wled: adalight: +esp32_ble: + io_capability: keyboard_only + esp32_ble_tracker: ble_client: @@ -307,6 +310,19 @@ ble_client: on_disconnect: then: - switch.turn_on: ble1_status + on_passkey_request: + then: + - ble_client.passkey_reply: + id: ble_blah + passkey: 123456 + on_passkey_notification: + then: + - logger.log: "Passkey notification received" + on_numeric_comparison_request: + then: + - ble_client.numeric_comparison_reply: + id: ble_blah + accept: True - mac_address: C4:4F:33:11:22:33 id: my_bedjet_ble_client bedjet: