[improv_ble] Rename from esp32_improv and decouple from ESP32 (#19264)

This commit is contained in:
Jesse Hills
2026-09-16 20:49:56 +12:00
committed by GitHub
parent 9b21b64836
commit 4c7aee1a77
33 changed files with 303 additions and 140 deletions
+1 -1
View File
@@ -182,7 +182,6 @@ esphome/components/esp32_camera_web_server/* @ayufan
esphome/components/esp32_can/* @Sympatron
esphome/components/esp32_hosted/* @swoboda1337
esphome/components/esp32_hosted/update/* @swoboda1337
esphome/components/esp32_improv/* @jesserockz
esphome/components/esp32_rmt/* @jesserockz
esphome/components/esp32_rmt_led_strip/* @jesserockz
esphome/components/esp8266/* @esphome/core
@@ -268,6 +267,7 @@ esphome/components/i2s_audio/speaker/* @jesserockz @kahrendt
esphome/components/iaqcore/* @yozik04
esphome/components/ili9xxx/* @clydebarrow @nielsnl68
esphome/components/improv_base/* @esphome/core
esphome/components/improv_ble/* @jesserockz
esphome/components/improv_serial/* @esphome/core
esphome/components/ina226/* @latonita @Sergio303
esphome/components/ina260/* @mreditor97
+1
View File
@@ -6,5 +6,6 @@ See the component-alias section of esphome/loader.py.
# alias -> (canonical component, removal version or None)
COMPONENT_ALIASES: dict[str, tuple[str, str | None]] = {
"esp32_improv": ("improv_ble", "2027.4.0"),
"rp2040": ("rp2", "2027.7.0"),
}
@@ -597,7 +597,7 @@ async def to_code(config):
cg.add(parent.advertising_set_appearance(config[CONF_APPEARANCE]))
cg.add(var.set_max_clients(config[CONF_MAX_CLIENTS]))
# Only advertise for the server itself when the configuration gives clients something to
# find. A server that is auto-loaded purely to host a runtime service (esp32_improv) stays
# find. A server that is auto-loaded purely to host a runtime service (improv_ble) stays
# silent until that service asks for advertising.
cg.add(
var.set_advertising_required(
@@ -40,7 +40,7 @@ class BLEServer final : public Component, public Parented<ESP32BLE> {
/** Whether this server needs the device to advertise so clients can find and connect to it.
*
* False for a server that only hosts services created at runtime (e.g. esp32_improv), which
* False for a server that only hosts services created at runtime (e.g. improv_ble), which
* request advertising themselves for as long as they need it.
*/
void set_advertising_required(bool required) { this->advertising_required_ = required; }
+4 -2
View File
@@ -38,9 +38,11 @@ def _process_next_url(url: str) -> str:
return url
async def setup_improv_core(var: MockObj, config: ConfigType, component: str) -> None:
async def setup_improv_core(var: MockObj, config: ConfigType) -> None:
if next_url := config.get(CONF_NEXT_URL):
cg.add(var.set_next_url(_process_next_url(next_url)))
cg.add_define(f"USE_{component.upper()}_NEXT_URL")
# One define for all transports: next_url_ is per object, so a transport
# configured without next_url: calls add_next_url_ and appends nothing.
cg.add_define("USE_IMPROV_NEXT_URL")
cg.add_library("improv/Improv", "1.2.7")
@@ -8,7 +8,7 @@
namespace esphome::improv_base {
#if defined(USE_ESP32_IMPROV_NEXT_URL) || defined(USE_IMPROV_SERIAL_NEXT_URL)
#ifdef USE_IMPROV_NEXT_URL
static const char *const TAG = "improv_base";
static constexpr const char DEVICE_NAME_PLACEHOLDER[] = "{{device_name}}";
+3 -3
View File
@@ -3,7 +3,7 @@
#include <cstddef>
#include "esphome/core/defines.h"
#if defined(USE_ESP32_IMPROV_NEXT_URL) || defined(USE_IMPROV_SERIAL_NEXT_URL)
#ifdef USE_IMPROV_NEXT_URL
#include <improv.h>
#endif
@@ -11,12 +11,12 @@ namespace esphome::improv_base {
class ImprovBase {
public:
#if defined(USE_ESP32_IMPROV_NEXT_URL) || defined(USE_IMPROV_SERIAL_NEXT_URL)
#ifdef USE_IMPROV_NEXT_URL
void set_next_url(const char *next_url) { this->next_url_ = next_url; }
#endif
protected:
#if defined(USE_ESP32_IMPROV_NEXT_URL) || defined(USE_IMPROV_SERIAL_NEXT_URL)
#ifdef USE_IMPROV_NEXT_URL
/// Format next_url_ into buffer, replacing placeholders. Returns length written.
size_t get_formatted_next_url_(char *buffer, size_t buffer_size);
/// Append the formatted next_url to the RPC response, warning if it does not fit.
@@ -1,14 +1,41 @@
from esphome import automation
import esphome.codegen as cg
from esphome.components import binary_sensor, esp32_ble, improv_base, output
from esphome.components.esp32_ble import BTLoggers
from esphome.components import binary_sensor, improv_base, output
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_ON_START, CONF_ON_STATE, CONF_TRIGGER_ID
from esphome.const import (
CONF_ID,
CONF_ON_START,
CONF_ON_STATE,
CONF_TRIGGER_ID,
PLATFORM_ESP32,
)
from esphome.core import CORE
from esphome.types import ConfigType
AUTO_LOAD = ["esp32_ble_server", "improv_base"]
# The BLE GATT server component that hosts the Improv service, per target
# platform. improv_ble itself is platform neutral; supporting another chip
# means adding its BLE server component here and the matching backend in
# improv_ble_component.cpp. Doubles as the platform gate below, so an
# unsupported chip is rejected in validation rather than at link time.
BLE_SERVER_BACKENDS: dict[str, str] = {
PLATFORM_ESP32: "esp32_ble_server",
}
def AUTO_LOAD() -> list[str]:
auto_load = ["improv_base"]
if backend := BLE_SERVER_BACKENDS.get(CORE.target_platform):
auto_load.append(backend)
return auto_load
CODEOWNERS = ["@jesserockz"]
DEPENDENCIES = ["wifi", "esp32"]
DEPENDENCIES = ["wifi"]
# Legacy top-level YAML key that routes here; esphome/loader.py and
# esphome/config.py handle the warning and the key rename.
ALIASES = ["esp32_improv"]
ALIAS_REMOVAL_VERSION = "2027.4.0"
CONF_AUTHORIZED_DURATION = "authorized_duration"
CONF_AUTHORIZER = "authorizer"
@@ -29,29 +56,29 @@ improv_ns = cg.esphome_ns.namespace("improv")
Error = improv_ns.enum("Error")
State = improv_ns.enum("State")
esp32_improv_ns = cg.esphome_ns.namespace("esp32_improv")
ESP32ImprovComponent = esp32_improv_ns.class_("ESP32ImprovComponent", cg.Component)
ESP32ImprovProvisionedTrigger = esp32_improv_ns.class_(
"ESP32ImprovProvisionedTrigger", automation.Trigger.template()
improv_ble_ns = cg.esphome_ns.namespace("improv_ble")
ImprovBLEComponent = improv_ble_ns.class_("ImprovBLEComponent", cg.Component)
ImprovBLEProvisionedTrigger = improv_ble_ns.class_(
"ImprovBLEProvisionedTrigger", automation.Trigger.template()
)
ESP32ImprovProvisioningTrigger = esp32_improv_ns.class_(
"ESP32ImprovProvisioningTrigger", automation.Trigger.template()
ImprovBLEProvisioningTrigger = improv_ble_ns.class_(
"ImprovBLEProvisioningTrigger", automation.Trigger.template()
)
ESP32ImprovStartTrigger = esp32_improv_ns.class_(
"ESP32ImprovStartTrigger", automation.Trigger.template()
ImprovBLEStartTrigger = improv_ble_ns.class_(
"ImprovBLEStartTrigger", automation.Trigger.template()
)
ESP32ImprovStateTrigger = esp32_improv_ns.class_(
"ESP32ImprovStateTrigger", automation.Trigger.template()
ImprovBLEStateTrigger = improv_ble_ns.class_(
"ImprovBLEStateTrigger", automation.Trigger.template()
)
ESP32ImprovStoppedTrigger = esp32_improv_ns.class_(
"ESP32ImprovStoppedTrigger", automation.Trigger.template()
ImprovBLEStoppedTrigger = improv_ble_ns.class_(
"ImprovBLEStoppedTrigger", automation.Trigger.template()
)
CONFIG_SCHEMA = (
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(ESP32ImprovComponent),
cv.GenerateID(): cv.declare_id(ImprovBLEComponent),
cv.Required(CONF_AUTHORIZER): cv.Any(
cv.none, cv.use_id(binary_sensor.BinarySensor)
),
@@ -68,55 +95,60 @@ CONFIG_SCHEMA = (
cv.Optional(CONF_ON_PROVISIONED): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
ESP32ImprovProvisionedTrigger
ImprovBLEProvisionedTrigger
),
}
),
cv.Optional(CONF_ON_PROVISIONING): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
ESP32ImprovProvisioningTrigger
ImprovBLEProvisioningTrigger
),
}
),
cv.Optional(CONF_ON_START): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
ESP32ImprovStartTrigger
ImprovBLEStartTrigger
),
}
),
cv.Optional(CONF_ON_STATE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
ESP32ImprovStateTrigger
ImprovBLEStateTrigger
),
}
),
cv.Optional(CONF_ON_STOP): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
ESP32ImprovStoppedTrigger
ImprovBLEStoppedTrigger
),
}
),
}
)
.extend(improv_base.IMPROV_SCHEMA)
.extend(cv.COMPONENT_SCHEMA)
.extend(cv.COMPONENT_SCHEMA),
cv.only_on(list(BLE_SERVER_BACKENDS)),
)
async def to_code(config: ConfigType) -> None:
# ESP32 backend setup: the platform gate above means this is the only backend
# that can reach to_code. Make it conditional when a second one is added.
from esphome.components import esp32_ble
# Register the loggers this component needs
esp32_ble.register_bt_logger(BTLoggers.GATT, BTLoggers.SMP)
esp32_ble.register_bt_logger(esp32_ble.BTLoggers.GATT, esp32_ble.BTLoggers.SMP)
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
cg.add_define("USE_IMPROV")
cg.add_define("USE_IMPROV_BLE")
await improv_base.setup_improv_core(var, config, "esp32_improv")
await improv_base.setup_improv_core(var, config)
cg.add(var.set_identify_duration(config[CONF_IDENTIFY_DURATION]))
cg.add(var.set_authorized_duration(config[CONF_AUTHORIZED_DURATION]))
@@ -155,4 +187,4 @@ async def to_code(config: ConfigType) -> None:
await automation.build_automation(trigger, [], conf)
use_state_callback = True
if use_state_callback:
cg.add_define("USE_ESP32_IMPROV_STATE_CALLBACK")
cg.add_define("USE_IMPROV_BLE_STATE_CALLBACK")
@@ -1,17 +1,17 @@
#pragma once
#ifdef USE_ESP32
#ifdef USE_ESP32_IMPROV_STATE_CALLBACK
#include "esp32_improv_component.h"
#ifdef USE_IMPROV_BLE_STATE_CALLBACK
#include "improv_ble_component.h"
#include "esphome/core/automation.h"
#include <improv.h>
namespace esphome::esp32_improv {
namespace esphome::improv_ble {
class ESP32ImprovProvisionedTrigger final : public Trigger<> {
class ImprovBLEProvisionedTrigger final : public Trigger<> {
public:
explicit ESP32ImprovProvisionedTrigger(ESP32ImprovComponent *parent) : parent_(parent) {
explicit ImprovBLEProvisionedTrigger(ImprovBLEComponent *parent) : parent_(parent) {
parent->add_on_state_callback([this](improv::State state, improv::Error error) {
if (state == improv::STATE_PROVISIONED && !this->parent_->is_failed()) {
this->trigger();
@@ -20,12 +20,12 @@ class ESP32ImprovProvisionedTrigger final : public Trigger<> {
}
protected:
ESP32ImprovComponent *parent_;
ImprovBLEComponent *parent_;
};
class ESP32ImprovProvisioningTrigger final : public Trigger<> {
class ImprovBLEProvisioningTrigger final : public Trigger<> {
public:
explicit ESP32ImprovProvisioningTrigger(ESP32ImprovComponent *parent) : parent_(parent) {
explicit ImprovBLEProvisioningTrigger(ImprovBLEComponent *parent) : parent_(parent) {
parent->add_on_state_callback([this](improv::State state, improv::Error error) {
if (state == improv::STATE_PROVISIONING && !this->parent_->is_failed()) {
this->trigger();
@@ -34,12 +34,12 @@ class ESP32ImprovProvisioningTrigger final : public Trigger<> {
}
protected:
ESP32ImprovComponent *parent_;
ImprovBLEComponent *parent_;
};
class ESP32ImprovStartTrigger final : public Trigger<> {
class ImprovBLEStartTrigger final : public Trigger<> {
public:
explicit ESP32ImprovStartTrigger(ESP32ImprovComponent *parent) : parent_(parent) {
explicit ImprovBLEStartTrigger(ImprovBLEComponent *parent) : parent_(parent) {
parent->add_on_state_callback([this](improv::State state, improv::Error error) {
if ((state == improv::STATE_AUTHORIZED || state == improv::STATE_AWAITING_AUTHORIZATION) &&
!this->parent_->is_failed()) {
@@ -49,12 +49,12 @@ class ESP32ImprovStartTrigger final : public Trigger<> {
}
protected:
ESP32ImprovComponent *parent_;
ImprovBLEComponent *parent_;
};
class ESP32ImprovStateTrigger final : public Trigger<improv::State, improv::Error> {
class ImprovBLEStateTrigger final : public Trigger<improv::State, improv::Error> {
public:
explicit ESP32ImprovStateTrigger(ESP32ImprovComponent *parent) : parent_(parent) {
explicit ImprovBLEStateTrigger(ImprovBLEComponent *parent) : parent_(parent) {
parent->add_on_state_callback([this](improv::State state, improv::Error error) {
if (!this->parent_->is_failed()) {
this->trigger(state, error);
@@ -63,12 +63,12 @@ class ESP32ImprovStateTrigger final : public Trigger<improv::State, improv::Erro
}
protected:
ESP32ImprovComponent *parent_;
ImprovBLEComponent *parent_;
};
class ESP32ImprovStoppedTrigger final : public Trigger<> {
class ImprovBLEStoppedTrigger final : public Trigger<> {
public:
explicit ESP32ImprovStoppedTrigger(ESP32ImprovComponent *parent) : parent_(parent) {
explicit ImprovBLEStoppedTrigger(ImprovBLEComponent *parent) : parent_(parent) {
parent->add_on_state_callback([this](improv::State state, improv::Error error) {
if (state == improv::STATE_STOPPED && !this->parent_->is_failed()) {
this->trigger();
@@ -77,10 +77,10 @@ class ESP32ImprovStoppedTrigger final : public Trigger<> {
}
protected:
ESP32ImprovComponent *parent_;
ImprovBLEComponent *parent_;
};
} // namespace esphome::esp32_improv
} // namespace esphome::improv_ble
#endif
#endif
@@ -1,10 +1,7 @@
#include "esp32_improv_component.h"
#include "improv_ble_component.h"
#include <array>
#include "esphome/components/bytebuffer/bytebuffer.h"
#include "esphome/components/esp32_ble/ble.h"
#include "esphome/components/esp32_ble_server/ble_2902.h"
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
@@ -15,11 +12,15 @@
#ifdef USE_ESP32
namespace esphome::esp32_improv {
#include "esphome/components/bytebuffer/bytebuffer.h"
#include "esphome/components/esp32_ble/ble.h"
#include "esphome/components/esp32_ble_server/ble_2902.h"
namespace esphome::improv_ble {
using namespace bytebuffer;
static const char *const TAG = "esp32_improv.component";
static const char *const TAG = "improv_ble.component";
static constexpr size_t IMPROV_MAX_LOG_BYTES = 128;
static constexpr char ESPHOME_MY_LINK[] = "https://my.home-assistant.io/redirect/config_flow_start?domain=esphome";
// command + data length + trailing byte
@@ -38,9 +39,9 @@ static constexpr uint8_t IMPROV_SERVICE_DATA_SIZE = 8;
static constexpr uint8_t IMPROV_PROTOCOL_ID_1 = 0x77; // 'P' << 1 | 'R' >> 7
static constexpr uint8_t IMPROV_PROTOCOL_ID_2 = 0x46; // 'I' << 1 | 'M' >> 7
ESP32ImprovComponent::ESP32ImprovComponent() { global_improv_component = this; }
ImprovBLEComponent::ImprovBLEComponent() { global_improv_component = this; }
void ESP32ImprovComponent::setup() {
void ImprovBLEComponent::setup() {
#ifdef USE_BINARY_SENSOR
if (this->authorizer_ != nullptr) {
this->authorizer_->add_on_state_callback([this](bool state) {
@@ -66,7 +67,7 @@ void ESP32ImprovComponent::setup() {
this->disable_loop();
}
void ESP32ImprovComponent::setup_characteristics() {
void ImprovBLEComponent::setup_characteristics() {
this->status_ = this->service_->create_characteristic(
improv::STATUS_UUID, BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY);
BLEDescriptor *status_descriptor = new BLE2902();
@@ -104,11 +105,11 @@ void ESP32ImprovComponent::setup_characteristics() {
this->setup_complete_ = true;
}
void ESP32ImprovComponent::loop() {
void ImprovBLEComponent::loop() {
if (!global_ble_server->is_running()) {
if (this->state_ != improv::STATE_STOPPED) {
this->state_ = improv::STATE_STOPPED;
#ifdef USE_ESP32_IMPROV_STATE_CALLBACK
#ifdef USE_IMPROV_BLE_STATE_CALLBACK
this->state_callback_.call(this->state_, this->error_state_);
#endif
}
@@ -200,7 +201,7 @@ void ESP32ImprovComponent::loop() {
}
}
void ESP32ImprovComponent::set_status_indicator_state_(bool state) {
void ImprovBLEComponent::set_status_indicator_state_(bool state) {
#ifdef USE_OUTPUT
if (this->status_indicator_ == nullptr)
return;
@@ -216,7 +217,7 @@ void ESP32ImprovComponent::set_status_indicator_state_(bool state) {
}
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG
const char *ESP32ImprovComponent::state_to_string_(improv::State state) {
const char *ImprovBLEComponent::state_to_string_(improv::State state) {
switch (state) {
case improv::STATE_STOPPED:
return "STOPPED";
@@ -234,7 +235,7 @@ const char *ESP32ImprovComponent::state_to_string_(improv::State state) {
}
#endif
bool ESP32ImprovComponent::check_identify_() {
bool ImprovBLEComponent::check_identify_() {
uint32_t now = millis();
bool identify = this->identify_start_ != 0 && now - this->identify_start_ <= this->identify_duration_;
@@ -246,7 +247,7 @@ bool ESP32ImprovComponent::check_identify_() {
return identify;
}
void ESP32ImprovComponent::set_state_(improv::State state, bool update_advertising) {
void ImprovBLEComponent::set_state_(improv::State state, bool update_advertising) {
// Skip if state hasn't changed
if (this->state_ == state) {
return;
@@ -274,12 +275,12 @@ void ESP32ImprovComponent::set_state_(improv::State state, bool update_advertisi
// Advertise the new state via service data
this->advertise_service_data_();
}
#ifdef USE_ESP32_IMPROV_STATE_CALLBACK
#ifdef USE_IMPROV_BLE_STATE_CALLBACK
this->state_callback_.call(this->state_, this->error_state_);
#endif
}
void ESP32ImprovComponent::set_error_(improv::Error error) {
void ImprovBLEComponent::set_error_(improv::Error error) {
if (error != improv::ERROR_NONE) {
ESP_LOGE(TAG, "Error: %d", error);
}
@@ -295,14 +296,14 @@ void ESP32ImprovComponent::set_error_(improv::Error error) {
}
}
void ESP32ImprovComponent::send_response_(std::span<const uint8_t> response) {
void ImprovBLEComponent::send_response_(std::span<const uint8_t> response) {
// The BLE characteristic owns its value, so one exact-size copy is required here
this->rpc_response_->set_value(std::vector<uint8_t>(response.begin(), response.end()));
if (this->state_ != improv::STATE_STOPPED)
this->rpc_response_->notify();
}
void ESP32ImprovComponent::start() {
void ImprovBLEComponent::start() {
if (this->should_start_ || this->state_ != improv::STATE_STOPPED)
return;
@@ -320,7 +321,7 @@ void ESP32ImprovComponent::start() {
this->enable_loop();
}
void ESP32ImprovComponent::stop() {
void ImprovBLEComponent::stop() {
this->should_start_ = false;
// Wait before stopping the service to ensure all BLE clients see the state change.
// This prevents clients from repeatedly reconnecting and wasting resources by allowing
@@ -335,10 +336,10 @@ void ESP32ImprovComponent::stop() {
});
}
float ESP32ImprovComponent::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; }
float ImprovBLEComponent::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; }
void ESP32ImprovComponent::dump_config() {
ESP_LOGCONFIG(TAG, "ESP32 Improv:");
void ImprovBLEComponent::dump_config() {
ESP_LOGCONFIG(TAG, "Improv BLE:");
#ifdef USE_BINARY_SENSOR
LOG_BINARY_SENSOR(" ", "Authorizer", this->authorizer_);
#endif
@@ -347,7 +348,7 @@ void ESP32ImprovComponent::dump_config() {
#endif
}
void ESP32ImprovComponent::process_incoming_data_() {
void ImprovBLEComponent::process_incoming_data_() {
if (this->incoming_data_.size() < 3)
return;
uint8_t length = this->incoming_data_[1];
@@ -422,7 +423,7 @@ void ESP32ImprovComponent::process_incoming_data_() {
}
}
void ESP32ImprovComponent::on_wifi_connect_timeout_() {
void ImprovBLEComponent::on_wifi_connect_timeout_() {
this->set_error_(improv::ERROR_UNABLE_TO_CONNECT);
this->set_state_(improv::STATE_AUTHORIZED);
#ifdef USE_BINARY_SENSOR
@@ -433,7 +434,7 @@ void ESP32ImprovComponent::on_wifi_connect_timeout_() {
wifi::global_wifi_component->clear_sta();
}
void ESP32ImprovComponent::check_wifi_connection_() {
void ImprovBLEComponent::check_wifi_connection_() {
if (!wifi::global_wifi_component->is_connected()) {
return;
}
@@ -447,7 +448,7 @@ void ESP32ImprovComponent::check_wifi_connection_() {
std::array<uint8_t, improv::RPC_RESPONSE_MAX_SIZE> buf;
improv::RpcResponseBuilder builder(buf, improv::WIFI_SETTINGS);
#ifdef USE_ESP32_IMPROV_NEXT_URL
#ifdef USE_IMPROV_NEXT_URL
// Add next_url if configured (should be first per Improv BLE spec)
this->add_next_url_(builder, MAX_NEXT_URL_LEN);
#endif
@@ -480,7 +481,7 @@ void ESP32ImprovComponent::check_wifi_connection_() {
this->stop();
}
void ESP32ImprovComponent::advertise_service_data_() {
void ImprovBLEComponent::advertise_service_data_() {
uint8_t service_data[IMPROV_SERVICE_DATA_SIZE] = {};
service_data[0] = IMPROV_PROTOCOL_ID_1; // PR
service_data[1] = IMPROV_PROTOCOL_ID_2; // IM
@@ -499,7 +500,7 @@ void ESP32ImprovComponent::advertise_service_data_() {
esp32_ble::global_ble->advertising_set_service_data_and_name(std::span<const uint8_t>(service_data), false);
}
void ESP32ImprovComponent::update_advertising_type_() {
void ImprovBLEComponent::update_advertising_type_() {
uint32_t now = App.get_loop_component_start_time();
// If we're advertising the device name and it's been more than NAME_ADVERTISING_DURATION, switch back to service data
@@ -524,21 +525,21 @@ void ESP32ImprovComponent::update_advertising_type_() {
}
}
void ESP32ImprovComponent::request_advertising_() {
void ImprovBLEComponent::request_advertising_() {
if (this->advertising_requested_)
return;
this->advertising_requested_ = true;
esp32_ble::global_ble->advertising_start();
}
void ESP32ImprovComponent::release_advertising_() {
void ImprovBLEComponent::release_advertising_() {
if (!this->advertising_requested_)
return;
this->advertising_requested_ = false;
esp32_ble::global_ble->advertising_stop();
}
improv::State ESP32ImprovComponent::get_initial_state_() const {
improv::State ImprovBLEComponent::get_initial_state_() const {
#ifdef USE_BINARY_SENSOR
// If we have an authorizer, start in awaiting authorization state
return this->authorizer_ == nullptr ? improv::STATE_AUTHORIZED : improv::STATE_AWAITING_AUTHORIZATION;
@@ -548,8 +549,8 @@ improv::State ESP32ImprovComponent::get_initial_state_() const {
#endif
}
ESP32ImprovComponent *global_improv_component = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
ImprovBLEComponent *global_improv_component = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
} // namespace esphome::esp32_improv
} // namespace esphome::improv_ble
#endif
@@ -5,12 +5,10 @@
#include "esphome/core/helpers.h"
#include "esphome/core/preferences.h"
#include "esphome/components/esp32_ble_server/ble_characteristic.h"
#include "esphome/components/esp32_ble_server/ble_server.h"
#include "esphome/components/improv_base/improv_base.h"
#include "esphome/components/wifi/wifi_component.h"
#ifdef USE_ESP32_IMPROV_STATE_CALLBACK
#ifdef USE_IMPROV_BLE_STATE_CALLBACK
#include "esphome/core/automation.h"
#endif
@@ -25,17 +23,23 @@
#include <span>
#include <vector>
// ESP-IDF is currently the only target platform with a BLE GATT server, so it is
// the only backend this component has. The Python side keeps the platform table
// (BLE_SERVER_BACKENDS in __init__.py); a second backend adds another arm here.
#ifdef USE_ESP32
#include "esphome/components/esp32_ble_server/ble_characteristic.h"
#include "esphome/components/esp32_ble_server/ble_server.h"
#include <improv.h>
namespace esphome::esp32_improv {
namespace esphome::improv_ble {
using namespace esp32_ble_server;
class ESP32ImprovComponent final : public Component, public improv_base::ImprovBase {
class ImprovBLEComponent final : public Component, public improv_base::ImprovBase {
public:
ESP32ImprovComponent();
ImprovBLEComponent();
void dump_config() override;
void loop() override;
void setup() override;
@@ -47,7 +51,7 @@ class ESP32ImprovComponent final : public Component, public improv_base::ImprovB
bool is_active() const { return this->state_ != improv::STATE_STOPPED; }
bool should_start() const { return this->should_start_; }
#ifdef USE_ESP32_IMPROV_STATE_CALLBACK
#ifdef USE_IMPROV_BLE_STATE_CALLBACK
template<typename F> void add_on_state_callback(F &&callback) {
this->state_callback_.add(std::forward<F>(callback));
}
@@ -97,7 +101,7 @@ class ESP32ImprovComponent final : public Component, public improv_base::ImprovB
improv::State state_{improv::STATE_STOPPED};
improv::Error error_state_{improv::ERROR_NONE};
#ifdef USE_ESP32_IMPROV_STATE_CALLBACK
#ifdef USE_IMPROV_BLE_STATE_CALLBACK
CallbackManager<void(improv::State, improv::Error)> state_callback_{};
#endif
@@ -125,8 +129,8 @@ class ESP32ImprovComponent final : public Component, public improv_base::ImprovB
};
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
extern ESP32ImprovComponent *global_improv_component;
extern ImprovBLEComponent *global_improv_component;
} // namespace esphome::esp32_improv
} // namespace esphome::improv_ble
#endif
+1 -1
View File
@@ -70,7 +70,7 @@ FINAL_VALIDATE_SCHEMA = validate_transport
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await improv_base.setup_improv_core(var, config, "improv_serial")
await improv_base.setup_improv_core(var, config)
cg.add_define("USE_IMPROV_SERIAL")
if (uart_id := config.get(CONF_UART_ID)) is not None:
cg.add(var.set_uart(await cg.get_variable(uart_id)))
@@ -208,7 +208,7 @@ void ImprovSerialComponent::add_webserver_urls_(improv::RpcResponseBuilder &buil
void ImprovSerialComponent::send_settings_response_(improv::Command command) {
std::array<uint8_t, improv::RPC_RESPONSE_MAX_SIZE> buf;
improv::RpcResponseBuilder builder(buf, command);
#ifdef USE_IMPROV_SERIAL_NEXT_URL
#ifdef USE_IMPROV_NEXT_URL
this->add_next_url_(builder, MAX_NEXT_URL_LEN);
#endif
#ifdef USE_WEBSERVER
@@ -55,7 +55,7 @@ static const uint8_t IMPROV_SERIAL_VERSION = 1;
#ifdef USE_WIFI
// Wi-Fi connect failure timers: a fresh provision reports at 30 s (stock behavior), while
// switching networks on an already-connected device (disconnect + reconnect) can legitimately
// take longer; 90 s matches esp32_improv's default wifi_timeout.
// take longer; 90 s matches improv_ble's default wifi_timeout.
static const uint32_t WIFI_CONNECT_TIMEOUT_MS = 30000;
static const uint32_t WIFI_SWITCH_TIMEOUT_MS = 90000;
#endif
+1 -1
View File
@@ -355,7 +355,7 @@ def final_validate(config):
has_sta = bool(config.get(CONF_NETWORKS, True))
has_ap = CONF_AP in config
full_config = fv.full_config.get()
has_improv = "esp32_improv" in full_config
has_improv = "improv_ble" in full_config
has_improv_serial = "improv_serial" in full_config
has_captive_portal = "captive_portal" in full_config
has_web_server = "web_server" in full_config
+22 -22
View File
@@ -37,8 +37,8 @@
#include "esphome/components/captive_portal/captive_portal.h"
#endif
#ifdef USE_IMPROV
#include "esphome/components/esp32_improv/esp32_improv_component.h"
#ifdef USE_IMPROV_BLE
#include "esphome/components/improv_ble/improv_ble_component.h"
#endif
#ifdef USE_IMPROV_SERIAL
@@ -226,7 +226,7 @@ bool CompactString::operator==(const StringRef &other) const {
/// ┌──────────────────────────────────────────────────────────────────────┐
/// │ Captive Portal / Improv Mode (AP active, scanning disabled) │
/// ├──────────────────────────────────────────────────────────────────────┤
/// │ When captive_portal or esp32_improv is active, WiFi scanning is │
/// │ When captive_portal or improv_ble is active, WiFi scanning is
/// │ disabled because it disrupts AP clients (radio leaves AP channel │
/// │ to hop through other channels, causing client disconnections). │
/// │ │
@@ -478,9 +478,9 @@ bool WiFiComponent::needs_full_scan_results_() const {
}
#endif
#ifdef USE_IMPROV
#ifdef USE_IMPROV_BLE
// BLE improv also needs results during provisioning
if (esp32_improv::global_improv_component != nullptr && esp32_improv::global_improv_component->is_active()) {
if (improv_ble::global_improv_component != nullptr && improv_ble::global_improv_component->is_active()) {
return true;
}
#endif
@@ -746,10 +746,10 @@ void WiFiComponent::start() {
#endif
#endif // USE_WIFI_AP
}
#ifdef USE_IMPROV
if (!this->has_sta() && esp32_improv::global_improv_component != nullptr) {
#ifdef USE_IMPROV_BLE
if (!this->has_sta() && improv_ble::global_improv_component != nullptr) {
if (this->wifi_mode_(true, {}))
esp32_improv::global_improv_component->start();
improv_ble::global_improv_component->start();
}
#endif
this->wifi_apply_hostname_();
@@ -805,7 +805,7 @@ void WiFiComponent::loop() {
break;
}
// Use longer cooldown when captive portal/improv is active to avoid disrupting user config
bool portal_active = this->is_captive_portal_active_() || this->is_esp32_improv_active_();
bool portal_active = this->is_captive_portal_active_() || this->is_improv_ble_active_();
uint32_t cooldown_duration = portal_active ? WIFI_COOLDOWN_WITH_AP_ACTIVE_MS : WIFI_COOLDOWN_DURATION_MS;
if (now - this->action_started_ > cooldown_duration) {
// After cooldown we either restarted the adapter because of
@@ -894,12 +894,12 @@ void WiFiComponent::loop() {
}
#endif // USE_WIFI_AP
#ifdef USE_IMPROV
if (esp32_improv::global_improv_component != nullptr && !esp32_improv::global_improv_component->is_active() &&
!esp32_improv::global_improv_component->should_start()) {
if (now - this->last_connected_ > esp32_improv::global_improv_component->get_wifi_timeout()) {
#ifdef USE_IMPROV_BLE
if (improv_ble::global_improv_component != nullptr && !improv_ble::global_improv_component->is_active() &&
!improv_ble::global_improv_component->should_start()) {
if (now - this->last_connected_ > improv_ble::global_improv_component->get_wifi_timeout()) {
if (this->wifi_mode_(true, {}))
esp32_improv::global_improv_component->start();
improv_ble::global_improv_component->start();
}
}
@@ -1644,9 +1644,9 @@ void WiFiComponent::check_connecting_finished(uint32_t now) {
ESP_LOGD(TAG, "Disabling AP");
this->wifi_mode_({}, false);
}
#ifdef USE_IMPROV
if (this->is_esp32_improv_active_()) {
esp32_improv::global_improv_component->stop();
#ifdef USE_IMPROV_BLE
if (this->is_improv_ble_active_()) {
improv_ble::global_improv_component->stop();
}
#endif
@@ -1878,7 +1878,7 @@ WiFiRetryPhase WiFiComponent::determine_next_phase_() {
return WiFiRetryPhase::RETRY_HIDDEN;
}
// Need to scan for captive portal
} else if (this->is_esp32_improv_active_()) {
} else if (this->is_improv_ble_active_()) {
// Improv doesn't need scan results
return WiFiRetryPhase::RETRY_HIDDEN;
}
@@ -1969,7 +1969,7 @@ bool WiFiComponent::transition_to_phase_(WiFiRetryPhase new_phase) {
// Skip actual adapter restart if captive portal/improv is active
// This allows state machine to reset num_retried_ and trigger fresh scan
// without disrupting the captive portal/improv connection
if (!this->is_captive_portal_active_() && !this->is_esp32_improv_active_()) {
if (!this->is_captive_portal_active_() && !this->is_improv_ble_active_()) {
this->restart_adapter();
} else {
// Even when skipping full restart, disconnect to clear driver state
@@ -2228,9 +2228,9 @@ bool WiFiComponent::is_captive_portal_active_() {
return false;
#endif
}
bool WiFiComponent::is_esp32_improv_active_() {
#ifdef USE_IMPROV
return esp32_improv::global_improv_component != nullptr && esp32_improv::global_improv_component->is_active();
bool WiFiComponent::is_improv_ble_active_() {
#ifdef USE_IMPROV_BLE
return improv_ble::global_improv_component != nullptr && improv_ble::global_improv_component->is_active();
#else
return false;
#endif
+1 -1
View File
@@ -797,7 +797,7 @@ class WiFiComponent final : public Component {
network::IPAddress wifi_dns_ip_(int num);
bool is_captive_portal_active_();
bool is_esp32_improv_active_();
bool is_improv_ble_active_();
#ifdef USE_WIFI_FAST_CONNECT
bool load_fast_connect_settings_(WiFiAP &params);
+3 -4
View File
@@ -70,7 +70,6 @@
#define USE_ESP32_CAMERA_JPEG_CONVERSION
#define USE_ESP32_HOSTED
#define USE_ESP32_HOSTED_HTTP_UPDATE
#define USE_ESP32_IMPROV_STATE_CALLBACK
#define USE_ESP_NOW_HOSTED
#define USE_EVENT
#define USE_FAN
@@ -83,6 +82,7 @@
#define USE_HTTP_REQUEST_OTA_WATCHDOG_TIMEOUT 8000 // NOLINT
#define USE_I2S_AUDIO_SPDIF_MODE
#define USE_IMAGE
#define USE_IMPROV_BLE_STATE_CALLBACK
#define USE_INFRARED
#define USE_IR_RF
#define USE_JSON
@@ -266,7 +266,7 @@
#define MAX_API_CONNECTIONS 6
// The Improv library is not in the Zephyr tidy environment
#define USE_IMPROV_SERIAL
#define USE_IMPROV_SERIAL_NEXT_URL
#define USE_IMPROV_NEXT_URL
#define USE_MD5
#define USE_NOISE
#define USE_SHA256
@@ -392,8 +392,7 @@
#define USE_ESP32_CAMERA_JPEG_ENCODER
#define USE_HTTP_REQUEST_RESPONSE
#define USE_I2C
#define USE_IMPROV
#define USE_ESP32_IMPROV_NEXT_URL
#define USE_IMPROV_BLE
#define USE_MICROPHONE
#define USE_PSRAM
#define USE_SENDSPIN
+1 -1
View File
@@ -46,7 +46,7 @@ lib_deps =
${common.lib_deps_base}
https://github.com/dudanov/MideaUART.git#eeea6c3e9b4474f067054592b435be1c4e466815 ; midea
esphome/noise-c@0.1.30 ; noise (api, ota)
improv/Improv@1.2.7 ; improv_serial / esp32_improv
improv/Improv@1.2.7 ; improv_serial / improv_ble
kikuchan98/pngle@1.1.0 ; online_image
; Using the repository directly, otherwise ESP-IDF can't use the library
https://github.com/bitbank2/JPEGDEC.git#1.8.4 ; online_image
@@ -9,5 +9,5 @@ wifi:
password: password1
# esp32_ble_server is only auto-loaded here, so it has no services of its own.
esp32_improv:
improv_ble:
authorizer: none
@@ -55,7 +55,7 @@ def test_uuid_is_matches_descriptor_short_strings(uuid16) -> None:
@pytest.mark.parametrize(
("config_file", "required"),
[
# Auto-loaded by esp32_improv only: nothing to find until Improv asks for it
# Auto-loaded by improv_ble only: nothing to find until Improv asks for it
("improv_only.yaml", False),
# The configuration defines a service clients are meant to connect to
("own_service.yaml", True),
@@ -0,0 +1,31 @@
esphome:
name: improv-ble-automations
esp32:
variant: esp32
framework:
type: esp-idf
logger:
wifi:
ssid: MySSID
password: password1
binary_sensor:
- platform: gpio
pin: 0
id: io0_button
output:
- platform: gpio
pin: 2
id: built_in_led
improv_ble:
authorizer: io0_button
status_indicator: built_in_led
on_provisioned:
- logger.log: provisioned
on_provisioning:
- logger.log: provisioning
on_start:
- logger.log: start
on_state:
- logger.log: state
on_stop:
- logger.log: stop
@@ -0,0 +1,12 @@
esphome:
name: improv-ble-esp32
esp32:
variant: esp32
framework:
type: esp-idf
logger:
wifi:
ssid: MySSID
password: password1
improv_ble:
authorizer: none
@@ -0,0 +1,10 @@
esphome:
name: improv-ble-esp8266
esp8266:
board: nodemcuv2
logger:
wifi:
ssid: MySSID
password: password1
improv_ble:
authorizer: none
@@ -0,0 +1,12 @@
esphome:
name: improv-ble-legacy-key
esp32:
variant: esp32
framework:
type: esp-idf
logger:
wifi:
ssid: MySSID
password: password1
esp32_improv:
authorizer: none
@@ -0,0 +1,59 @@
"""improv_ble is platform neutral; only its BLE server backends are not.
Covers the platform gate (BLE_SERVER_BACKENDS) and the esp32_improv alias that
keeps pre-rename configurations working.
"""
from collections.abc import Callable
from pathlib import Path
import pytest
from esphome.config import read_config
from esphome.core import CORE
def test_esp32_generates_component(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
main_cpp = generate_main(component_config_path("esp32.yaml"))
assert "improv_ble::ImprovBLEComponent" in main_cpp
def test_legacy_key_routes_to_improv_ble(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
caplog: pytest.LogCaptureFixture,
) -> None:
main_cpp = generate_main(component_config_path("legacy_key.yaml"))
assert "improv_ble::ImprovBLEComponent" in main_cpp
assert "'esp32_improv:' top-level key is deprecated" in caplog.text
def test_platform_without_ble_server_rejected(
component_config_path: Callable[[str], Path],
capsys: pytest.CaptureFixture[str],
) -> None:
# AUTO_LOAD finds no backend for esp8266 and pulls in improv_base only, so
# the platform gate in CONFIG_SCHEMA is what has to reject the config.
CORE.config_path = component_config_path("esp8266.yaml")
assert read_config({}) is None
assert "only available on" in capsys.readouterr().out
def test_automations_emit_renamed_triggers(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
main_cpp = generate_main(component_config_path("automations.yaml"))
for trigger in (
"ImprovBLEProvisionedTrigger",
"ImprovBLEProvisioningTrigger",
"ImprovBLEStartTrigger",
"ImprovBLEStateTrigger",
"ImprovBLEStoppedTrigger",
):
assert f"improv_ble::{trigger}" in main_cpp
assert "set_authorizer" in main_cpp
assert "set_status_indicator" in main_cpp
@@ -52,7 +52,7 @@ TEST(RpcResponseBuilder, GoldenBytes) {
(std::vector<uint8_t>{0x04, 0x03, 0x02, 'a', 'b', 0xCC}));
}
// esp32_improv calls finish() and build_rpc_response() with no checksum flag,
// improv_ble calls finish() and build_rpc_response() with no checksum flag,
// so the two defaults must agree
TEST(RpcResponseBuilder, DefaultChecksumFlagMatches) {
const std::vector<std::string> urls = {"https://example.com"};
@@ -12,7 +12,7 @@ output:
pin: 2
id: built_in_led
esp32_improv:
improv_ble:
authorizer: io0_button
authorized_duration: 1min
status_indicator: built_in_led
@@ -5,6 +5,6 @@ wifi:
logger:
hardware_uart: UART0
# next_url compiles the USE_IMPROV_SERIAL_NEXT_URL branch and add_next_url_
# next_url compiles the USE_IMPROV_NEXT_URL branch and add_next_url_
improv_serial:
next_url: https://example.com/?device_name={{device_name}}&ip_address={{ip_address}}
@@ -1,6 +1,6 @@
# Exercises the provisioning window: api registers as a provisioning source
# (encryption enabled, no key), the on_timeout automation, and the wifi (AP +
# captive portal) and esp32_improv cross-component guards. improv_serial is
# captive portal) and improv_ble cross-component guards. improv_serial is
# intentionally NOT gated.
provisioning:
timeout: 1min
@@ -26,5 +26,5 @@ binary_sensor:
pin: 0
id: io0_button
esp32_improv:
improv_ble:
authorizer: io0_button