mirror of
https://github.com/esphome/esphome.git
synced 2026-09-19 02:58:38 +00:00
Merge branch 'esp8266-native-build-spec' into esp8266-native-ninja-emission
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}}"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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(
|
||||
|
||||
@@ -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"),
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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};
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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_;
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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});
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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.
|
||||
|
||||
+61
-29
@@ -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")
|
||||
+19
-19
@@ -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
|
||||
+35
-34
@@ -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
|
||||
+14
-10
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user