mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 07:03:55 +00:00 
			
		
		
		
	Add support for BLE passkey authentication (#4258)
Co-authored-by: Branden Cash <203336+ammmze@users.noreply.github.com>
This commit is contained in:
		| @@ -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) | ||||
|   | ||||
| @@ -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<uint32_t>, 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<uint32_t>, 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<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ | ||||
|   std::function<std::vector<uint8_t>(Ts...)> value_template_{}; | ||||
| }; | ||||
|  | ||||
| template<typename... Ts> class BLEClientPasskeyReplyAction : public Action<Ts...> { | ||||
|  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<uint32_t(Ts...)> 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<uint32_t(Ts...)> value_template_{}; | ||||
| }; | ||||
|  | ||||
| template<typename... Ts> class BLEClientNumericComparisonReplyAction : public Action<Ts...> { | ||||
|  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<bool(Ts...)> 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<bool(Ts...)> value_template_{}; | ||||
| }; | ||||
|  | ||||
| template<typename... Ts> class BLEClientRemoveBondAction : public Action<Ts...> { | ||||
|  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 | ||||
|  | ||||
|   | ||||
| @@ -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; } | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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"); | ||||
|   } | ||||
|   | ||||
| @@ -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<BLEEvent> ble_events_; | ||||
|   BLEAdvertising *advertising_; | ||||
|   esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE}; | ||||
| }; | ||||
|  | ||||
| // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) | ||||
|   | ||||
| @@ -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: | ||||
|   | ||||
		Reference in New Issue
	
	Block a user