Merge remote-tracking branch 'origin/dev' into esp8266-native-build-spec

This commit is contained in:
J. Nick Koston
2026-09-16 09:29:06 -05:00
330 changed files with 5498 additions and 1330 deletions
+23
View File
@@ -9,6 +9,29 @@ body:
If you have a feature request or enhancement, please [request them here instead][fr].
[fr]: https://github.com/orgs/esphome/discussions
- type: markdown
attributes:
value: |
## Use of AI in bug reports
AI tools are good at carrying out well-defined tasks, but they are not good at troubleshooting.
Please do NOT paste an AI-generated wall of text into the issue template - if the AI hasn't solved
your problem, its wild guesses are not likely to help.
Please DO include your own words and observations, compile/boot logs, and
especially a minimal reproducible example of your YAML configuration that demonstrates the problem.
It is however quite acceptable to use AI to translate your *own* report,
if you aren't a competent English speaker.
If you really think it will be useful to include an AI's analysis, preferably wrap it in a `<details>` block which will be collapsed by default.
If you are using AI to help solve a problem, rather than asking it to speculate about what the problem is,
it can be more useful to ask it to create a step-by-step troubleshooting procedure.
AI is also useful for generating boilerplate code, such as a minimal reproducible example of your YAML
configuration that demonstrates the problem.
Used properly, AI can be a useful tool to help you solve your problem, but don't let it get in the way.
- type: textarea
validations:
required: true
+1 -1
View File
@@ -32,7 +32,7 @@ runs:
# detects the activated venv via ``VIRTUAL_ENV`` so the venv layout
# downstream jobs rely on is preserved.
if: steps.cache-venv.outputs.cache-hit != 'true'
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
with:
enable-cache: true
# Pull request saves land in per-PR scopes nothing else can
+1 -1
View File
@@ -29,7 +29,7 @@ jobs:
- name: Set up uv
# ``--system`` (below) installs into the setup-python interpreter;
# no venv is created or restored by this workflow.
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
with:
enable-cache: true
# Pull-request-only workflow: a save could never be shared and
+3 -3
View File
@@ -49,7 +49,7 @@ jobs:
# detects the activated venv via ``VIRTUAL_ENV`` so downstream jobs
# that ``. venv/bin/activate`` see an identical layout.
if: steps.cache-venv.outputs.cache-hit != 'true'
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
with:
enable-cache: true
# Pull request saves land in per-PR scopes nothing else can
@@ -413,7 +413,7 @@ jobs:
- name: Set up uv
# Only needed on cache miss to populate the venv.
if: steps.cache-venv.outputs.cache-hit != 'true'
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
with:
enable-cache: true
# Pull request saves land in per-PR scopes nothing else can
@@ -1274,7 +1274,7 @@ jobs:
# install step (order-of-magnitude faster on cold boots,
# with its own wheel cache). actions/setup-python still
# provides the interpreter.
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
with:
enable-cache: true
# Pull request saves land in per-PR scopes nothing else can
+2 -2
View File
@@ -56,7 +56,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9
uses: github/codeql-action/init@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
@@ -84,6 +84,6 @@ jobs:
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9
uses: github/codeql-action/analyze@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
with:
category: "/language:${{matrix.language}}"
+1 -1
View File
@@ -47,7 +47,7 @@ jobs:
# setup-python interpreter so subsequent ``prek`` /
# ``script/run-in-env.py`` steps find the deps without a
# ``uv run`` prefix.
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
with:
enable-cache: true
# Pin uv version so the action does not have to fetch the
+1 -1
View File
@@ -10,7 +10,7 @@ ci:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.16.6
rev: v0.16.7
hooks:
# Run the linter.
- id: ruff
+13 -2
View File
@@ -431,7 +431,17 @@ file does, and it is the authority when they disagree. The most useful starting
MyComponent *parent_;
};
```
Register with `@automation.register_action("my_component.do_something", MyAction, schema, synchronous=True)`. Use `synchronous=True` for actions that run to completion inside `play()` without deferring. Use `synchronous=False` if the action may suspend/defer execution (e.g. `delay`, `wait_until`, `script.wait`) or store trigger arguments for later use.
Register it without writing a builder:
```python
automation.register_simple_action(
"my_component.do_something", MyAction, schema, synchronous=True
)
```
The constructor receives the object named by `config[CONF_ID]`. Use `register_bare_action` for a
no-argument constructor, `register_parented_action` for a class deriving from `Parented<T>`, and
the `@automation.register_action(...)` decorator only when the builder must also set fields.
Use `synchronous=True` for actions that run to completion inside `play()` without deferring. Use `synchronous=False` if the action may suspend/defer execution (e.g. `delay`, `wait_until`, `script.wait`) or store trigger arguments for later use.
* **Conditions:**
```cpp
@@ -443,7 +453,8 @@ file does, and it is the authority when they disagree. The most useful starting
MyComponent *parent_;
};
```
Register with `@automation.register_condition("my_component.is_active", MyCondition, schema)`.
Register with `automation.register_simple_condition("my_component.is_active", MyCondition, schema)`;
`register_bare_condition`, `register_parented_condition` and the decorator follow the action rules.
* **Type Hints:** Type-hint all function signatures, including test functions and config validators (e.g. `def validate_x(config: ConfigType) -> ConfigType:`, `def test_x() -> None:`). Import `ConfigType` from `esphome.types`.
+2 -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
@@ -589,6 +589,7 @@ esphome/components/uart/* @esphome/core
esphome/components/uart/button/* @ssieb
esphome/components/uart/event/* @eoasmxd
esphome/components/uart/packet_transport/* @clydebarrow
esphome/components/uart_mux/* @kbx81
esphome/components/udp/* @clydebarrow
esphome/components/ufire_ec/* @pvizeli
esphome/components/ufire_ise/* @pvizeli
+1 -1
View File
@@ -22,7 +22,7 @@ RUN \
-r /requirements.txt
# Install the ESPHome Device Builder dashboard.
RUN uv pip install --no-cache-dir esphome-device-builder==1.14.7
RUN uv pip install --no-cache-dir esphome-device-builder==1.14.9
RUN \
platformio settings set enable_telemetry No \
+99 -28
View File
@@ -102,6 +102,101 @@ def register_condition(name: str, condition_type: MockObjClass, schema: cv.Schem
return CONDITION_REGISTRY.register(name, condition_type, schema)
async def _build_with_parent(
config: ConfigType,
automation_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
parent = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(automation_id, template_arg, parent)
async def _build_without_parent(
config: ConfigType,
automation_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
return cg.new_Pvariable(automation_id, template_arg)
async def _build_parented(
config: ConfigType,
automation_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
var = cg.new_Pvariable(automation_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
return var
def register_simple_action(
name: str,
action_type: MockObjClass,
schema: cv.Schema,
*,
synchronous: bool,
) -> None:
"""Register an action whose constructor takes the object named by ``config[CONF_ID]``.
Use the ``register_action`` decorator instead when the builder must also set fields.
"""
register_action(name, action_type, schema, synchronous=synchronous)(
_build_with_parent
)
def register_simple_condition(
name: str, condition_type: MockObjClass, schema: cv.Schema
) -> None:
"""Condition counterpart of ``register_simple_action``."""
register_condition(name, condition_type, schema)(_build_with_parent)
def register_bare_action(
name: str,
action_type: MockObjClass,
schema: cv.Schema,
*,
synchronous: bool,
) -> None:
"""Register an action whose constructor takes no arguments."""
register_action(name, action_type, schema, synchronous=synchronous)(
_build_without_parent
)
def register_bare_condition(
name: str, condition_type: MockObjClass, schema: cv.Schema
) -> None:
"""Condition counterpart of ``register_bare_action``."""
register_condition(name, condition_type, schema)(_build_without_parent)
def register_parented_action(
name: str,
action_type: MockObjClass,
schema: cv.Schema,
*,
synchronous: bool,
) -> None:
"""Register an action deriving from ``Parented<T>``.
The object is constructed without arguments and ``set_parent()`` receives the object
named by ``config[CONF_ID]``.
"""
register_action(name, action_type, schema, synchronous=synchronous)(_build_parented)
def register_parented_condition(
name: str, condition_type: MockObjClass, schema: cv.Schema
) -> None:
"""Condition counterpart of ``register_parented_action``."""
register_condition(name, condition_type, schema)(_build_parented)
Action = cg.esphome_ns.class_("Action")
Trigger = cg.esphome_ns.class_("Trigger")
ACTION_REGISTRY = Registry()
@@ -534,44 +629,20 @@ async def lambda_action_to_code(
return new_lambda_pvariable(action_id, lambda_, StatelessLambdaAction, template_arg)
@register_action(
register_simple_action(
"component.update",
UpdateComponentAction,
maybe_simple_id(
{
cv.Required(CONF_ID): cv.use_id(cg.PollingComponent),
}
),
maybe_simple_id({cv.Required(CONF_ID): cv.use_id(cg.PollingComponent)}),
synchronous=True,
)
async def component_update_action_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
comp = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, comp)
@register_action(
register_simple_action(
"component.suspend",
SuspendComponentAction,
maybe_simple_id(
{
cv.Required(CONF_ID): cv.use_id(cg.PollingComponent),
}
),
maybe_simple_id({cv.Required(CONF_ID): cv.use_id(cg.PollingComponent)}),
synchronous=True,
)
async def component_suspend_action_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
comp = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, comp)
@register_action(
+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"),
}
+1 -1
View File
@@ -94,7 +94,7 @@ class ADCSensor final : public sensor::Sensor, public PollingComponent, public v
/// - SamplingMode::MIN: Use the lowest sample value
/// - SamplingMode::MAX: Use the highest sample value
/// @param sampling_mode The desired sampling mode to use for aggregating ADC samples.
void set_sampling_mode(SamplingMode sampling_mode);
void set_sampling_mode(SamplingMode sampling_mode) { this->sampling_mode_ = sampling_mode; }
/// Perform a single ADC sampling operation and return the measured value.
/// This function handles raw readings, calibration, and averaging as needed.
@@ -76,6 +76,4 @@ void ADCSensor::set_sample_count(uint8_t sample_count) {
}
}
void ADCSensor::set_sampling_mode(SamplingMode sampling_mode) { this->sampling_mode_ = sampling_mode; }
} // namespace esphome::adc
@@ -138,11 +138,11 @@ class AlarmControlPanel : public EntityBase {
// in order to store last panel state in flash
ESPPreferenceObject pref_;
// current state
AlarmControlPanelState current_state_;
AlarmControlPanelState current_state_{ACP_STATE_DISARMED};
// the desired (or previous) state
AlarmControlPanelState desired_state_;
AlarmControlPanelState desired_state_{ACP_STATE_DISARMED};
// last time the state was updated
uint32_t last_update_;
uint32_t last_update_{0};
// the call control function
virtual void control(const AlarmControlPanelCall &call) = 0;
// state callback - passes the new state to listeners
+20 -9
View File
@@ -136,6 +136,12 @@ CONF_LISTEN_BACKLOG = "listen_backlog"
CONF_MAX_SEND_QUEUE = "max_send_queue"
CONF_STATE_SUBSCRIPTION_ONLY = "state_subscription_only"
# Schema defaults that also match the C++ initializers in api_server.h; codegen
# skips the setter when the config equals them.
DEFAULT_PORT = 6053
DEFAULT_REBOOT_TIMEOUT = "15min"
DEFAULT_BATCH_DELAY = "100ms"
def _register_provisioning_source(config: ConfigType) -> ConfigType:
"""Register the API as a provisioning source when encryption is enabled.
@@ -292,7 +298,7 @@ CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(APIServer),
cv.Optional(CONF_PORT, default=6053): cv.port,
cv.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
# Removed in 2026.1.0 - kept to provide helpful error message
cv.Optional(CONF_PASSWORD): cv.invalid(
"The 'password' option has been removed in ESPHome 2026.1.0.\n"
@@ -305,14 +311,14 @@ CONFIG_SCHEMA = cv.All(
"Or visit https://esphome.io/components/api/#configuration-variables"
),
cv.Optional(
CONF_REBOOT_TIMEOUT, default="15min"
CONF_REBOOT_TIMEOUT, default=DEFAULT_REBOOT_TIMEOUT
): cv.positive_time_period_milliseconds,
cv.Exclusive(
CONF_SERVICES, group_of_exclusion=CONF_ACTIONS
): ACTIONS_SCHEMA,
cv.Exclusive(CONF_ACTIONS, group_of_exclusion=CONF_ACTIONS): ACTIONS_SCHEMA,
cv.Optional(CONF_ENCRYPTION): encryption_schema,
cv.Optional(CONF_BATCH_DELAY, default="100ms"): cv.All(
cv.Optional(CONF_BATCH_DELAY, default=DEFAULT_BATCH_DELAY): cv.All(
cv.positive_time_period_milliseconds,
cv.Range(max=cv.TimePeriod(milliseconds=65535)),
),
@@ -350,10 +356,9 @@ CONFIG_SCHEMA = cv.All(
ln882x=5, # Moderate RAM
nrf52=4, # ~256KB RAM, BSD sockets, Thread (single HA controller)
): cv.int_range(min=1, max=20),
# Maximum queued send buffers per connection before dropping connection
# Each buffer uses ~8-12 bytes overhead plus actual message size
# Max queued messages per connection, and 2 KB of backlog per slot up
# to 64 KB (a lone message is exempt), before the connection is dropped
# Platform defaults based on available RAM and typical message rates:
# CONF_MAX_SEND_QUEUE defaults are power of 2 for efficient modulo
cv.SplitDefault(
CONF_MAX_SEND_QUEUE,
esp8266=4, # Limited RAM, need to fail fast
@@ -463,9 +468,15 @@ async def to_code(config: ConfigType) -> None:
# Request a log listener slot for API log streaming
request_log_listener()
cg.add(var.set_port(config[CONF_PORT]))
cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT]))
cg.add(var.set_batch_delay(config[CONF_BATCH_DELAY]))
# Skip the setters when the config matches the C++ initializers (DEFAULT_*).
if (port := config[CONF_PORT]) != DEFAULT_PORT:
cg.add(var.set_port(port))
if (reboot_timeout := config[CONF_REBOOT_TIMEOUT]) != cv.time_period(
DEFAULT_REBOOT_TIMEOUT
):
cg.add(var.set_reboot_timeout(reboot_timeout))
if (batch_delay := config[CONF_BATCH_DELAY]) != cv.time_period(DEFAULT_BATCH_DELAY):
cg.add(var.set_batch_delay(batch_delay))
if CONF_LISTEN_BACKLOG in config:
cg.add(var.set_listen_backlog(config[CONF_LISTEN_BACKLOG]))
cg.add_define("MAX_API_CONNECTIONS", config[CONF_MAX_CONNECTIONS])
+26 -9
View File
@@ -1,20 +1,37 @@
#include "api_buffer.h"
#include <new>
#ifdef ESPHOME_DEBUG_API
#include "esphome/core/log.h"
#endif
namespace esphome::api {
#ifdef ESPHOME_DEBUG_API
void APIBuffer::debug_check_drop_(size_t drop) const {
if (drop > this->size_) {
ESP_LOGE("api.buffer", "drop_front: drop=%zu size=%u", drop, this->size_);
abort();
}
}
#endif
bool APIBuffer::grow_(size_t n) {
// nothrow (no zero-fill) so OOM is reportable; plain new aborts instead
// (NEW_OOM_ABORT on ESP8266 Arduino, exception stub on ESP-IDF).
// RAMAllocator is no fit here: unique_ptr needs delete[]-compatible memory.
std::unique_ptr<uint8_t[]> new_data(new (std::nothrow) uint8_t[n]);
if (new_data == nullptr)
if (n > MAX_SIZE)
return false;
if (this->size_)
std::memcpy(new_data.get(), this->data_.get(), this->size_);
this->data_ = std::move(new_data);
// realloc extends in place when it can, avoiding the copy
uint8_t *grown = RAMAllocator<uint8_t>().reallocate(this->data_.get(), n);
if (grown == nullptr)
return false;
(void) this->data_.release(); // realloc already freed or reused the old block
this->data_.reset(grown);
this->capacity_ = n;
return true;
}
uint8_t *APIBuffer::append(size_t n) {
const size_t old_size = this->size_;
if (!this->resize(old_size + n))
return nullptr;
return this->data_.get() + old_size;
}
} // namespace esphome::api
+20 -4
View File
@@ -25,6 +25,7 @@ namespace esphome::api {
/// writes in debug builds.
class APIBuffer {
public:
static constexpr size_t MAX_SIZE = UINT16_MAX; // API frames carry 16 bit lengths
void clear() { this->size_ = 0; }
/// Returns false if allocation fails; the buffer is left unchanged.
[[nodiscard]] inline bool reserve(size_t n) ESPHOME_ALWAYS_INLINE { return n <= this->capacity_ || this->grow_(n); }
@@ -36,9 +37,19 @@ class APIBuffer {
[[nodiscard]] inline bool reserve_and_resize(size_t reserve_size, size_t new_size) ESPHOME_ALWAYS_INLINE {
if (!this->reserve(std::max(reserve_size, new_size)))
return false;
this->size_ = new_size;
this->size_ = static_cast<uint16_t>(new_size);
return true;
}
/// Grow by n bytes; returns the new bytes, or nullptr on allocation failure.
[[nodiscard]] uint8_t *append(size_t n);
/// Drop the first `drop` bytes, sliding the rest down. Precondition: drop <= size().
void drop_front(size_t drop) {
#ifdef ESPHOME_DEBUG_API
this->debug_check_drop_(drop);
#endif
this->size_ -= drop;
std::memmove(this->data_.get(), this->data_.get() + drop, this->size_);
}
uint8_t *data() { return this->data_.get(); }
const uint8_t *data() const { return this->data_.get(); }
size_t size() const { return this->size_; }
@@ -55,9 +66,14 @@ class APIBuffer {
protected:
bool grow_(size_t n);
std::unique_ptr<uint8_t[]> data_;
size_t size_{0};
size_t capacity_{0};
#ifdef ESPHOME_DEBUG_API
void debug_check_drop_(size_t drop) const;
#endif
// RAMAllocator: PSRAM when available, and it reports failure where
// new (std::nothrow) still aborts on ESP-IDF without exceptions
RAMUniquePtr<uint8_t[]> data_;
uint16_t size_{0};
uint16_t capacity_{0};
};
} // namespace esphome::api
+4 -1
View File
@@ -364,7 +364,10 @@ void APIConnection::check_keepalive_(uint32_t now) {
ESP_LOGVV(TAG, "Sending keepalive PING");
PingRequest req;
this->flags_.sent_ping = this->send_message(req);
if (!this->flags_.sent_ping) {
if (this->flags_.sent_ping) {
// Quiet for a keepalive period and the ping is on its way: a one-off stall's storage can go
this->helper_->release_overflow_buffer();
} else {
// If we can't send the ping request directly (tx_buffer full),
// schedule it at the front of the batch so it will be sent with priority
ESP_LOGW(TAG, "Buffer full, ping queued");
+1 -1
View File
@@ -171,7 +171,7 @@ APIError APIFrameHelper::write_raw_iov_(const struct iovec *iov, int iovcnt, uin
return APIError::OK;
// Queue unsent data into overflow buffer
if (!this->overflow_buf_.enqueue_iov(iov, iovcnt, total_write_len, static_cast<uint16_t>(sent))) {
if (!this->overflow_buf_.enqueue_iov(iov, iovcnt, total_write_len, sent)) {
HELPER_LOG("Overflow buffer full or out of memory, dropping connection");
this->state_ = State::FAILED;
return APIError::SOCKET_WRITE_FAILED;
@@ -219,7 +219,10 @@ class APIFrameHelper {
if (this->rx_buf_len_ == 0) {
this->rx_buf_.release();
}
this->release_overflow_buffer();
}
// Free the send backlog storage once it has drained
void release_overflow_buffer() { this->overflow_buf_.release(); }
protected:
// Drain backlogged overflow data to the socket and handle errors.
@@ -67,15 +67,15 @@ APIError APINoiseFrameHelper::init() {
}
// init prologue
size_t old_size = prologue_.size();
if (!prologue_.resize(old_size + PROLOGUE_INIT_LEN)) [[unlikely]] {
uint8_t *dst = prologue_.append(PROLOGUE_INIT_LEN);
if (dst == nullptr) [[unlikely]] {
state_ = State::FAILED;
return APIError::OUT_OF_MEMORY;
}
#ifdef USE_ESP8266
memcpy_P(prologue_.data() + old_size, PROLOGUE_INIT, PROLOGUE_INIT_LEN);
memcpy_P(dst, PROLOGUE_INIT, PROLOGUE_INIT_LEN);
#else
std::memcpy(prologue_.data() + old_size, PROLOGUE_INIT, PROLOGUE_INIT_LEN);
std::memcpy(dst, PROLOGUE_INIT, PROLOGUE_INIT_LEN);
#endif
state_ = State::CLIENT_HELLO;
@@ -272,17 +272,17 @@ APIError APINoiseFrameHelper::state_action_client_hello_() {
return handle_handshake_frame_error_(aerr);
}
// ignore contents, may be used in future for flags
// Resize for: existing prologue + 2 size bytes + frame data
size_t old_size = this->prologue_.size();
// Append 2 size bytes + frame data to the prologue
size_t rx_size = this->rx_buf_.size();
if (!this->prologue_.resize(old_size + 2 + rx_size)) [[unlikely]] {
uint8_t *dst = this->prologue_.append(2 + rx_size);
if (dst == nullptr) [[unlikely]] {
state_ = State::FAILED;
return APIError::OUT_OF_MEMORY;
}
this->prologue_[old_size] = (uint8_t) (rx_size >> 8);
this->prologue_[old_size + 1] = (uint8_t) rx_size;
dst[0] = (uint8_t) (rx_size >> 8);
dst[1] = (uint8_t) rx_size;
if (rx_size > 0) {
std::memcpy(this->prologue_.data() + old_size + 2, this->rx_buf_.data(), rx_size);
std::memcpy(dst + 2, this->rx_buf_.data(), rx_size);
}
state_ = State::SERVER_HELLO;
+57 -64
View File
@@ -1,98 +1,91 @@
#include "api_overflow_buffer.h"
#ifdef USE_API
#include <cstring>
#include <new>
namespace esphome::api {
APIOverflowBuffer::~APIOverflowBuffer() {
for (auto *entry : this->queue_) {
if (entry != nullptr)
Entry::destroy(entry);
}
}
ssize_t APIOverflowBuffer::try_drain(socket::Socket *socket) {
// socket->write() can re-enter this function: a log message emitted from an
// lwip callback during the write goes out over the API and lands back in the
// frame helper's write/drain path. If a nested drain ran here it would send
// and free the entry the outer drain is still holding, causing a double free.
// Report "no progress" instead; the outer drain keeps draining, and the
// nested send is enqueued behind the existing backlog.
// Nested call from inside socket->write(); see draining_
if (this->draining_)
return 0;
// RAII so the flag is cleared on every return path
struct DrainGuard {
explicit DrainGuard(bool &flag) : flag_(flag) { flag_ = true; }
~DrainGuard() { this->flag_ = false; }
bool &flag_;
} guard(this->draining_);
APIOverflowBuffer &owner;
~DrainGuard() { this->owner.draining_ = false; }
} guard{*this};
this->draining_ = true;
while (this->count_ > 0) {
Entry *front = this->queue_[this->head_];
uint8_t *msg = this->buf_.data() + this->head_;
size_t len = msg[0] | (msg[1] << 8);
ssize_t sent = socket->write(front->current_data(), front->remaining());
if (sent <= 0) {
// -1 = error (caller checks errno for EWOULDBLOCK vs hard error)
// 0 = nothing sent (treat as no progress)
ssize_t sent = socket->write(msg + LEN_PREFIX, len);
if (sent <= 0)
return sent;
if (static_cast<size_t>(sent) < len) {
// Step past the sent bytes and rewrite the prefix there; it lands on bytes already sent
this->head_ += sent;
len -= sent;
msg += sent;
msg[0] = len;
msg[1] = len >> 8;
return sent;
}
if (static_cast<uint16_t>(sent) < front->remaining()) {
// Partially sent, update offset and stop
front->offset += static_cast<uint16_t>(sent);
return sent;
}
// Entry fully sent — unlink it before freeing so a freed pointer is never
// reachable from the queue
this->queue_[this->head_] = nullptr;
this->head_ = (this->head_ + 1) % API_MAX_SEND_QUEUE;
this->head_ += LEN_PREFIX + len;
this->count_--;
Entry::destroy(front);
}
return 0; // All drained
this->head_ = 0;
if (this->release_when_drained_) {
this->release_when_drained_ = false;
this->buf_.release();
} else {
this->buf_.clear();
}
return 0;
}
bool APIOverflowBuffer::enqueue_iov(const struct iovec *iov, int iovcnt, uint16_t total_len, uint16_t skip) {
bool APIOverflowBuffer::enqueue_iov(const struct iovec *iov, int iovcnt, size_t total_len, size_t skip) {
if (this->count_ >= API_MAX_SEND_QUEUE)
return false;
uint16_t buffer_size = total_len - skip;
// nothrow: a failed allocation returns nullptr so the connection is dropped
// cleanly instead of plain new's crash or abort on OOM
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory)
auto *data = new (std::nothrow) uint8_t[buffer_size];
if (data == nullptr)
return false;
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory)
auto *entry = new (std::nothrow) Entry{data, buffer_size, 0};
if (entry == nullptr) {
delete[] data;
const size_t new_len = total_len - skip;
const size_t new_bytes = LEN_PREFIX + new_len;
const size_t live = this->buf_.size() - this->head_;
// A lone message is only bound by the buffer; refusing it would just drop the connection
if (live + new_bytes > (this->count_ > 0 ? MAX_BYTES : MAX_LONE_BYTES))
return false;
if (this->buf_.size() + new_bytes > this->buf_.capacity()) {
// Storage would move under an outer drain's write()
if (this->draining_)
return false;
if (this->head_ > 0) {
// Reclaim the sent prefix before growing
this->buf_.drop_front(this->head_);
this->head_ = 0;
}
if (!this->buf_.reserve(reserve_for(live + new_bytes)))
return false;
}
uint16_t to_skip = skip;
uint16_t write_pos = 0;
for (int i = 0; i < iovcnt; i++) {
if (to_skip >= iov[i].iov_len) {
to_skip -= static_cast<uint16_t>(iov[i].iov_len);
uint8_t *dst = this->buf_.append(new_bytes);
if (dst == nullptr)
return false;
dst[0] = new_len;
dst[1] = new_len >> 8;
dst += LEN_PREFIX;
for (const struct iovec *end = iov + iovcnt; iov != end; iov++) {
if (skip >= iov->iov_len) {
skip -= iov->iov_len;
} else {
const uint8_t *src = reinterpret_cast<uint8_t *>(iov[i].iov_base) + to_skip;
uint16_t len = static_cast<uint16_t>(iov[i].iov_len) - to_skip;
std::memcpy(entry->data + write_pos, src, len);
write_pos += len;
to_skip = 0;
const size_t len = iov->iov_len - skip;
std::memcpy(dst, static_cast<const uint8_t *>(iov->iov_base) + skip, len);
dst += len;
skip = 0;
}
}
// Publish only after the copy completes so a half-built entry is never reachable
this->queue_[this->tail_] = entry;
this->tail_ = (this->tail_ + 1) % API_MAX_SEND_QUEUE;
this->count_++;
return true;
}
+40 -53
View File
@@ -1,5 +1,6 @@
#pragma once
#include <array>
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <sys/types.h>
@@ -8,71 +9,57 @@
#include "esphome/components/socket/headers.h"
#include "esphome/components/socket/socket.h"
#include "esphome/core/helpers.h"
#include "api_buffer.h"
namespace esphome::api {
/// Circular queue of heap-allocated byte buffers used as a TCP send backlog.
///
/// Under normal operation this buffer is **never used** — data goes straight
/// from the frame helper to the socket. It only fills when the LWIP TCP
/// send buffer is full (slow client, congested network, heavy logging).
/// The queue drains automatically on subsequent write/loop calls once the
/// socket becomes writable again.
///
/// Capacity is compile-time-fixed via API_MAX_SEND_QUEUE (set from Python
/// config). If the queue fills completely the connection is marked failed.
/// TCP send backlog, only used when the socket send buffer is full.
/// One contiguous buffer per connection, allocated on the first stall and
/// kept at its high-water mark so a lossy link does not churn the heap.
/// Messages are stored as a 2 byte length prefix plus payload.
/// API_MAX_SEND_QUEUE bounds queued messages and, at 2 KB per slot, queued
/// bytes; exceeding either fails the connection.
class APIOverflowBuffer {
public:
/// A single heap-allocated send-backlog entry.
/// Lifetime is manually managed — see destroy().
struct Entry {
uint8_t *data;
uint16_t size; // Total size of the buffer
uint16_t offset; // Current send offset within the buffer
uint16_t remaining() const { return this->size - this->offset; }
const uint8_t *current_data() const { return this->data + this->offset; }
/// Free this entry and its data buffer.
static ESPHOME_ALWAYS_INLINE void destroy(Entry *entry) {
delete[] entry->data;
delete entry; // NOLINT(cppcoreguidelines-owning-memory)
}
};
~APIOverflowBuffer();
/// True when no backlogged data is waiting.
bool empty() const { return this->count_ == 0; }
/// True when the queue has no room for another entry.
bool full() const { return this->count_ >= API_MAX_SEND_QUEUE; }
/// Number of entries currently queued.
uint8_t count() const { return this->count_; }
/// Try to drain queued data to the socket.
/// Returns bytes-written > 0 on success/partial, 0 if all drained or no progress,
/// -1 on error (caller must check errno to distinguish EWOULDBLOCK from hard errors).
/// Callers only need to act on -1; 0 and positive values both mean "no error".
/// Frees entries as they are fully sent.
/// Drain queued messages to the socket.
/// Returns bytes written, 0 for a re-entrant call, -1 on error (check errno
/// for EWOULDBLOCK); callers only need to act on -1.
ssize_t try_drain(socket::Socket *socket);
/// Enqueue unsent IOV data into the backlog.
/// Copies iov data starting at byte offset `skip` into a new entry.
/// Returns false if the queue is full or allocation fails (caller should fail the connection).
bool enqueue_iov(const struct iovec *iov, int iovcnt, uint16_t total_len, uint16_t skip);
/// Queue iov data from byte offset `skip` as one message.
/// Returns false when a limit is hit, allocation fails, or storage would move
/// during a drain; the caller should fail the connection.
bool enqueue_iov(const struct iovec *iov, int iovcnt, size_t total_len, size_t skip);
/// Free the retained storage, now if empty, otherwise once it has drained.
void release() {
if (this->count_ == 0) {
this->buf_.release();
} else {
this->release_when_drained_ = true;
}
}
protected:
std::array<Entry *, API_MAX_SEND_QUEUE> queue_{};
uint8_t head_{0};
uint8_t tail_{0};
static constexpr size_t LEN_PREFIX = 2;
static constexpr size_t BYTES_PER_SLOT = 2048;
// Reserve in 256 byte steps so a creeping high-water mark settles quickly
static constexpr size_t GROW_QUANTUM = 256;
// Lone message ceiling, rounded down so reserve_for() never exceeds the buffer limit
static constexpr size_t MAX_LONE_BYTES = APIBuffer::MAX_SIZE & ~(GROW_QUANTUM - 1);
static constexpr size_t MAX_BYTES = std::min(API_MAX_SEND_QUEUE * BYTES_PER_SLOT, MAX_LONE_BYTES);
static constexpr size_t reserve_for(size_t want) { return (want + GROW_QUANTUM - 1) & ~(GROW_QUANTUM - 1); }
APIBuffer buf_;
uint16_t head_{0}; // offset of the front message's length prefix; bytes before it are sent
uint8_t count_{0};
// Guards against re-entrant drains: socket->write() can re-enter the API
// send path (e.g. a log message emitted from an lwip callback), and a nested
// drain would free the entry the outer drain is still holding.
bool draining_{false};
// socket->write() can re-enter the send path (log from an lwip callback):
// a nested drain makes no progress and a nested enqueue never moves storage
bool draining_ : 1 {false};
bool release_when_drained_ : 1 {false};
};
} // namespace esphome::api
+3 -3
View File
@@ -314,7 +314,7 @@ class APIServer final : public Component,
#endif
// 4-byte aligned types
uint32_t reboot_timeout_{300000};
uint32_t reboot_timeout_{900000}; // Keep in sync with DEFAULT_REBOOT_TIMEOUT in __init__.py
uint32_t last_connected_{0};
// Slots [0, api_connection_count_) are populated; trailing slots are always nullptr.
@@ -351,8 +351,8 @@ class APIServer final : public Component,
#endif
// Group smaller types together
uint16_t port_{6053};
uint16_t batch_delay_{100};
uint16_t port_{6053}; // Keep in sync with DEFAULT_PORT in __init__.py
uint16_t batch_delay_{100}; // Keep in sync with DEFAULT_BATCH_DELAY in __init__.py
// Connection limits - these defaults will be overridden by config values
// from cv.SplitDefault in __init__.py which sets platform-specific defaults.
uint8_t listen_backlog_{4};
+1 -4
View File
@@ -339,10 +339,7 @@ async def to_code(config: ConfigType) -> None:
# HTTPS streams verify the server against the root certificate bundle
require_certificate_bundle()
add_idf_component(
name="esphome/esp-audio-libs",
ref="3.2.1",
)
add_idf_component(name="esphome/esp-audio-libs", ref="4.0.0")
data = _get_data()
@@ -203,16 +203,10 @@ void BangBangClimate::set_away_config(const BangBangClimateTargetTempConfig &awa
this->away_config_ = away_config;
}
void BangBangClimate::set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; }
void BangBangClimate::set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; }
Trigger<> *BangBangClimate::get_idle_trigger() { return &this->idle_trigger_; }
Trigger<> *BangBangClimate::get_cool_trigger() { return &this->cool_trigger_; }
Trigger<> *BangBangClimate::get_heat_trigger() { return &this->heat_trigger_; }
void BangBangClimate::set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; }
void BangBangClimate::set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; }
void BangBangClimate::dump_config() {
LOG_CLIMATE("", "Bang Bang Climate", this);
ESP_LOGCONFIG(TAG,
@@ -22,10 +22,10 @@ class BangBangClimate final : public climate::Climate, public Component {
void setup() override;
void dump_config() override;
void set_sensor(sensor::Sensor *sensor);
void set_humidity_sensor(sensor::Sensor *humidity_sensor);
void set_supports_cool(bool supports_cool);
void set_supports_heat(bool supports_heat);
void set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; }
void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; }
void set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; }
void set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; }
void set_normal_config(const BangBangClimateTargetTempConfig &normal_config);
void set_away_config(const BangBangClimateTargetTempConfig &away_config);
+5 -1
View File
@@ -39,6 +39,7 @@ from esphome.const import (
DEVICE_CLASS_EMPTY,
DEVICE_CLASS_GARAGE_DOOR,
DEVICE_CLASS_GAS,
DEVICE_CLASS_GLASS_BREAK,
DEVICE_CLASS_HEAT,
DEVICE_CLASS_LIGHT,
DEVICE_CLASS_LOCK,
@@ -81,6 +82,7 @@ DEVICE_CLASSES = [
DEVICE_CLASS_EMPTY,
DEVICE_CLASS_GARAGE_DOOR,
DEVICE_CLASS_GAS,
DEVICE_CLASS_GLASS_BREAK,
DEVICE_CLASS_HEAT,
DEVICE_CLASS_LIGHT,
DEVICE_CLASS_LOCK,
@@ -452,7 +454,9 @@ _BINARY_SENSOR_SCHEMA = (
cv.Optional(
CONF_DEVICE_CLASS, visibility=cv.Visibility.ADVANCED
): validate_device_class,
cv.Optional(CONF_FILTERS): validate_filters,
cv.Optional(
CONF_FILTERS, visibility=cv.Visibility.ADVANCED
): validate_filters,
cv.Optional(CONF_ON_PRESS): automation.validate_automation({}),
cv.Optional(CONF_ON_RELEASE): automation.validate_automation({}),
cv.Optional(CONF_ON_CLICK): cv.All(
@@ -32,7 +32,8 @@ void log_binary_sensor(const char *tag, const char *prefix, const char *type, Bi
*/
class BinarySensor : public StatefulEntityBase<bool> {
public:
explicit BinarySensor() = default;
// User provided, not "= default": `new(p) BinarySensor()` would zero-fill .bss that is already zero.
explicit BinarySensor() {}
const bool &get_state() const override { return this->state; }
void set_trigger_on_initial_state(bool value) { this->trigger_on_initial_state_ = value; }
@@ -53,6 +53,9 @@ class DelayedOnOffFilter final : public Filter {
class DelayedOnFilter : public Filter {
public:
// User provided, not "= default": `new(p) DelayedOnFilter()` would zero-fill .bss that is already zero.
DelayedOnFilter() {}
optional<bool> new_value(bool value) override;
template<typename T> void set_delay(T delay) { this->delay_ = delay; }
@@ -63,6 +66,9 @@ class DelayedOnFilter : public Filter {
class DelayedOffFilter : public Filter {
public:
// User provided, not "= default": `new(p) DelayedOffFilter()` would zero-fill .bss that is already zero.
DelayedOffFilter() {}
optional<bool> new_value(bool value) override;
template<typename T> void set_delay(T delay) { this->delay_ = delay; }
@@ -143,6 +149,8 @@ class StatelessLambdaFilter : public Filter {
class SettleFilter : public Filter {
public:
// User provided, not "= default": `new(p) SettleFilter()` would zero-fill .bss that is already zero.
SettleFilter() {}
optional<bool> new_value(bool value) override;
template<typename T> void set_delay(T delay) { this->delay_ = delay; }
+10 -1
View File
@@ -23,7 +23,7 @@ public ble_api.h.
import logging
import esphome.codegen as cg
from esphome.components import libretiny
from esphome.components import libretiny, wifi
from esphome.components.libretiny.const import (
FAMILY_BK7231N,
FAMILY_BK7231Q,
@@ -84,6 +84,15 @@ def _final_validate(config: ConfigType) -> None:
# which run on a BLE 4.2 board. The hard error is raised at codegen.
if msg := _unsupported_family_message(libretiny.get_libretiny_family()):
_LOGGER.warning("%s (this configuration cannot compile)", msg)
# Any wifi power_save_mode other than NONE also arms the Beken SDK's MCU
# sleep. With the BLE controller running, that sleep never wakes up once the
# station is stopped (adapter restart after failed roams, wifi.disable): the
# device is dead until a power cycle (esphome#18592). Keep power save off
# until LibreTiny ships the SDK-side fix (libretiny-eu/libretiny#414).
wifi.force_power_save_off(
"with BLE running, the Beken SDK's MCU sleep halts the device once the "
"station is stopped (https://github.com/esphome/esphome/issues/18592)"
)
FINAL_VALIDATE_SCHEMA = _final_validate
@@ -45,15 +45,7 @@ void BluedroidGattClient::setup() {
void BluedroidGattClient::loop() {
if (!esp32_ble::global_ble->is_active()) {
// Stack down: no CLOSE_EVT will come. Settle a live link so the consumer
// frees its slot, then re-register the app on the next enable.
auto down_st = this->state();
if (down_st != ClientState::IDLE && down_st != ClientState::INIT) {
this->release_services();
this->set_idle_();
this->listener_->on_connection_state(false, 0, ble_device_base::GATT_ERR_NOT_CONNECTED);
}
this->set_state(ClientState::INIT);
// ble_before_disabled_event_handler() settles the slot.
return;
}
auto st = this->state();
@@ -65,7 +57,7 @@ void BluedroidGattClient::loop() {
ESP_LOGE(TAG, "gattc app register failed: app_id=%d code=%d", this->app_id, ret);
this->mark_failed();
}
// Do not wait for REG_EVT; a dropped event must not wedge the slot.
// Do not wait for REG_EVT; connect() rejects until it lands.
this->set_idle_();
} else if (st == ClientState::DISCONNECTING || this->disconnect_pending()) {
// The one teardown safety net: a lost CLOSE_EVT, or a scheduled
@@ -78,8 +70,8 @@ void BluedroidGattClient::loop() {
this->listener_->on_connection_state(false, 0, ESP_GATT_CONN_TIMEOUT);
}
} else {
// The loop stays on while a link exists (stack-down watch, pre-started
// search flush); it settles only back at IDLE.
// The loop stays on while a link exists (pre-started search flush); it
// settles only back at IDLE.
this->deliver_pending_search_();
if (this->state() == ClientState::IDLE) {
this->disable_loop();
@@ -87,6 +79,22 @@ void BluedroidGattClient::loop() {
}
}
// Stack down: no CLOSE_EVT will come. Settle a live link so the consumer
// frees its slot, then register the app again on the next enable.
void BluedroidGattClient::ble_before_disabled_event_handler() {
auto st = this->state();
if (st != ClientState::IDLE && st != ClientState::INIT) {
this->release_services();
this->set_idle_();
this->listener_->on_connection_state(false, 0, ble_device_base::GATT_ERR_NOT_CONNECTED);
}
// The interface belongs to the torn-down stack.
this->gattc_if_ = ESP_GATT_IF_NONE;
this->set_state(ClientState::INIT);
// An idle slot runs no loop; the INIT branch must run to register again.
this->enable_loop();
}
void BluedroidGattClient::dump_config() {
ESP_LOGCONFIG(TAG, "Bluedroid GATT client %d", this->connection_index_);
if (this->is_failed()) {
@@ -97,6 +105,11 @@ void BluedroidGattClient::dump_config() {
// ---- contract ops ----
int BluedroidGattClient::connect(uint64_t address, uint8_t addr_type) {
if (this->gattc_if_ == ESP_GATT_IF_NONE) {
// Bluedroid drops an open on an unknown interface without any event.
ESP_LOGW(TAG, "[%d] Connect rejected, GATT app not registered", this->connection_index_);
return ble_device_base::GATT_ERR_NOT_CONNECTED;
}
// Only from idle: clobbering DISCONNECTING would open a new link the
// stale CLOSE_EVT then tears down.
if (this->state() != ClientState::IDLE) {
@@ -31,6 +31,9 @@ class BluetoothConnection;
// void disconnect() cannot overload with an int-returning twin.
class BluedroidGattClient final : public esp32_ble_tracker::ESPBTClient, public Component {
public:
// User provided, not "= default": `new(p) BluedroidGattClient()` would zero-fill .bss that is already zero.
BluedroidGattClient() {}
static constexpr uint16_t UNSET_CONN_ID = 0xFFFF;
// Lifecycle of one connection attempt's service search.
@@ -56,6 +59,7 @@ class BluedroidGattClient final : public esp32_ble_tracker::ESPBTClient, public
void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override;
void connect() override;
void disconnect() override;
void ble_before_disabled_event_handler() override;
bool wants_parsed_advertisements() override { return false; }
void on_scan_end() override {}
bool parse_device(const ble_device_base::ESPBTDevice &device) override { return false; }
@@ -37,6 +37,9 @@ enum class PendingAck : uint8_t {
class BluetoothConnection final : public ble_device_base::GattClientListener {
public:
// User provided, not "= default": `new(p) BluetoothConnection()` would zero-fill .bss that is already zero.
BluetoothConnection() {}
/// Wire the platform backend. Called from codegen before setup.
void set_backend(ble_device_base::BLEGattConnection *backend) {
this->backend_ = backend;
@@ -395,6 +395,17 @@ async def _to_code_ble_hub(config: ConfigType) -> None:
await _connections_to_code(var, config)
def enable_advertisement_filter() -> None:
"""Compile the advertisement filter hook into bluetooth_proxy.
Called by external filtering components from to_code(). The define behind
this is an implementation detail; do not emit it directly.
Public API for external components. Do not remove.
"""
cg.add_define("USE_BLUETOOTH_PROXY_ADVERTISEMENT_FILTER")
async def to_code(config: ConfigType) -> None:
if CORE.is_esp32:
await _to_code_esp32(config)
@@ -94,6 +94,15 @@ void BluetoothProxy::on_raw_advertisement_(const ble_device_base::RawAdvertiseme
if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr)
return;
#ifdef USE_BLUETOOTH_PROXY_ADVERTISEMENT_FILTER
// Ask the filter before the packet is queued, so a dropped advertisement never
// reaches the batch or the network.
if (this->advertisement_filter_.is_set() && !this->advertisement_filter_.should_forward(raw)) {
ESP_LOGVV(TAG, "Filtered packet from %012" PRIX64, raw.address);
return;
}
#endif
auto &adv = this->response_.advertisements[this->response_.advertisements_len];
adv.address = raw.address;
adv.rssi = raw.rssi;
@@ -184,6 +193,9 @@ void BluetoothProxy::dump_config() {
" Adapter MAC: %s",
scan_mode, mac_out);
#endif
#ifdef USE_BLUETOOTH_PROXY_ADVERTISEMENT_FILTER
ESP_LOGCONFIG(TAG, " Advertisement filter: %s", YESNO(this->advertisement_filter_.is_set()));
#endif
}
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
@@ -97,6 +97,29 @@ static_assert(pending_reply_round_trips(0xABCD112233445566ULL, 0x000011223344556
static_assert(PendingReply{}.empty());
#endif
#ifdef USE_BLUETOOTH_PROXY_ADVERTISEMENT_FILTER
/// Predicate slot letting an external component drop advertisements before they
/// are queued for the API. Same shape as
/// ble_device_base::RawAdvertisementCallback. Runs on the advertisement hot
/// path, so it must be cheap and must not block.
///
/// Usage:
/// proxy->set_advertisement_filter({this, [](void *self, const ble_device_base::RawAdvertisement &adv) {
/// return static_cast<MyFilter *>(self)->should_forward(adv);
/// }});
///
/// Returning false drops the advertisement. Not called at all while the API is
/// disconnected, which matters to a stateful filter. Compiled in only when an
/// external component calls bluetooth_proxy.enable_advertisement_filter().
struct AdvertisementFilter {
void *instance{nullptr};
bool (*fn)(void *instance, const ble_device_base::RawAdvertisement &adv){nullptr};
/// A default-constructed slot is "no filter"; the proxy guards on this.
bool is_set() const { return this->fn != nullptr; }
bool should_forward(const ble_device_base::RawAdvertisement &adv) const { return this->fn(this->instance, adv); }
};
#endif // USE_BLUETOOTH_PROXY_ADVERTISEMENT_FILTER
class BluetoothProxy final : public Component {
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
// Allow the connection to update connections_free_response_
@@ -162,6 +185,11 @@ class BluetoothProxy final : public Component {
void set_active(bool active) { this->active_ = active; }
bool has_active() { return this->active_; }
#ifdef USE_BLUETOOTH_PROXY_ADVERTISEMENT_FILTER
/// One subscriber; a later call replaces an earlier one.
void set_advertisement_filter(AdvertisementFilter filter) { this->advertisement_filter_ = filter; }
#endif
uint32_t get_legacy_version() const {
if (!this->active_) {
return LEGACY_PASSIVE_ONLY_VERSION;
@@ -330,6 +358,10 @@ class BluetoothProxy final : public Component {
// start on an even word, closing two alignment holes.
uint32_t last_advertisement_flush_time_{0};
#ifdef USE_BLUETOOTH_PROXY_ADVERTISEMENT_FILTER
AdvertisementFilter advertisement_filter_{};
#endif
// BLE advertisement batching
api::BluetoothLERawAdvertisementsResponse response_;
@@ -341,7 +341,6 @@ void BME280Component::set_pressure_oversampling(BME280Oversampling pressure_over
void BME280Component::set_humidity_oversampling(BME280Oversampling humidity_over_sampling) {
this->humidity_oversampling_ = humidity_over_sampling;
}
void BME280Component::set_iir_filter(BME280IIRFilter iir_filter) { this->iir_filter_ = iir_filter; }
uint8_t BME280Component::read_u8_(uint8_t a_register) {
uint8_t data = 0;
this->read_byte(a_register, &data);
+1 -1
View File
@@ -69,7 +69,7 @@ class BME280Component : public PollingComponent {
/// Set the oversampling value for the humidity sensor. Default is 16x.
void set_humidity_oversampling(BME280Oversampling humidity_over_sampling);
/// Set the IIR Filter used to increase accuracy, defaults to no IIR Filter.
void set_iir_filter(BME280IIRFilter iir_filter);
void set_iir_filter(BME280IIRFilter iir_filter) { this->iir_filter_ = iir_filter; }
// ========== INTERNAL METHODS ==========
// (In most use cases you won't need these)
-1
View File
@@ -503,7 +503,6 @@ void BME680Component::set_pressure_oversampling(BME680Oversampling pressure_over
void BME680Component::set_humidity_oversampling(BME680Oversampling humidity_oversampling) {
this->humidity_oversampling_ = humidity_oversampling;
}
void BME680Component::set_iir_filter(BME680IIRFilter iir_filter) { this->iir_filter_ = iir_filter; }
void BME680Component::set_heater(uint16_t heater_temperature, uint16_t heater_duration) {
this->heater_temperature_ = heater_temperature;
this->heater_duration_ = heater_duration;
+1 -1
View File
@@ -74,7 +74,7 @@ class BME680Component final : public PollingComponent, public i2c::I2CDevice {
/// Set the humidity oversampling value. Defaults to 16X.
void set_humidity_oversampling(BME680Oversampling humidity_oversampling);
/// Set the IIR Filter value. Defaults to no IIR Filter.
void set_iir_filter(BME680IIRFilter iir_filter);
void set_iir_filter(BME680IIRFilter iir_filter) { this->iir_filter_ = iir_filter; }
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
void set_pressure_sensor(sensor::Sensor *pressure_sensor) { pressure_sensor_ = pressure_sensor; }
@@ -254,7 +254,6 @@ void BMP280Component::set_temperature_oversampling(BMP280Oversampling temperatur
void BMP280Component::set_pressure_oversampling(BMP280Oversampling pressure_over_sampling) {
this->pressure_oversampling_ = pressure_over_sampling;
}
void BMP280Component::set_iir_filter(BMP280IIRFilter iir_filter) { this->iir_filter_ = iir_filter; }
uint8_t BMP280Component::read_u8_(uint8_t a_register) {
uint8_t data = 0;
this->bmp_read_byte(a_register, &data);
+1 -1
View File
@@ -59,7 +59,7 @@ class BMP280Component : public PollingComponent {
/// Set the oversampling value for the pressure sensor. Default is 16x.
void set_pressure_oversampling(BMP280Oversampling pressure_over_sampling);
/// Set the IIR Filter used to increase accuracy, defaults to no IIR Filter.
void set_iir_filter(BMP280IIRFilter iir_filter);
void set_iir_filter(BMP280IIRFilter iir_filter) { this->iir_filter_ = iir_filter; }
void setup() override;
void dump_config() override;
@@ -30,6 +30,7 @@ class CDCACMUARTBridge final : public Component {
void set_line_coding();
void set_line_state(bool dtr, bool rts);
uart::IDFUARTComponent *get_uart_parent() const { return this->uart_parent_; }
/**
* Stop forwarding in both directions and hand the UART back to its configured
+22 -13
View File
@@ -83,18 +83,23 @@ void ESP32BLE::setup() {
}
}
void ESP32BLE::enable() {
if (this->state_ != BLE_COMPONENT_STATE_DISABLED)
return;
this->state_ = BLE_COMPONENT_STATE_ENABLE;
}
void ESP32BLE::disable() {
if (this->state_ == BLE_COMPONENT_STATE_DISABLED)
return;
this->state_ = BLE_COMPONENT_STATE_DISABLE;
// Queue the transition for loop(). A pending transition the other way is
// cancelled instead, since nothing was torn down or brought up yet; any other
// state is already there or on its way.
void ESP32BLE::request_state_(bool enable) {
if (enable) {
if (this->state_ == BLE_COMPONENT_STATE_DISABLED) {
this->state_ = BLE_COMPONENT_STATE_ENABLE;
} else if (this->state_ == BLE_COMPONENT_STATE_DISABLE) {
this->state_ = BLE_COMPONENT_STATE_ACTIVE;
}
} else {
if (this->state_ == BLE_COMPONENT_STATE_ACTIVE) {
this->state_ = BLE_COMPONENT_STATE_DISABLE;
} else if (this->state_ == BLE_COMPONENT_STATE_ENABLE) {
this->state_ = BLE_COMPONENT_STATE_DISABLED;
}
}
}
#ifdef USE_ESP32_BLE_ADVERTISING
@@ -580,7 +585,11 @@ void ESP32BLE::loop_handle_state_transition_not_active_() {
this->mark_failed();
return;
}
this->state_ = BLE_COMPONENT_STATE_DISABLED;
this->drain_ble_events_();
// A status callback may have asked for BLE back; the stack is down now, so
// that request becomes a bring-up.
this->state_ =
this->state_ == BLE_COMPONENT_STATE_ACTIVE ? BLE_COMPONENT_STATE_ENABLE : BLE_COMPONENT_STATE_DISABLED;
} else if (this->state_ == BLE_COMPONENT_STATE_ENABLE) {
ESP_LOGD(TAG, "Enabling");
this->state_ = BLE_COMPONENT_STATE_OFF;
+11 -2
View File
@@ -102,8 +102,8 @@ class ESP32BLE final : public Component {
}
uint32_t get_advertising_cycle_time() const { return this->advertising_cycle_time_; }
void enable();
void disable();
void enable() { this->request_state_(true); }
void disable() { this->request_state_(false); }
ESPHOME_ALWAYS_INLINE bool is_active() { return this->state_ == BLE_COMPONENT_STATE_ACTIVE; }
void setup() override;
void loop() override;
@@ -176,6 +176,15 @@ class ESP32BLE final : public Component {
bool ble_setup_();
bool ble_dismantle_();
void request_state_(bool enable);
// Drop what the old stack queued; the next stack reuses the same interface ids.
void drain_ble_events_() {
BLEEvent *ble_event;
while ((ble_event = this->ble_events_.pop()) != nullptr) {
this->ble_event_pool_.release(ble_event);
}
this->ble_events_.get_and_reset_dropped_count();
}
bool ble_pre_setup_();
#ifdef USE_ESP32_BLE_ADVERTISING
void advertising_init_();
@@ -42,7 +42,7 @@ void BLEClientBase::set_state(espbt::ClientState st) {
void BLEClientBase::loop() {
if (!esp32_ble::global_ble->is_active()) {
this->set_state(espbt::ClientState::INIT);
// ble_before_disabled_event_handler() resets the client.
return;
}
if (this->state() == espbt::ClientState::INIT) {
@@ -72,6 +72,21 @@ void BLEClientBase::loop() {
float BLEClientBase::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; }
void BLEClientBase::ble_before_disabled_event_handler() {
auto st = this->state();
if (st != espbt::ClientState::IDLE && st != espbt::ClientState::INIT) {
// No CLOSE_EVT will come: free the services and settle the link.
this->release_services();
this->set_idle_();
this->on_disconnect_complete(ESP_GATT_CONN_TERMINATE_LOCAL_HOST);
}
// The interface belongs to the torn-down stack.
this->gattc_if_ = ESP_GATT_IF_NONE;
this->set_state(espbt::ClientState::INIT);
// An idle client runs no loop; the INIT branch must run to register again.
this->enable_loop();
}
void BLEClientBase::dump_config() {
ESP_LOGCONFIG(TAG,
" Address: %s\n"
@@ -93,6 +108,10 @@ bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) {
return false;
if (this->state() != espbt::ClientState::IDLE)
return false;
// Not registered on this stack yet; promoting now would stop the scan for a
// connect that connect() rejects anyway.
if (this->gattc_if_ == ESP_GATT_IF_NONE)
return false;
this->log_event_("Found device");
if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG)
@@ -117,6 +136,15 @@ void BLEClientBase::connect() {
this->connection_index_, this->address_str_);
return;
}
if (this->gattc_if_ == ESP_GATT_IF_NONE) {
// Bluedroid drops an open on an unknown interface without any event.
this->log_warning_("Connect rejected, GATT app not registered");
// INIT stays so loop() still registers; only a promoted client goes back.
if (this->state() == espbt::ClientState::DISCOVERED) {
this->set_state(espbt::ClientState::IDLE);
}
return;
}
ESP_LOGI(TAG, "[%d] [%s] 0x%02x Connecting", this->connection_index_, this->address_str_, this->remote_addr_type_);
this->paired_ = false;
// A registration whose event never arrived must not block this connection's release.
@@ -199,7 +227,10 @@ void BLEClientBase::release_services() {
#ifndef CONFIG_BT_GATTC_CACHE_NVS_FLASH
// Only the cache clean makes the stack's database unsafe to walk.
this->services_released_ = true;
esp_ble_gattc_cache_clean(this->remote_bda_);
// A stack on its way down frees its own cache.
if (esp32_ble::global_ble->is_active()) {
esp_ble_gattc_cache_clean(this->remote_bda_);
}
#endif
}
@@ -41,6 +41,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
void connect() override;
esp_err_t pair();
void disconnect() override;
void ble_before_disabled_event_handler() override;
void unconditional_disconnect();
void release_services();
@@ -114,7 +115,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
#endif
// Group 3: 4-byte types
int gattc_if_;
int gattc_if_{ESP_GATT_IF_NONE};
esp_gatt_status_t status_{ESP_GATT_OK};
// Group 4: Arrays
@@ -139,7 +140,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
uint8_t pending_notify_regs_{0};
bool auto_connect_{false};
bool paired_{false};
// Set only when release_services() cleans the stack's GATT cache, which no API may then walk
// Set by release_services() on RAM-cache builds; the stack's GATT database must not be walked after it
bool services_released_{false};
// 8 bytes used, no padding
@@ -155,10 +156,11 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
void log_connection_params_(const char *param_type);
void handle_connection_result_(esp_err_t ret);
/// Hook called once a connection has been fully torn down (after release_services() and
/// set_idle_()), from both the CLOSE_EVT handler and the DISCONNECTING safety timeout.
/// set_idle_()): CLOSE_EVT, the DISCONNECTING safety timeout, or the BLE stack going down.
/// Subclasses with extra per-connection accounting (e.g. bluetooth_proxy slot state)
/// override this to release that state. `reason` is the controller reason code, or
/// ESP_GATT_CONN_TIMEOUT for the safety-timeout path.
/// override this to release that state. `reason` is the controller reason code,
/// ESP_GATT_CONN_TIMEOUT for the safety timeout, or ESP_GATT_CONN_TERMINATE_LOCAL_HOST
/// for the stack going down.
virtual void on_disconnect_complete(esp_err_t reason) {}
/// Transition to IDLE and reset conn_id — call when the connection is fully dead.
void set_idle_() {
@@ -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; }
@@ -62,6 +62,11 @@ void ESP32BLETracker::on_ota_global_state(ota::OTAState state, float progress, u
for (auto *client : this->clients_) {
client->disconnect();
}
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
// The OTA transfer blocks the main loop, so the revert in loop() cannot run. No
// active-connection gate here: every client was just told to disconnect.
this->update_coex_preference_(false);
#endif
#endif
} else if ((state == ota::OTA_ERROR || state == ota::OTA_ABORT) && this->scan_continuous_before_ota_) {
this->scan_continuous_before_ota_ = false;
@@ -74,11 +79,11 @@ void ESP32BLETracker::on_ota_global_state(ota::OTAState state, float progress, u
void ESP32BLETracker::loop() {
if (!this->parent_->is_active()) {
this->ble_was_disabled_ = true;
return;
} else if (this->ble_was_disabled_) {
}
if (this->ble_was_disabled_) {
this->ble_was_disabled_ = false;
// If the BLE stack was disabled, we need to start the scan again.
// First start after boot or after the stack came back.
if (this->scan_continuous_) {
this->start_scan();
}
@@ -218,7 +223,27 @@ void ESP32BLETracker::stop_scan() {
this->stop_scan_();
}
void ESP32BLETracker::ble_before_disabled_event_handler() { this->stop_scan_(); }
void ESP32BLETracker::ble_before_disabled_event_handler() {
// Tell the controller to stop; a scan still starting has nothing to stop yet.
if (this->scanner_state_ == ScannerState::RUNNING || this->scanner_state_ == ScannerState::FAILED) {
this->stop_scan_();
}
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
for (auto *client : this->clients_) {
client->ble_before_disabled_event_handler();
}
this->skip_next_scan_end_ = false;
#endif
// The stop above never completes (stack torn down, events dropped); settle
// here so start_scan_() sees IDLE once the stack is back.
if (this->scanner_state_ != ScannerState::IDLE) {
this->cleanup_scan_state_(true);
}
// A failure latched by the old stack must not be handled against the next.
this->scan_start_failed_ = ESP_BT_STATUS_SUCCESS;
this->scan_set_param_failed_ = ESP_BT_STATUS_SUCCESS;
this->ble_was_disabled_ = true;
}
bool ESP32BLETracker::stop_scan_() {
if (this->scanner_state_ != ScannerState::RUNNING && this->scanner_state_ != ScannerState::FAILED) {
@@ -113,6 +113,9 @@ class ESPBTClient : public ESPBTDeviceListener {
virtual void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) = 0;
virtual void connect() = 0;
virtual void disconnect() = 0;
/// Called right before the BLE stack is dismantled. Nothing in flight will
/// complete, and the GATT app must register again once the stack is back.
virtual void ble_before_disabled_event_handler() {}
bool disconnect_pending() const { return this->want_disconnect_; }
void cancel_pending_disconnect() { this->want_disconnect_ = false; }
@@ -132,7 +135,7 @@ class ESPBTClient : public ESPBTDeviceListener {
void set_tracker_state_version(uint8_t *version) { this->tracker_state_version_ = version; }
// Memory optimized layout
uint8_t app_id; // App IDs are small integers assigned sequentially
uint8_t app_id{0}; // App IDs are small integers assigned sequentially
protected:
/// Set state without IDLE handling - use for direct state transitions.
@@ -433,25 +433,10 @@ void ESP32Camera::set_pixel_format(ESP32CameraPixelFormat format) {
}
}
void ESP32Camera::set_jpeg_quality(uint8_t quality) { this->config_.jpeg_quality = quality; }
void ESP32Camera::set_vertical_flip(bool vertical_flip) { this->vertical_flip_ = vertical_flip; }
void ESP32Camera::set_horizontal_mirror(bool horizontal_mirror) { this->horizontal_mirror_ = horizontal_mirror; }
void ESP32Camera::set_contrast(int contrast) { this->contrast_ = contrast; }
void ESP32Camera::set_brightness(int brightness) { this->brightness_ = brightness; }
void ESP32Camera::set_saturation(int saturation) { this->saturation_ = saturation; }
void ESP32Camera::set_special_effect(ESP32SpecialEffect effect) { this->special_effect_ = effect; }
/* set exposure parameters */
void ESP32Camera::set_aec_mode(ESP32GainControlMode mode) { this->aec_mode_ = mode; }
void ESP32Camera::set_aec2(bool aec2) { this->aec2_ = aec2; }
void ESP32Camera::set_ae_level(int ae_level) { this->ae_level_ = ae_level; }
void ESP32Camera::set_aec_value(uint32_t aec_value) { this->aec_value_ = aec_value; }
/* set gains parameters */
void ESP32Camera::set_agc_mode(ESP32GainControlMode mode) { this->agc_mode_ = mode; }
void ESP32Camera::set_agc_value(uint8_t agc_value) { this->agc_value_ = agc_value; }
void ESP32Camera::set_agc_gain_ceiling(ESP32AgcGainCeiling gain_ceiling) { this->agc_gain_ceiling_ = gain_ceiling; }
/* set white balance */
void ESP32Camera::set_wb_mode(ESP32WhiteBalanceMode mode) { this->wb_mode_ = mode; }
/* set test mode */
void ESP32Camera::set_test_pattern(bool test_pattern) { this->test_pattern_ = test_pattern; }
/* set fps */
void ESP32Camera::set_max_update_interval(uint32_t max_update_interval) {
this->max_update_interval_ = max_update_interval;
+15 -15
View File
@@ -140,25 +140,25 @@ class ESP32Camera final : public camera::Camera {
void set_pixel_format(ESP32CameraPixelFormat format);
void set_frame_size(ESP32CameraFrameSize size);
void set_jpeg_quality(uint8_t quality);
void set_vertical_flip(bool vertical_flip);
void set_horizontal_mirror(bool horizontal_mirror);
void set_contrast(int contrast);
void set_brightness(int brightness);
void set_saturation(int saturation);
void set_special_effect(ESP32SpecialEffect effect);
void set_vertical_flip(bool vertical_flip) { this->vertical_flip_ = vertical_flip; }
void set_horizontal_mirror(bool horizontal_mirror) { this->horizontal_mirror_ = horizontal_mirror; }
void set_contrast(int contrast) { this->contrast_ = contrast; }
void set_brightness(int brightness) { this->brightness_ = brightness; }
void set_saturation(int saturation) { this->saturation_ = saturation; }
void set_special_effect(ESP32SpecialEffect effect) { this->special_effect_ = effect; }
/* -- exposure */
void set_aec_mode(ESP32GainControlMode mode);
void set_aec2(bool aec2);
void set_ae_level(int ae_level);
void set_aec_value(uint32_t aec_value);
void set_aec_mode(ESP32GainControlMode mode) { this->aec_mode_ = mode; }
void set_aec2(bool aec2) { this->aec2_ = aec2; }
void set_ae_level(int ae_level) { this->ae_level_ = ae_level; }
void set_aec_value(uint32_t aec_value) { this->aec_value_ = aec_value; }
/* -- gains */
void set_agc_mode(ESP32GainControlMode mode);
void set_agc_value(uint8_t agc_value);
void set_agc_gain_ceiling(ESP32AgcGainCeiling gain_ceiling);
void set_agc_mode(ESP32GainControlMode mode) { this->agc_mode_ = mode; }
void set_agc_value(uint8_t agc_value) { this->agc_value_ = agc_value; }
void set_agc_gain_ceiling(ESP32AgcGainCeiling gain_ceiling) { this->agc_gain_ceiling_ = gain_ceiling; }
/* -- white balance */
void set_wb_mode(ESP32WhiteBalanceMode mode);
void set_wb_mode(ESP32WhiteBalanceMode mode) { this->wb_mode_ = mode; }
/* -- test */
void set_test_pattern(bool test_pattern);
void set_test_pattern(bool test_pattern) { this->test_pattern_ = test_pattern; }
/* -- framerates */
void set_max_update_interval(uint32_t max_update_interval);
void set_idle_update_interval(uint32_t idle_update_interval);
+5 -2
View File
@@ -11,6 +11,9 @@ namespace esphome::esp8266_pwm {
class ESP8266PWM final : public output::FloatOutput, public Component {
public:
// User provided, not "= default": `new(p) ESP8266PWM()` would zero-fill .bss that is already zero.
ESP8266PWM() {}
void set_pin(InternalGPIOPin *pin) { pin_ = pin; }
void set_frequency(float frequency) { this->frequency_ = frequency; }
/// Dynamically update frequency
@@ -28,8 +31,8 @@ class ESP8266PWM final : public output::FloatOutput, public Component {
protected:
void write_state(float state) override;
InternalGPIOPin *pin_;
float frequency_{1000.0};
InternalGPIOPin *pin_{nullptr};
float frequency_{1000.0}; // Keep in sync with DEFAULT_FREQUENCY in output.py
/// Cache last output level for dynamic frequency updating
float last_output_{0.0};
};
+8 -2
View File
@@ -22,6 +22,10 @@ ESP8266PWM = esp8266_pwm_ns.class_("ESP8266PWM", output.FloatOutput, cg.Componen
SetFrequencyAction = esp8266_pwm_ns.class_("SetFrequencyAction", automation.Action)
validate_frequency = cv.All(cv.frequency, cv.float_range(min=1.0e-6))
# Schema default that also matches the C++ initializer in esp8266_pwm.h; codegen
# skips the setter when the config equals it.
DEFAULT_FREQUENCY = 1000.0
CONFIG_SCHEMA = cv.All(
output.FLOAT_OUTPUT_SCHEMA.extend(
{
@@ -29,7 +33,7 @@ CONFIG_SCHEMA = cv.All(
cv.Required(CONF_PIN): cv.All(
pins.internal_gpio_output_pin_schema, valid_pwm_pin
),
cv.Optional(CONF_FREQUENCY, default="1kHz"): validate_frequency,
cv.Optional(CONF_FREQUENCY, default=DEFAULT_FREQUENCY): validate_frequency,
}
).extend(cv.COMPONENT_SCHEMA),
cv.require_framework_version(
@@ -48,7 +52,9 @@ async def to_code(config: ConfigType) -> None:
pin = await cg.gpio_pin_expression(config[CONF_PIN])
cg.add(var.set_pin(pin))
cg.add(var.set_frequency(config[CONF_FREQUENCY]))
# Skip the setter when the config matches the C++ initializer (DEFAULT_FREQUENCY).
if (frequency := config[CONF_FREQUENCY]) != DEFAULT_FREQUENCY:
cg.add(var.set_frequency(frequency))
@automation.register_action(
@@ -842,7 +842,14 @@ bool ESPHomeOTAComponent::handle_auth_send_() {
const size_t hex_size = hasher.get_size() * 2;
const size_t nonce_len = hasher.get_size() / 4;
const size_t auth_buf_size = 1 + 3 * hex_size;
this->auth_buf_ = std::make_unique<uint8_t[]>(auth_buf_size);
// Internal RAM first: 128 of these bytes go straight into the hardware SHA engine
this->auth_buf_ =
RAMAllocator<uint8_t>(RAMAllocator<uint8_t>::PREFER_INTERNAL).make_unique_array_for_overwrite(auth_buf_size);
if (!this->auth_buf_) {
this->log_auth_warning_(LOG_STR("No memory"));
this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_UNKNOWN);
return false;
}
this->auth_buf_pos_ = 0;
char *buf = reinterpret_cast<char *>(this->auth_buf_.get() + 1);
+2 -2
View File
@@ -145,13 +145,13 @@ class ESPHomeOTAComponent final : public ota::OTAComponent {
#ifdef USE_OTA_PASSWORD
std::string password_;
std::unique_ptr<uint8_t[]> auth_buf_;
RAMUniquePtr<uint8_t[]> auth_buf_;
#endif // USE_OTA_PASSWORD
#ifdef USE_OTA_ENCRYPTION
#ifndef USE_OTA_ENCRYPTION_FROM_API
noise::NoiseContext noise_ctx_;
#endif
std::unique_ptr<NoiseSession> noise_;
RAMUniquePtr<NoiseSession> noise_;
#endif // USE_OTA_ENCRYPTION
socket::ListenSocket *server_{nullptr};
@@ -7,7 +7,6 @@
#include "esphome/core/log.h"
#include <cstring>
#include <new>
#ifdef USE_ESP8266
#include <pgmspace.h>
@@ -43,9 +42,8 @@ ESPHomeOTAComponent::NoiseSession::~NoiseSession() {
bool ESPHomeOTAComponent::noise_start_session_(uint8_t server_feature_flags) {
// A provisioned key cleared between the offer and here is not guarded: the
// session runs on the zero key load_psk fills in and fails the client's MAC.
// Default-init: the frame buffer is written before it is read
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks)
this->noise_ = std::unique_ptr<NoiseSession>(new (std::nothrow) NoiseSession);
// Default placement, PSRAM first where present: the session only lives for one upload
this->noise_ = RAMAllocator<NoiseSession>().make_unique();
static constexpr size_t PROLOGUE_ACK_LEN = 2; // OTA_RESPONSE_OK + version
static constexpr size_t PROLOGUE_CLIENT_FEATURES_LEN = 1;
static constexpr size_t PROLOGUE_FEATURE_ACK_LEN = 2; // OTA_RESPONSE_FEATURE_FLAGS + server flags
+3 -1
View File
@@ -420,7 +420,9 @@ def _validate(config: ConfigType) -> ConfigType:
BASE_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(EthernetComponent),
cv.Optional(CONF_MANUAL_IP): MANUAL_IP_SCHEMA,
cv.Optional(
CONF_MANUAL_IP, visibility=cv.Visibility.ADVANCED
): MANUAL_IP_SCHEMA,
cv.Optional(CONF_DOMAIN, default=".local"): cv.domain_name,
cv.Optional(CONF_USE_ADDRESS): cv.string_strict,
cv.Optional(CONF_MAC_ADDRESS): cv.mac_address,
@@ -6,17 +6,21 @@
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <cstring>
#include <new>
namespace esphome::ethernet {
namespace {
// Per-device context returned by init() and handed back to read/write/deinit.
// Context returned by init() and handed back to read/write/deinit. There is one W5500 per device, so a
// single static instance replaces a heap allocation that could fail. It is always clear when init() runs:
// esp_eth_mac_new_w5500() calls deinit() on every failure after init() succeeded, and nothing else
// uninstalls the driver
struct W5500CustomSpiContext {
spi_device_handle_t handle;
SemaphoreHandle_t lock;
};
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) - intentional mutable state
W5500CustomSpiContext w5500_context{};
// Transfers up to the ESP32 SPI hardware FIFO size (64 bytes) stay on the polling path; larger
// transfers (the frame payloads) use the blocking, DMA-backed transmit.
@@ -25,23 +29,20 @@ constexpr uint32_t W5500_SPI_LOCK_TIMEOUT_MS = 50;
void *w5500_custom_spi_init(const void *spi_config) {
const auto *config = static_cast<const eth_w5500_config_t *>(spi_config);
auto *ctx = new (std::nothrow) W5500CustomSpiContext{};
if (ctx == nullptr) {
return nullptr;
}
auto *ctx = &w5500_context;
// The W5500 SPI frame carries the 16-bit address in the command phase and the 8-bit control
// byte in the address phase; mirror what the stock driver configures.
spi_device_interface_config_t devcfg = *config->spi_devcfg;
devcfg.command_bits = 16;
devcfg.address_bits = 8;
if (spi_bus_add_device(config->spi_host_id, &devcfg, &ctx->handle) != ESP_OK) {
delete ctx;
ctx->handle = nullptr;
return nullptr;
}
ctx->lock = xSemaphoreCreateMutex();
if (ctx->lock == nullptr) {
spi_bus_remove_device(ctx->handle);
delete ctx;
ctx->handle = nullptr;
return nullptr;
}
return ctx;
@@ -51,7 +52,7 @@ esp_err_t w5500_custom_spi_deinit(void *spi_ctx) {
auto *ctx = static_cast<W5500CustomSpiContext *>(spi_ctx);
spi_bus_remove_device(ctx->handle);
vSemaphoreDelete(ctx->lock);
delete ctx;
*ctx = {};
return ESP_OK;
}
@@ -13,6 +13,9 @@ class IPAddressEthernetInfo final : public Component,
public text_sensor::TextSensor,
public ethernet::EthernetIPStateListener {
public:
// User provided, not "= default": `new(p) IPAddressEthernetInfo()` would zero-fill .bss that is already zero.
IPAddressEthernetInfo() {}
void setup() override;
void dump_config() override;
void add_ip_sensors(uint8_t index, text_sensor::TextSensor *s) { this->ip_sensors_[index] = s; }
+1 -1
View File
@@ -183,7 +183,7 @@ class Fan : public EntityBase {
LazyCallbackManager<void()> state_callback_{};
ESPPreferenceObject rtc_;
FanRestoreMode restore_mode_;
FanRestoreMode restore_mode_{FanRestoreMode::NO_RESTORE};
private:
/// Lazy-allocate preset modes vector (never freed — entity lives forever).
+12 -10
View File
@@ -42,7 +42,7 @@ from esphome.const import (
CONF_TYPE,
CONF_URL,
)
from esphome.core import CORE, HexInt
from esphome.core import HexInt
from esphome.cpp_generator import MockObj, MockObjClass
from esphome.external_files import RemoteFile
from esphome.types import ConfigType
@@ -76,16 +76,18 @@ def compute_local_image_path(value: str | ConfigType) -> Path:
return external_files.compute_local_file_path(DOMAIN, url)
def local_path(value: str | ConfigType) -> str:
value = value[CONF_PATH] if isinstance(value, dict) else value
return str(CORE.relative_config_path(value))
def local_path(value: Path | ConfigType) -> Path:
# cv.file_ has already resolved the path against the config dir.
return value[CONF_PATH] if isinstance(value, dict) else value
def download_file(url: str, path: Path) -> str:
def download_file(url: str, path: Path) -> Path:
# The shared NETWORK_TIMEOUT applies; a per-caller timeout would be
# silently ignored on a per-run memo hit anyway (memos key by path).
external_files.download_content(url, path)
return str(path)
# Keep the Path: config-hash normalizes Path values under the data dir,
# which a str would dump verbatim and break the CLI/add-on comparison.
return path
def _gh_svg_url_path(mdi_id: str, source: str) -> tuple[str, Path]:
@@ -93,13 +95,13 @@ def _gh_svg_url_path(mdi_id: str, source: str) -> tuple[str, Path]:
return MDI_SOURCES[source] + mdi_id + ".svg", base_dir / f"{mdi_id}.svg"
def download_gh_svg(value: str | ConfigType, source: str) -> str:
def download_gh_svg(value: str | ConfigType, source: str) -> Path:
mdi_id = value[CONF_ICON] if isinstance(value, dict) else value
url, path = _gh_svg_url_path(mdi_id, source)
return download_file(url, path)
def download_image(value: str | ConfigType) -> str:
def download_image(value: str | ConfigType) -> Path:
value = value[CONF_URL] if isinstance(value, dict) else value
return download_file(value, compute_local_image_path(value))
@@ -147,7 +149,7 @@ def _extract_entry_ref(entry: ConfigType) -> RemoteFile | None:
PREFETCH_FILES = external_files.single_stage_prefetch(_extract_entry_ref)
def validate_file_shorthand(value: Any) -> str:
def validate_file_shorthand(value: Any) -> Path:
value = cv.string_strict(value)
if (remote := _parse_remote_shorthand(value)) is not None:
return download_file(remote.url, remote.path)
@@ -165,7 +167,7 @@ LOCAL_SCHEMA = cv.All(
def mdi_schema(source: str) -> cv.All:
def validate_mdi(value: ConfigType) -> str:
def validate_mdi(value: ConfigType) -> Path:
return download_gh_svg(value, source)
return cv.All(
@@ -47,6 +47,9 @@ class GPIOBinarySensorStore {
class GPIOBinarySensor final : public binary_sensor::BinarySensor, public Component {
public:
// User provided, not "= default": `new(p) GPIOBinarySensor()` would zero-fill .bss that is already zero.
GPIOBinarySensor() {}
// No destructor needed: ESPHome components are created at boot and live forever.
// Interrupts are only detached on reboot when memory is cleared anyway.
@@ -70,7 +73,7 @@ class GPIOBinarySensor final : public binary_sensor::BinarySensor, public Compon
void loop() override;
protected:
GPIOPin *pin_;
GPIOPin *pin_{nullptr};
GPIOBinarySensorStore store_;
};
+6 -2
View File
@@ -15,9 +15,13 @@ CONFIG_SCHEMA = (
.extend(
{
cv.Required(CONF_PIN): pins.gpio_output_pin_schema,
cv.Optional(CONF_INTERLOCK): cv.ensure_list(cv.use_id(switch.Switch)),
cv.Optional(
CONF_INTERLOCK_WAIT_TIME, default="0ms"
CONF_INTERLOCK, visibility=cv.Visibility.ADVANCED
): cv.ensure_list(cv.use_id(switch.Switch)),
cv.Optional(
CONF_INTERLOCK_WAIT_TIME,
default="0ms",
visibility=cv.Visibility.ADVANCED,
): cv.positive_time_period_milliseconds,
}
)
+4 -1
View File
@@ -9,6 +9,9 @@ namespace esphome::gpio {
class GPIOSwitch final : public switch_::Switch, public Component {
public:
// User provided, not "= default": `new(p) GPIOSwitch()` would zero-fill .bss that is already zero.
GPIOSwitch() {}
void set_pin(GPIOPin *pin) { pin_ = pin; }
// ========== INTERNAL METHODS ==========
@@ -25,7 +28,7 @@ class GPIOSwitch final : public switch_::Switch, public Component {
protected:
void write_state(bool state) override;
GPIOPin *pin_;
GPIOPin *pin_{nullptr};
#ifdef USE_GPIO_SWITCH_INTERLOCK
FixedVector<Switch *> interlock_;
uint32_t interlock_wait_time_{0};
@@ -57,10 +57,6 @@ void GraphicalDisplayMenu::dump_config() {
}
}
void GraphicalDisplayMenu::set_display(display::Display *display) { this->display_ = display; }
void GraphicalDisplayMenu::set_font(display::BaseFont *font) { this->font_ = font; }
void GraphicalDisplayMenu::set_foreground_color(Color foreground_color) { this->foreground_color_ = foreground_color; }
void GraphicalDisplayMenu::set_background_color(Color background_color) { this->background_color_ = background_color; }
@@ -38,8 +38,8 @@ class GraphicalDisplayMenu final : public display_menu_base::DisplayMenuComponen
void setup() override;
void dump_config() override;
void set_display(display::Display *display);
void set_font(display::BaseFont *font);
void set_display(display::Display *display) { this->display_ = display; }
void set_font(display::BaseFont *font) { this->font_ = font; }
template<typename V> void set_menu_item_value(V menu_item_value) { this->menu_item_value_ = menu_item_value; }
void set_foreground_color(Color foreground_color);
void set_background_color(Color background_color);
-2
View File
@@ -190,8 +190,6 @@ void HaierClimateBase::set_supported_presets(climate::ClimatePresetMask presets)
this->traits_.add_supported_preset(climate::CLIMATE_PRESET_NONE);
}
void HaierClimateBase::set_send_wifi(bool send_wifi) { this->send_wifi_signal_ = send_wifi; }
void HaierClimateBase::send_custom_command(const haier_protocol::HaierMessage &message) {
this->action_request_ = PendingAction({ActionRequest::SEND_CUSTOM_COMMAND, message});
}
+1 -1
View File
@@ -71,7 +71,7 @@ class HaierClimateBase : public esphome::Component,
};
bool can_send_message() const { return haier_protocol_.get_outgoing_queue_size() == 0; };
void set_answer_timeout(uint32_t timeout);
void set_send_wifi(bool send_wifi);
void set_send_wifi(bool send_wifi) { this->send_wifi_signal_ = send_wifi; }
void send_custom_command(const haier_protocol::HaierMessage &message);
template<typename F> void add_status_message_callback(F &&callback) {
this->status_message_callback_.add(std::forward<F>(callback));
+47 -17
View File
@@ -331,27 +331,46 @@ class HttpRequestComponent : public Component {
void set_follow_redirects(bool follow_redirects) { this->follow_redirects_ = follow_redirects; }
void set_redirect_limit(uint16_t limit) { this->redirect_limit_ = limit; }
std::shared_ptr<HttpContainer> get(const std::string &url) {
return this->start(url, "GET", "", std::vector<Header>{});
}
std::shared_ptr<HttpContainer> get(const std::string &url, const std::vector<Header> &request_headers) {
std::shared_ptr<HttpContainer> get(const char *url) { return this->start(url, "GET", "", std::vector<Header>{}); }
std::shared_ptr<HttpContainer> get(const char *url, const std::vector<Header> &request_headers) {
return this->start(url, "GET", "", request_headers);
}
std::shared_ptr<HttpContainer> get(const std::string &url, const std::vector<Header> &request_headers,
std::shared_ptr<HttpContainer> get(const char *url, const std::vector<Header> &request_headers,
const std::vector<std::string> &lower_case_collect_headers) {
return this->start(url, "GET", "", request_headers, lower_case_collect_headers);
}
std::shared_ptr<HttpContainer> post(const std::string &url, const std::string &body) {
std::shared_ptr<HttpContainer> post(const char *url, const std::string &body) {
return this->start(url, "POST", body, std::vector<Header>{});
}
std::shared_ptr<HttpContainer> post(const char *url, const std::string &body,
const std::vector<Header> &request_headers) {
return this->start(url, "POST", body, request_headers);
}
std::shared_ptr<HttpContainer> post(const char *url, const std::string &body,
const std::vector<Header> &request_headers,
const std::vector<std::string> &lower_case_collect_headers) {
return this->start(url, "POST", body, request_headers, lower_case_collect_headers);
}
std::shared_ptr<HttpContainer> get(const std::string &url) { return this->get(url.c_str()); }
std::shared_ptr<HttpContainer> get(const std::string &url, const std::vector<Header> &request_headers) {
return this->get(url.c_str(), request_headers);
}
std::shared_ptr<HttpContainer> get(const std::string &url, const std::vector<Header> &request_headers,
const std::vector<std::string> &lower_case_collect_headers) {
return this->get(url.c_str(), request_headers, lower_case_collect_headers);
}
std::shared_ptr<HttpContainer> post(const std::string &url, const std::string &body) {
return this->post(url.c_str(), body);
}
std::shared_ptr<HttpContainer> post(const std::string &url, const std::string &body,
const std::vector<Header> &request_headers) {
return this->start(url, "POST", body, request_headers);
return this->post(url.c_str(), body, request_headers);
}
std::shared_ptr<HttpContainer> post(const std::string &url, const std::string &body,
const std::vector<Header> &request_headers,
const std::vector<std::string> &lower_case_collect_headers) {
return this->start(url, "POST", body, request_headers, lower_case_collect_headers);
return this->post(url.c_str(), body, request_headers, lower_case_collect_headers);
}
// Remove before 2027.1.0
@@ -379,11 +398,15 @@ class HttpRequestComponent : public Component {
return this->post(url, body, std::vector<Header>(request_headers.begin(), request_headers.end()), collect_headers);
}
std::shared_ptr<HttpContainer> start(const std::string &url, const std::string &method, const std::string &body,
std::shared_ptr<HttpContainer> start(const char *url, const char *method, const std::string &body,
const std::vector<Header> &request_headers) {
// Call perform() directly to avoid ambiguity with the deprecated overloads
return this->perform(url, method, body, request_headers, {});
}
std::shared_ptr<HttpContainer> start(const std::string &url, const std::string &method, const std::string &body,
const std::vector<Header> &request_headers) {
return this->start(url.c_str(), method.c_str(), body, request_headers);
}
// Remove before 2027.1.0
ESPDEPRECATED("Pass request_headers as std::vector<Header> instead of std::list. Removed in 2027.1.0.", "2026.7.0")
@@ -403,7 +426,7 @@ class HttpRequestComponent : public Component {
for (const auto &h : collect_headers) {
lower.push_back(str_lower_case(h)); // NOLINT
}
return this->perform(url, method, body, request_headers, lower);
return this->perform(url.c_str(), method.c_str(), body, request_headers, lower);
}
// Remove before 2027.1.0
@@ -418,7 +441,8 @@ class HttpRequestComponent : public Component {
for (const auto &h : collect_headers) {
lower.push_back(str_lower_case(h)); // NOLINT
}
return this->perform(url, method, body, std::vector<Header>(request_headers.begin(), request_headers.end()), lower);
return this->perform(url.c_str(), method.c_str(), body,
std::vector<Header>(request_headers.begin(), request_headers.end()), lower);
}
// Remove before 2027.1.0
@@ -426,19 +450,25 @@ class HttpRequestComponent : public Component {
std::shared_ptr<HttpContainer> start(const std::string &url, const std::string &method, const std::string &body,
const std::list<Header> &request_headers,
const std::vector<std::string> &lower_case_collect_headers) {
return this->perform(url, method, body, std::vector<Header>(request_headers.begin(), request_headers.end()),
return this->perform(url.c_str(), method.c_str(), body,
std::vector<Header>(request_headers.begin(), request_headers.end()),
lower_case_collect_headers);
}
std::shared_ptr<HttpContainer> start(const std::string &url, const std::string &method, const std::string &body,
std::shared_ptr<HttpContainer> start(const char *url, const char *method, const std::string &body,
const std::vector<Header> &request_headers,
const std::vector<std::string> &lower_case_collect_headers) {
return this->perform(url, method, body, request_headers, lower_case_collect_headers);
}
std::shared_ptr<HttpContainer> start(const std::string &url, const std::string &method, const std::string &body,
const std::vector<Header> &request_headers,
const std::vector<std::string> &lower_case_collect_headers) {
return this->start(url.c_str(), method.c_str(), body, request_headers, lower_case_collect_headers);
}
protected:
virtual std::shared_ptr<HttpContainer> perform(const std::string &url, const std::string &method,
const std::string &body, const std::vector<Header> &request_headers,
virtual std::shared_ptr<HttpContainer> perform(const char *url, const char *method, const std::string &body,
const std::vector<Header> &request_headers,
const std::vector<std::string> &lower_case_collect_headers) = 0;
const char *useragent_{nullptr};
bool follow_redirects_{};
@@ -499,8 +529,8 @@ template<typename... Ts> class HttpRequestSendAction final : public Action<Ts...
request_headers.push_back({key, val.value(x...)});
}
auto container = this->parent_->start(this->url_.value(x...), this->method_.value(x...), body, request_headers,
this->lower_case_collect_headers_);
auto container = this->parent_->start(this->url_.value(x...).c_str(), this->method_.value(x...), body,
request_headers, this->lower_case_collect_headers_);
auto captured_args = std::make_tuple(x...);
@@ -2,6 +2,8 @@
#if defined(USE_ARDUINO) && !defined(USE_ESP32) && !defined(USE_LIBRETINY)
#include <cstring>
#include "esphome/components/network/util.h"
#include "esphome/components/watchdog/watchdog.h"
@@ -22,8 +24,7 @@ static const char *const TAG = "http_request";
static constexpr int ESP8266_SSL_ERR_OOM = -1000;
#endif
std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &url, const std::string &method,
const std::string &body,
std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const char *url, const char *method, const std::string &body,
const std::vector<Header> &request_headers,
const std::vector<std::string> &lower_case_collect_headers) {
if (!network::is_connected()) {
@@ -37,7 +38,7 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &ur
const uint32_t start = millis();
bool secure = url.find("https:") != std::string::npos;
bool secure = strstr(url, "https:") != nullptr;
container->set_secure(secure);
watchdog::WatchdogManager wdm(this->get_watchdog_timeout());
@@ -70,19 +71,19 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &ur
stream_ptr = std::make_unique<WiFiClient>();
#endif // USE_HTTP_REQUEST_ESP8266_HTTPS
bool status = container->client_.begin(*stream_ptr, url.c_str());
bool status = container->client_.begin(*stream_ptr, url);
#elif defined(USE_RP2)
if (secure) {
container->client_.setInsecure();
}
bool status = container->client_.begin(url.c_str());
bool status = container->client_.begin(url);
#endif
App.feed_wdt();
if (!status) {
ESP_LOGW(TAG, "HTTP Request failed; URL: %s", url.c_str());
ESP_LOGW(TAG, "HTTP Request failed; URL: %s", url);
container->end();
this->status_momentary_error("failed", 1000);
return nullptr;
@@ -107,7 +108,7 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &ur
container->client_.collectHeaders(header_keys, index);
App.feed_wdt();
container->status_code = container->client_.sendRequest(method.c_str(), body.c_str());
container->status_code = container->client_.sendRequest(method, body.c_str());
App.feed_wdt();
if (container->status_code < 0) {
#if defined(USE_ESP8266) && defined(USE_HTTP_REQUEST_ESP8266_HTTPS)
@@ -139,7 +140,7 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &ur
}
#endif
ESP_LOGW(TAG, "HTTP Request failed; URL: %s; Error: %s", url.c_str(),
ESP_LOGW(TAG, "HTTP Request failed; URL: %s; Error: %s", url,
HTTPClient::errorToString(container->status_code).c_str());
this->status_momentary_error("failed", 1000);
@@ -147,7 +148,7 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &ur
return nullptr;
}
if (!is_success(container->status_code)) {
ESP_LOGE(TAG, "HTTP Request failed; URL: %s; Code: %d", url.c_str(), container->status_code);
ESP_LOGE(TAG, "HTTP Request failed; URL: %s; Code: %d", url, container->status_code);
this->status_momentary_error("failed", 1000);
// Still return the container, so it can be used to get the status code and error message
}
@@ -54,7 +54,7 @@ class HttpRequestArduino final : public HttpRequestComponent {
#endif
protected:
std::shared_ptr<HttpContainer> perform(const std::string &url, const std::string &method, const std::string &body,
std::shared_ptr<HttpContainer> perform(const char *url, const char *method, const std::string &body,
const std::vector<Header> &request_headers,
const std::vector<std::string> &lower_case_collect_headers) override;
#ifdef USE_ESP8266
@@ -5,6 +5,8 @@
#include "httplib.h"
#include "http_request_host.h"
#include <cstring>
#include <regex>
#include "esphome/components/network/util.h"
#include "esphome/components/watchdog/watchdog.h"
@@ -16,8 +18,7 @@ namespace esphome::http_request {
static const char *const TAG = "http_request";
std::shared_ptr<HttpContainer> HttpRequestHost::perform(const std::string &url, const std::string &method,
const std::string &body,
std::shared_ptr<HttpContainer> HttpRequestHost::perform(const char *url, const char *method, const std::string &body,
const std::vector<Header> &request_headers,
const std::vector<std::string> &lower_case_collect_headers) {
if (!network::is_connected()) {
@@ -27,10 +28,10 @@ std::shared_ptr<HttpContainer> HttpRequestHost::perform(const std::string &url,
}
std::regex url_regex(R"(^(([^:\/?#]+):)?(//([^\/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?)", std::regex::extended);
std::smatch url_match_result;
std::cmatch url_match_result;
if (!std::regex_match(url, url_match_result, url_regex) || url_match_result.length() < 7) {
ESP_LOGE(TAG, "HTTP Request failed; Malformed URL: %s", url.c_str());
ESP_LOGE(TAG, "HTTP Request failed; Malformed URL: %s", url);
return nullptr;
}
auto host = url_match_result[4].str();
@@ -54,7 +55,7 @@ std::shared_ptr<HttpContainer> HttpRequestHost::perform(const std::string &url,
}
httplib::Client client(scheme_host.c_str());
if (!client.is_valid()) {
ESP_LOGE(TAG, "HTTP Request failed; Invalid URL: %s", url.c_str());
ESP_LOGE(TAG, "HTTP Request failed; Invalid URL: %s", url);
return nullptr;
}
client.set_follow_location(this->follow_redirects_);
@@ -64,41 +65,41 @@ std::shared_ptr<HttpContainer> HttpRequestHost::perform(const std::string &url,
#endif
httplib::Result result;
if (method == "GET") {
if (strcmp(method, "GET") == 0) {
result = client.Get(path, h_headers, [&](const char *data, size_t data_length) {
ESP_LOGV(TAG, "Got data length: %zu", data_length);
container->response_body_.insert(container->response_body_.end(), (const uint8_t *) data,
(const uint8_t *) data + data_length);
return true;
});
} else if (method == "HEAD") {
} else if (strcmp(method, "HEAD") == 0) {
result = client.Head(path, h_headers);
} else if (method == "PUT") {
} else if (strcmp(method, "PUT") == 0) {
result = client.Put(path, h_headers, body, "");
if (result) {
auto data = std::vector<uint8_t>(result->body.begin(), result->body.end());
container->response_body_.insert(container->response_body_.end(), data.begin(), data.end());
}
} else if (method == "PATCH") {
} else if (strcmp(method, "PATCH") == 0) {
result = client.Patch(path, h_headers, body, "");
if (result) {
auto data = std::vector<uint8_t>(result->body.begin(), result->body.end());
container->response_body_.insert(container->response_body_.end(), data.begin(), data.end());
}
} else if (method == "POST") {
} else if (strcmp(method, "POST") == 0) {
result = client.Post(path, h_headers, body, "");
if (result) {
auto data = std::vector<uint8_t>(result->body.begin(), result->body.end());
container->response_body_.insert(container->response_body_.end(), data.begin(), data.end());
}
} else {
ESP_LOGW(TAG, "HTTP Request failed - unsupported method %s; URL: %s", method.c_str(), url.c_str());
ESP_LOGW(TAG, "HTTP Request failed - unsupported method %s; URL: %s", method, url);
container->end();
return nullptr;
}
App.feed_wdt();
if (!result) {
ESP_LOGW(TAG, "HTTP Request failed; URL: %s, error code: %u", url.c_str(), (unsigned) result.error());
ESP_LOGW(TAG, "HTTP Request failed; URL: %s, error code: %u", url, (unsigned) result.error());
container->end();
this->status_momentary_error("failed", 1000);
return nullptr;
@@ -107,7 +108,7 @@ std::shared_ptr<HttpContainer> HttpRequestHost::perform(const std::string &url,
auto response = *result;
container->status_code = response.status;
if (!is_success(response.status)) {
ESP_LOGE(TAG, "HTTP Request failed; URL: %s; Code: %d", url.c_str(), response.status);
ESP_LOGE(TAG, "HTTP Request failed; URL: %s; Code: %d", url, response.status);
this->status_momentary_error("failed", 1000);
// Still return the container, so it can be used to get the status code and error message
}
@@ -18,7 +18,7 @@ class HttpContainerHost : public HttpContainer {
class HttpRequestHost final : public HttpRequestComponent {
public:
std::shared_ptr<HttpContainer> perform(const std::string &url, const std::string &method, const std::string &body,
std::shared_ptr<HttpContainer> perform(const char *url, const char *method, const std::string &body,
const std::vector<Header> &request_headers,
const std::vector<std::string> &lower_case_collect_headers) override;
void set_ca_path(const char *ca_path) { this->ca_path_ = ca_path; }
@@ -2,6 +2,8 @@
#ifdef USE_ESP32
#include <cstring>
#include "esphome/components/network/util.h"
#include "esphome/components/watchdog/watchdog.h"
@@ -48,8 +50,7 @@ esp_err_t HttpRequestIDF::http_event_handler(esp_http_client_event_t *evt) {
return ESP_OK;
}
std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, const std::string &method,
const std::string &body,
std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const char *url, const char *method, const std::string &body,
const std::vector<Header> &request_headers,
const std::vector<std::string> &lower_case_collect_headers) {
if (!network::is_connected()) {
@@ -59,15 +60,15 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
}
esp_http_client_method_t method_idf;
if (method == "GET") {
if (strcmp(method, "GET") == 0) {
method_idf = HTTP_METHOD_GET;
} else if (method == "POST") {
} else if (strcmp(method, "POST") == 0) {
method_idf = HTTP_METHOD_POST;
} else if (method == "PUT") {
} else if (strcmp(method, "PUT") == 0) {
method_idf = HTTP_METHOD_PUT;
} else if (method == "DELETE") {
} else if (strcmp(method, "DELETE") == 0) {
method_idf = HTTP_METHOD_DELETE;
} else if (method == "PATCH") {
} else if (strcmp(method, "PATCH") == 0) {
method_idf = HTTP_METHOD_PATCH;
} else {
this->status_momentary_error("failed", ERROR_DURATION_MS);
@@ -75,11 +76,11 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
return nullptr;
}
bool secure = url.find("https:") != std::string::npos;
bool secure = strstr(url, "https:") != nullptr;
esp_http_client_config_t config = {};
config.url = url.c_str();
config.url = url;
config.method = method_idf;
config.timeout_ms = this->timeout_;
config.disable_auto_redirect = !this->follow_redirects_;
@@ -218,7 +219,7 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
}
}
ESP_LOGE(TAG, "HTTP Request failed; URL: %s; Code: %d", url.c_str(), container->status_code);
ESP_LOGE(TAG, "HTTP Request failed; URL: %s; Code: %d", url, container->status_code);
this->status_momentary_error("failed", ERROR_DURATION_MS);
return container;
}
@@ -30,6 +30,9 @@ class HttpContainerIDF : public HttpContainer {
class HttpRequestIDF final : public HttpRequestComponent {
public:
// User provided, not "= default": `new(p) HttpRequestIDF()` would zero-fill .bss that is already zero.
HttpRequestIDF() {}
void dump_config() override;
void set_buffer_size_rx(uint16_t buffer_size_rx) { this->buffer_size_rx_ = buffer_size_rx; }
@@ -38,7 +41,7 @@ class HttpRequestIDF final : public HttpRequestComponent {
void set_ca_certificate(const char *ca_certificate) { this->ca_certificate_ = ca_certificate; }
protected:
std::shared_ptr<HttpContainer> perform(const std::string &url, const std::string &method, const std::string &body,
std::shared_ptr<HttpContainer> perform(const char *url, const char *method, const std::string &body,
const std::vector<Header> &request_headers,
const std::vector<std::string> &lower_case_collect_headers) override;
// if zero ESP-IDF will use DEFAULT_HTTP_BUF_SIZE
@@ -1,5 +1,7 @@
#include "http_request_update.h"
#include <cstring>
#include "esphome/core/application.h"
#include "esphome/core/version.h"
@@ -94,7 +96,7 @@ void HttpRequestUpdate::update_task(void *params) {
auto container = this_update->request_parent_->get(this_update->source_url_);
if (container == nullptr || container->status_code != HTTP_STATUS_OK) {
ESP_LOGE(TAG, "Failed to fetch manifest from %s", this_update->source_url_.c_str());
ESP_LOGE(TAG, "Failed to fetch manifest from %s", this_update->source_url_);
if (container != nullptr)
container->end();
result->error_str = LOG_STR("Failed to fetch manifest");
@@ -174,21 +176,26 @@ void HttpRequestUpdate::update_task(void *params) {
allocator.deallocate(data, content_length);
if (!valid) {
ESP_LOGE(TAG, "Failed to parse JSON from %s", this_update->source_url_.c_str());
ESP_LOGE(TAG, "Failed to parse JSON from %s", this_update->source_url_);
result->error_str = LOG_STR("Failed to parse manifest JSON");
goto defer; // NOLINT(cppcoreguidelines-avoid-goto)
}
// Merge source_url_ and firmware_url
if (!info->firmware_url.empty() && info->firmware_url.find("http") == std::string::npos) {
std::string path = info->firmware_url;
if (path[0] == '/') {
std::string domain = this_update->source_url_.substr(0, this_update->source_url_.find('/', 8));
info->firmware_url = domain + path;
const char *source = this_update->source_url_;
const size_t source_len = strlen(source);
size_t prefix_len;
if (info->firmware_url[0] == '/') {
// scheme and host, up to the first slash after "https://"
const char *host_end = source_len > 8 ? strchr(source + 8, '/') : nullptr;
prefix_len = host_end != nullptr ? host_end - source : source_len;
} else {
std::string domain = this_update->source_url_.substr(0, this_update->source_url_.rfind('/') + 1);
info->firmware_url = domain + path;
// directory of the manifest, up to and including its last slash
const char *dir_end = strrchr(source, '/');
prefix_len = dir_end != nullptr ? dir_end - source + 1 : 0;
}
info->firmware_url.insert(0, source, prefix_len);
}
#ifdef ESPHOME_PROJECT_VERSION
@@ -21,7 +21,7 @@ class HttpRequestUpdate final : public update::UpdateEntity, public PollingCompo
void perform(bool force) override;
void check() override { this->update(); }
void set_source_url(const std::string &source_url) { this->source_url_ = source_url; }
void set_source_url(const char *source_url) { this->source_url_ = source_url; }
void set_request_parent(HttpRequestComponent *request_parent) { this->request_parent_ = request_parent; }
void set_ota_parent(OtaHttpRequestComponent *ota_parent) { this->ota_parent_ = ota_parent; }
@@ -33,13 +33,15 @@ class HttpRequestUpdate final : public update::UpdateEntity, public PollingCompo
protected:
HttpRequestComponent *request_parent_;
OtaHttpRequestComponent *ota_parent_;
std::string source_url_;
static void update_task(void *params);
#ifdef USE_ESP32
TaskHandle_t update_task_handle_{nullptr};
#endif
uint8_t initial_check_remaining_{0};
private:
const char *source_url_{nullptr}; // literal from codegen
};
} // namespace esphome::http_request
@@ -14,17 +14,19 @@
#include "esp_timer.h"
// esp-audio-libs
#include <gain.h>
#include <cmath>
namespace esphome::i2s_audio {
static const char *const TAG = "i2s_audio.speaker";
// Software volume control maps the user-facing [0.0, 1.0] range to a Q31 scale factor.
// Volumes in (0.0, 1.0) map linearly to a dB reduction in [-49.0, 0.0] dB.
// Software volume control maps the user-facing (0.0, 1.0) range linearly to a dB reduction in
// [-49.0, 0.0] dB; 0.0 is silence.
static constexpr float SOFTWARE_VOLUME_MIN_DB = -49.0f;
// Rate at which the software gain moves toward a new target.
static constexpr uint32_t GAIN_RAMP_MS_PER_DB = 1;
void I2SAudioSpeakerBase::setup() {
this->event_group_ = xEventGroupCreate();
@@ -34,9 +36,10 @@ void I2SAudioSpeakerBase::setup() {
return;
}
// Initialize volume control. When audio_dac is configured, this sets the DAC volume.
// Initialize volume control. When audio_dac is configured, this sets the DAC volume and mute state.
// When no audio_dac is configured, this initializes software volume control.
this->set_volume(this->volume_);
this->set_mute_state(this->mute_state_);
}
void I2SAudioSpeakerBase::dump_config() {
@@ -53,6 +56,13 @@ void I2SAudioSpeakerBase::dump_config() {
void I2SAudioSpeakerBase::loop() {
uint32_t event_group_bits = xEventGroupGetBits(this->event_group_);
// A stop that arrives while stopped cancels any start that has not been processed yet
constexpr uint32_t stop_bits = SpeakerEventGroupBits::COMMAND_STOP | SpeakerEventGroupBits::COMMAND_STOP_GRACEFULLY;
if ((event_group_bits & stop_bits) && (this->state_ == speaker::STATE_STOPPED)) {
xEventGroupClearBits(this->event_group_, stop_bits | SpeakerEventGroupBits::COMMAND_START);
event_group_bits &= ~(stop_bits | SpeakerEventGroupBits::COMMAND_START);
}
if ((event_group_bits & SpeakerEventGroupBits::COMMAND_START) && (this->state_ == speaker::STATE_STOPPED)) {
this->state_ = speaker::STATE_STARTING;
xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::COMMAND_START);
@@ -129,6 +139,10 @@ void I2SAudioSpeakerBase::loop() {
break;
}
// Seed the ramp at the live target so this run adopts it instantly rather than fading to it
// from wherever the previous run left off. Posted here, not in the task: the ramp's mailbox
// allows one writer, and that is the main loop.
this->post_software_gain_(0);
xTaskCreate(I2SAudioSpeakerBase::speaker_task, "speaker_task", TASK_STACK_SIZE, (void *) this, TASK_PRIORITY,
&this->speaker_task_handle_);
@@ -146,50 +160,31 @@ void I2SAudioSpeakerBase::loop() {
}
void I2SAudioSpeakerBase::set_volume(float volume) {
this->volume_ = volume;
#ifdef USE_AUDIO_DAC
if (this->audio_dac_ != nullptr) {
if (volume > 0.0f) {
this->audio_dac_->set_mute_off();
}
this->audio_dac_->set_volume(volume);
} else
#endif // USE_AUDIO_DAC
{
// Fallback to software volume control by using a Q31 fixed point scaling factor.
// At maximum volume (1.0), set to INT32_MAX to bypass volume processing entirely
// and avoid any floating-point precision issues that could cause slight volume reduction.
if (volume >= 1.0f) {
this->q31_volume_factor_ = INT32_MAX;
} else if (volume <= 0.0f) {
this->q31_volume_factor_ = 0;
} else {
this->q31_volume_factor_ =
esp_audio_libs::gain::db_to_q31(remap<float, float>(volume, 0.0f, 1.0f, SOFTWARE_VOLUME_MIN_DB, 0.0f));
}
}
speaker::Speaker::set_volume(volume);
this->post_software_gain_(this->audio_stream_info_.ms_to_samples(GAIN_RAMP_MS_PER_DB));
}
void I2SAudioSpeakerBase::set_mute_state(bool mute_state) {
this->mute_state_ = mute_state;
speaker::Speaker::set_mute_state(mute_state);
this->post_software_gain_(this->audio_stream_info_.ms_to_samples(GAIN_RAMP_MS_PER_DB));
}
void I2SAudioSpeakerBase::post_software_gain_(uint32_t rate_samples) {
#ifdef USE_AUDIO_DAC
if (this->audio_dac_) {
if (mute_state) {
this->audio_dac_->set_mute_on();
} else {
this->audio_dac_->set_mute_off();
}
} else
#endif // USE_AUDIO_DAC
{
if (mute_state) {
// Fallback to software volume control and scale by 0
this->q31_volume_factor_ = 0;
} else {
// Revert to previous volume when unmuting
this->set_volume(this->volume_);
}
if (this->audio_dac_ != nullptr) {
return; // Hardware volume; the ramp stays at unity
}
#endif // USE_AUDIO_DAC
// Software volume control. The ramp treats 0 dB as unity and skips processing there.
float target_db;
if (this->is_silent_()) {
target_db = -INFINITY;
} else if (this->volume_ >= 1.0f) {
target_db = 0.0f;
} else {
target_db = remap<float, float>(this->volume_, 0.0f, 1.0f, SOFTWARE_VOLUME_MIN_DB, 0.0f);
}
this->gain_ramp_.set_target_db_at_rate(target_db, rate_samples);
}
size_t I2SAudioSpeakerBase::play(const uint8_t *data, size_t length, TickType_t ticks_to_wait) {
@@ -239,8 +234,6 @@ void I2SAudioSpeakerBase::start() {
if ((this->state_ == speaker::STATE_STARTING) || (this->state_ == speaker::STATE_RUNNING))
return;
// Mark STARTING immediately to avoid transient STOPPED observations before loop() processes COMMAND_START.
this->state_ = speaker::STATE_STARTING;
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::COMMAND_START);
}
@@ -249,11 +242,10 @@ void I2SAudioSpeakerBase::stop() { this->stop_(false); }
void I2SAudioSpeakerBase::finish() { this->stop_(true); }
void I2SAudioSpeakerBase::stop_(bool wait_on_empty) {
if (this->is_failed())
return;
if (this->state_ == speaker::STATE_STOPPED)
if (!this->is_ready() || this->is_failed())
return;
// Always set the bit, even when stopped, so loop() can cancel a start that is still pending
if (wait_on_empty) {
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::COMMAND_STOP_GRACEFULLY);
} else {
@@ -351,14 +343,14 @@ bool IRAM_ATTR I2SAudioSpeakerBase::i2s_on_sent_cb(i2s_chan_handle_t handle, i2s
}
void I2SAudioSpeakerBase::apply_software_volume_(uint8_t *data, size_t bytes_read) {
if (this->q31_volume_factor_ == INT32_MAX) {
return; // Max volume, no processing needed
#ifdef USE_AUDIO_DAC
if (this->audio_dac_ != nullptr) {
return; // Hardware volume; the ramp is never targeted
}
#endif // USE_AUDIO_DAC
const size_t bytes_per_sample = this->current_stream_info_.samples_to_bytes(1);
const uint32_t len = bytes_read / bytes_per_sample;
esp_audio_libs::gain::apply(data, data, this->q31_volume_factor_, len, bytes_per_sample);
this->gain_ramp_.process(data, static_cast<uint8_t>(bytes_per_sample),
this->current_stream_info_.bytes_to_samples(bytes_read));
}
void I2SAudioSpeakerBase::swap_esp32_mono_samples_(uint8_t *data, size_t bytes_read) {
@@ -16,6 +16,8 @@
#include "esphome/core/gpio.h"
#include "esphome/core/helpers.h"
#include <gain.h> // esp-audio-libs
namespace esphome::i2s_audio {
// Shared constants used by both standard and SPDIF speaker implementations
@@ -77,19 +79,23 @@ class I2SAudioSpeakerBase : public I2SAudioOut, public speaker::Speaker, public
bool has_buffered_data() const override;
/// @brief Sets the volume of the speaker. Uses the speaker's configured audio dac component. If unavailble, it is
/// implemented as a software volume control. Overrides the default setter to convert the floating point volume to a
/// Q15 fixed-point factor.
/// @brief Sets the volume of the speaker. Uses the speaker's configured audio dac component. If unavailable, it is
/// implemented as a software volume control. Overrides the default setter to convert the volume to a dB target for
/// the gain ramp.
/// @param volume between 0.0 and 1.0
void set_volume(float volume) override;
/// @brief Mutes or unmute the speaker. Uses the speaker's configured audio dac component. If unavailble, it is
/// implemented as a software volume control. Overrides the default setter to convert the floating point volume to a
/// Q15 fixed-point factor.
/// @brief Mutes or unmutes the speaker. Uses the speaker's configured audio dac component. If unavailable, it is
/// implemented as a software volume control. Overrides the default setter to post the mute state to the gain ramp.
/// @param mute_state true for muting, false for unmuting
void set_mute_state(bool mute_state) override;
protected:
/// @brief Posts the ramp target derived from the current volume and mute state. No-op when an audio dac owns
/// volume. Main loop only.
/// @param rate_samples Samples the ramp takes per dB of change; 0 adopts the target at once
void post_software_gain_(uint32_t rate_samples);
/// @brief FreeRTOS task entry point. Casts params to I2SAudioSpeakerBase and calls run_speaker_task_().
/// @param params I2SAudioSpeakerBase component pointer
static void speaker_task(void *params);
@@ -128,7 +134,8 @@ class I2SAudioSpeakerBase : public I2SAudioOut, public speaker::Speaker, public
/// @brief Called in loop() when the task has stopped. Override for mode-specific cleanup.
virtual void on_task_stopped() {}
/// @brief Apply software volume control using Q15 fixed-point scaling.
/// @brief Apply software volume control by running the samples through the gain ramp. Called from the
/// speaker task only.
/// @param data Pointer to audio sample data (modified in place)
/// @param bytes_read Number of bytes of audio data
void apply_software_volume_(uint8_t *data, size_t bytes_read);
@@ -155,7 +162,9 @@ class I2SAudioSpeakerBase : public I2SAudioOut, public speaker::Speaker, public
bool pause_state_{false};
int32_t q31_volume_factor_{INT32_MAX};
// Smooths software gain changes. The main loop posts targets, the speaker task processes;
// GainRamp's mailbox makes that safe. The main loop is the only poster.
esp_audio_libs::gain::GainRamp gain_ramp_;
audio::AudioStreamInfo current_stream_info_; // Format of the audio in the ring buffer (the I2S input)
// Format actually clocked out of the I2S peripheral. Same channel count and sample rate as
+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
@@ -13,6 +13,9 @@ namespace esphome::internal_temperature {
class InternalTemperatureSensor final : public sensor::Sensor, public PollingComponent {
public:
// User provided, not "= default": `new(p) InternalTemperatureSensor()` would zero-fill .bss that is already zero.
InternalTemperatureSensor() {}
#if defined(USE_ESP32) || (defined(USE_ZEPHYR) && defined(USE_NRF52))
void setup() override;
#endif // USE_ESP32 || (USE_ZEPHYR && USE_NRF52)
@@ -275,8 +275,4 @@ void Lc709203f::set_pack_size(uint16_t pack_size) {
// not cause an error or crash, so I am not doing any additional checking here.
}
void Lc709203f::set_thermistor_b_constant(uint16_t b_constant) { this->b_constant_ = b_constant; }
void Lc709203f::set_pack_voltage(LC709203FBatteryVoltage pack_voltage) { this->pack_voltage_ = pack_voltage; }
} // namespace esphome::lc709203f
+2 -2
View File
@@ -26,8 +26,8 @@ class Lc709203f final : public sensor::Sensor, public PollingComponent, public i
void dump_config() override;
void set_pack_size(uint16_t pack_size);
void set_thermistor_b_constant(uint16_t b_constant);
void set_pack_voltage(LC709203FBatteryVoltage pack_voltage);
void set_thermistor_b_constant(uint16_t b_constant) { this->b_constant_ = b_constant; }
void set_pack_voltage(LC709203FBatteryVoltage pack_voltage) { this->pack_voltage_ = pack_voltage; }
void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; }
void set_battery_remaining_sensor(sensor::Sensor *battery_remaining_sensor) {
battery_remaining_sensor_ = battery_remaining_sensor;
+2 -2
View File
@@ -109,14 +109,14 @@ async def to_code(config: ConfigType) -> None:
for x in range(14):
if gate_conf := config.get(f"gate_{x}"):
move_config = gate_conf[CONF_MOVE_THRESHOLD]
n = cg.new_Pvariable(move_config[CONF_ID], x)
n = cg.new_Pvariable(move_config[CONF_ID])
await number.register_number(
n, move_config, min_value=0, max_value=100, step=1
)
await cg.register_parented(n, config[CONF_LD2412_ID])
cg.add(LD2412_component.set_gate_move_threshold_number(x, n))
still_config = gate_conf[CONF_STILL_THRESHOLD]
n = cg.new_Pvariable(still_config[CONF_ID], x)
n = cg.new_Pvariable(still_config[CONF_ID])
await number.register_number(
n, still_config, min_value=0, max_value=100, step=1
)
@@ -2,8 +2,6 @@
namespace esphome::ld2412 {
GateThresholdNumber::GateThresholdNumber(uint8_t gate) : gate_(gate) {}
void GateThresholdNumber::control(float value) {
this->publish_state(value);
this->parent_->set_gate_threshold();
@@ -7,10 +7,10 @@ namespace esphome::ld2412 {
class GateThresholdNumber final : public number::Number, public Parented<LD2412Component> {
public:
GateThresholdNumber(uint8_t gate);
// Not "= default": that makes new(p) T() zero-fill the object at every codegen site before the ctor runs.
GateThresholdNumber() {}
protected:
uint8_t gate_;
void control(float value) override;
};
@@ -7,7 +7,8 @@ namespace esphome::ld2412 {
class LightThresholdNumber final : public number::Number, public Parented<LD2412Component> {
public:
LightThresholdNumber() = default;
// User provided, not "= default": `new(p) LightThresholdNumber()` would zero-fill .bss that is already zero.
LightThresholdNumber() {}
protected:
void control(float value) override;

Some files were not shown because too many files have changed in this diff Show More