Merge pull request #9394 from esphome/bump-2025.7.0b1

2025.7.0b1
This commit is contained in:
Jesse Hills 2025-07-09 19:33:49 +12:00 committed by GitHub
commit 6178ab7513
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
895 changed files with 31269 additions and 12889 deletions

View File

@ -49,7 +49,7 @@ jobs:
with:
python-version: "3.10"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.10.0
uses: docker/setup-buildx-action@v3.11.1
- name: Set TAG
run: |

View File

@ -214,17 +214,51 @@ jobs:
if: matrix.os == 'windows-latest'
run: |
./venv/Scripts/activate
pytest -vv --cov-report=xml --tb=native -n auto tests
pytest -vv --cov-report=xml --tb=native -n auto tests --ignore=tests/integration/
- name: Run pytest
if: matrix.os == 'ubuntu-latest' || matrix.os == 'macOS-latest'
run: |
. venv/bin/activate
pytest -vv --cov-report=xml --tb=native -n auto tests
pytest -vv --cov-report=xml --tb=native -n auto tests --ignore=tests/integration/
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5.4.3
with:
token: ${{ secrets.CODECOV_TOKEN }}
integration-tests:
name: Run integration tests
runs-on: ubuntu-latest
needs:
- common
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.2.2
- name: Set up Python 3.13
id: python
uses: actions/setup-python@v5.6.0
with:
python-version: "3.13"
- name: Restore Python virtual environment
id: cache-venv
uses: actions/cache@v4.2.3
with:
path: venv
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
- name: Create Python virtual environment
if: steps.cache-venv.outputs.cache-hit != 'true'
run: |
python -m venv venv
. venv/bin/activate
python --version
pip install -r requirements.txt -r requirements_test.txt
pip install -e .
- name: Register matcher
run: echo "::add-matcher::.github/workflows/matchers/pytest.json"
- name: Run integration tests
run: |
. venv/bin/activate
pytest -vv --no-cov --tb=native -n auto tests/integration/
clang-format:
name: Check clang-format
runs-on: ubuntu-24.04
@ -494,6 +528,7 @@ jobs:
- flake8
- pylint
- pytest
- integration-tests
- pyupgrade
- clang-tidy
- list-components

View File

@ -1,28 +1,11 @@
---
name: Lock
name: Lock closed issues and PRs
on:
schedule:
- cron: "30 0 * * *"
- cron: "30 0 * * *" # Run daily at 00:30 UTC
workflow_dispatch:
permissions:
issues: write
pull-requests: write
concurrency:
group: lock
jobs:
lock:
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v5.0.1
with:
pr-inactive-days: "1"
pr-lock-reason: ""
exclude-any-pr-labels: keep-open
issue-inactive-days: "7"
issue-lock-reason: ""
exclude-any-issue-labels: keep-open
uses: esphome/workflows/.github/workflows/lock.yml@main

View File

@ -99,7 +99,7 @@ jobs:
python-version: "3.10"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.10.0
uses: docker/setup-buildx-action@v3.11.1
- name: Log in to docker hub
uses: docker/login-action@v3.4.0
@ -178,7 +178,7 @@ jobs:
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.10.0
uses: docker/setup-buildx-action@v3.11.1
- name: Log in to docker hub
if: matrix.registry == 'dockerhub'

View File

@ -4,7 +4,7 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.11.10
rev: v0.12.2
hooks:
# Run the linter.
- id: ruff
@ -12,7 +12,7 @@ repos:
# Run the formatter.
- id: ruff-format
- repo: https://github.com/PyCQA/flake8
rev: 7.2.0
rev: 7.3.0
hooks:
- id: flake8
additional_dependencies:

View File

@ -87,6 +87,7 @@ esphome/components/bp1658cj/* @Cossid
esphome/components/bp5758d/* @Cossid
esphome/components/button/* @esphome/core
esphome/components/bytebuffer/* @clydebarrow
esphome/components/camera/* @DT-art1 @bdraco
esphome/components/canbus/* @danielschramm @mvturnho
esphome/components/cap1188/* @mreditor97
esphome/components/captive_portal/* @OttoWinter
@ -124,6 +125,7 @@ esphome/components/dht/* @OttoWinter
esphome/components/display_menu_base/* @numo68
esphome/components/dps310/* @kbx81
esphome/components/ds1307/* @badbadc0ffee
esphome/components/ds2484/* @mrk-its
esphome/components/dsmr/* @glmnet @zuidwijk
esphome/components/duty_time/* @dudanov
esphome/components/ee895/* @Stock-M
@ -146,6 +148,7 @@ esphome/components/esp32_ble_client/* @jesserockz
esphome/components/esp32_ble_server/* @Rapsssito @clydebarrow @jesserockz
esphome/components/esp32_camera_web_server/* @ayufan
esphome/components/esp32_can/* @Sympatron
esphome/components/esp32_hosted/* @swoboda1337
esphome/components/esp32_improv/* @jesserockz
esphome/components/esp32_rmt/* @jesserockz
esphome/components/esp32_rmt_led_strip/* @jesserockz
@ -167,6 +170,7 @@ esphome/components/ft5x06/* @clydebarrow
esphome/components/ft63x6/* @gpambrozio
esphome/components/gcja5/* @gcormier
esphome/components/gdk101/* @Szewcson
esphome/components/gl_r01_i2c/* @pkejval
esphome/components/globals/* @esphome/core
esphome/components/gp2y1010au0f/* @zry98
esphome/components/gp8403/* @jesserockz
@ -247,9 +251,11 @@ esphome/components/libretiny_pwm/* @kuba2k2
esphome/components/light/* @esphome/core
esphome/components/lightwaverf/* @max246
esphome/components/lilygo_t5_47/touchscreen/* @jesserockz
esphome/components/ln882x/* @lamauny
esphome/components/lock/* @esphome/core
esphome/components/logger/* @esphome/core
esphome/components/logger/select/* @clydebarrow
esphome/components/lps22/* @nagisa
esphome/components/ltr390/* @latonita @sjtrny
esphome/components/ltr501/* @latonita
esphome/components/ltr_als_ps/* @latonita
@ -323,6 +329,7 @@ esphome/components/one_wire/* @ssieb
esphome/components/online_image/* @clydebarrow @guillempages
esphome/components/opentherm/* @olegtarasov
esphome/components/openthread/* @mrene
esphome/components/opt3001/* @ccutrer
esphome/components/ota/* @esphome/core
esphome/components/output/* @esphome/core
esphome/components/packet_transport/* @clydebarrow
@ -330,6 +337,7 @@ esphome/components/pca6416a/* @Mat931
esphome/components/pca9554/* @clydebarrow @hwstar
esphome/components/pcf85063/* @brogon
esphome/components/pcf8563/* @KoenBreeman
esphome/components/pi4ioe5v6408/* @jesserockz
esphome/components/pid/* @OttoWinter
esphome/components/pipsolar/* @andreashergert1984
esphome/components/pm1006/* @habbie
@ -436,6 +444,8 @@ esphome/components/sun/* @OttoWinter
esphome/components/sun_gtil2/* @Mat931
esphome/components/switch/* @esphome/core
esphome/components/switch/binary_sensor/* @ssieb
esphome/components/sx126x/* @swoboda1337
esphome/components/sx127x/* @swoboda1337
esphome/components/syslog/* @clydebarrow
esphome/components/t6615/* @tylermenezes
esphome/components/tc74/* @sethgirvan
@ -494,6 +504,7 @@ esphome/components/voice_assistant/* @jesserockz @kahrendt
esphome/components/wake_on_lan/* @clydebarrow @willwill2will54
esphome/components/watchdog/* @oarcher
esphome/components/waveshare_epaper/* @clydebarrow
esphome/components/web_server/ota/* @esphome/core
esphome/components/web_server_base/* @OttoWinter
esphome/components/web_server_idf/* @dentra
esphome/components/weikai/* @DrCoolZic
@ -520,6 +531,7 @@ esphome/components/xiaomi_lywsd03mmc/* @ahpohl
esphome/components/xiaomi_mhoc303/* @drug123
esphome/components/xiaomi_mhoc401/* @vevsvevs
esphome/components/xiaomi_rtcgq02lm/* @jesserockz
esphome/components/xiaomi_xmwsdj04mmc/* @medusalix
esphome/components/xl9535/* @mreditor97
esphome/components/xpt2046/touchscreen/* @nielsnl68 @numo68
esphome/components/xxtea/* @clydebarrow

View File

@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
# could be handy for archiving the generated documentation or if some version
# control system is used.
PROJECT_NUMBER = 2025.6.3
PROJECT_NUMBER = 2025.7.0b1
# Using the PROJECT_BRIEF tag one can provide an optional one line description
# for a project that appears at the top of each page and should give viewer a

View File

@ -34,11 +34,9 @@ from esphome.const import (
CONF_PORT,
CONF_SUBSTITUTIONS,
CONF_TOPIC,
PLATFORM_BK72XX,
PLATFORM_ESP32,
PLATFORM_ESP8266,
PLATFORM_RP2040,
PLATFORM_RTL87XX,
SECRETS_FILES,
)
from esphome.core import CORE, EsphomeError, coroutine
@ -354,7 +352,7 @@ def upload_program(config, args, host):
if CORE.target_platform in (PLATFORM_RP2040):
return upload_using_platformio(config, args.device)
if CORE.target_platform in (PLATFORM_BK72XX, PLATFORM_RTL87XX):
if CORE.is_libretiny:
return upload_using_platformio(config, host)
return 1 # Unknown target platform

View File

@ -22,6 +22,7 @@ from esphome.cpp_generator import ( # noqa: F401
TemplateArguments,
add,
add_build_flag,
add_build_unflag,
add_define,
add_global,
add_library,
@ -34,6 +35,7 @@ from esphome.cpp_generator import ( # noqa: F401
process_lambda,
progmem_array,
safe_exp,
set_cpp_standard,
statement,
static_const_array,
templatable,

View File

@ -4,6 +4,7 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <cmath>
#include <numbers>
#ifdef USE_ESP8266
#include <core_esp8266_waveform.h>
@ -193,18 +194,17 @@ void AcDimmer::setup() {
setTimer1Callback(&timer_interrupt);
#endif
#ifdef USE_ESP32
// 80 Divider -> 1 count=1µs
dimmer_timer = timerBegin(0, 80, true);
timerAttachInterrupt(dimmer_timer, &AcDimmerDataStore::s_timer_intr, true);
// timer frequency of 1mhz
dimmer_timer = timerBegin(1000000);
timerAttachInterrupt(dimmer_timer, &AcDimmerDataStore::s_timer_intr);
// For ESP32, we can't use dynamic interval calculation because the timerX functions
// are not callable from ISR (placed in flash storage).
// Here we just use an interrupt firing every 50 µs.
timerAlarmWrite(dimmer_timer, 50, true);
timerAlarmEnable(dimmer_timer);
timerAlarm(dimmer_timer, 50, true, 0);
#endif
}
void AcDimmer::write_state(float state) {
state = std::acos(1 - (2 * state)) / 3.14159; // RMS power compensation
state = std::acos(1 - (2 * state)) / std::numbers::pi; // RMS power compensation
auto new_value = static_cast<uint16_t>(roundf(state * 65535));
if (new_value != 0 && this->store_.value == 0)
this->store_.init_cycle = this->init_with_half_cycle_;

View File

@ -10,8 +10,15 @@ from esphome.components.esp32.const import (
VARIANT_ESP32S2,
VARIANT_ESP32S3,
)
from esphome.config_helpers import filter_source_files_from_platform
import esphome.config_validation as cv
from esphome.const import CONF_ANALOG, CONF_INPUT, CONF_NUMBER, PLATFORM_ESP8266
from esphome.const import (
CONF_ANALOG,
CONF_INPUT,
CONF_NUMBER,
PLATFORM_ESP8266,
PlatformFramework,
)
from esphome.core import CORE
CODEOWNERS = ["@esphome/core"]
@ -229,3 +236,20 @@ def validate_adc_pin(value):
)(value)
raise NotImplementedError
FILTER_SOURCE_FILES = filter_source_files_from_platform(
{
"adc_sensor_esp32.cpp": {
PlatformFramework.ESP32_ARDUINO,
PlatformFramework.ESP32_IDF,
},
"adc_sensor_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO},
"adc_sensor_rp2040.cpp": {PlatformFramework.RP2040_ARDUINO},
"adc_sensor_libretiny.cpp": {
PlatformFramework.BK72XX_ARDUINO,
PlatformFramework.RTL87XX_ARDUINO,
PlatformFramework.LN882X_ARDUINO,
},
}
)

View File

@ -15,8 +15,7 @@ namespace adc {
#ifdef USE_ESP32
// clang-format off
#if (ESP_IDF_VERSION_MAJOR == 4 && ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 7)) || \
(ESP_IDF_VERSION_MAJOR == 5 && \
#if (ESP_IDF_VERSION_MAJOR == 5 && \
((ESP_IDF_VERSION_MINOR == 0 && ESP_IDF_VERSION_PATCH >= 5) || \
(ESP_IDF_VERSION_MINOR == 1 && ESP_IDF_VERSION_PATCH >= 3) || \
(ESP_IDF_VERSION_MINOR >= 2)) \
@ -28,19 +27,24 @@ static const adc_atten_t ADC_ATTEN_DB_12_COMPAT = ADC_ATTEN_DB_11;
#endif
#endif // USE_ESP32
enum class SamplingMode : uint8_t { AVG = 0, MIN = 1, MAX = 2 };
enum class SamplingMode : uint8_t {
AVG = 0,
MIN = 1,
MAX = 2,
};
const LogString *sampling_mode_to_str(SamplingMode mode);
class Aggregator {
public:
Aggregator(SamplingMode mode);
void add_sample(uint32_t value);
uint32_t aggregate();
Aggregator(SamplingMode mode);
protected:
SamplingMode mode_{SamplingMode::AVG};
uint32_t aggr_{0};
uint32_t samples_{0};
SamplingMode mode_{SamplingMode::AVG};
};
class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage_sampler::VoltageSampler {
@ -81,9 +85,9 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage
#endif // USE_RP2040
protected:
InternalGPIOPin *pin_;
bool output_raw_{false};
uint8_t sample_count_{1};
bool output_raw_{false};
InternalGPIOPin *pin_;
SamplingMode sampling_mode_{SamplingMode::AVG};
#ifdef USE_RP2040
@ -95,11 +99,7 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage
adc1_channel_t channel1_{ADC1_CHANNEL_MAX};
adc2_channel_t channel2_{ADC2_CHANNEL_MAX};
bool autorange_{false};
#if ESP_IDF_VERSION_MAJOR >= 5
esp_adc_cal_characteristics_t cal_characteristics_[SOC_ADC_ATTEN_NUM] = {};
#else
esp_adc_cal_characteristics_t cal_characteristics_[ADC_ATTEN_MAX] = {};
#endif // ESP_IDF_VERSION_MAJOR
#endif // USE_ESP32
};

View File

@ -61,7 +61,7 @@ uint32_t Aggregator::aggregate() {
void ADCSensor::update() {
float value_v = this->sample();
ESP_LOGV(TAG, "'%s': Got voltage=%.4fV", this->get_name().c_str(), value_v);
ESP_LOGV(TAG, "'%s': Voltage=%.4fV", this->get_name().c_str(), value_v);
this->publish_state(value_v);
}

View File

@ -55,32 +55,40 @@ void ADCSensor::setup() {
}
void ADCSensor::dump_config() {
static const char *const ATTEN_AUTO_STR = "auto";
static const char *const ATTEN_0DB_STR = "0 db";
static const char *const ATTEN_2_5DB_STR = "2.5 db";
static const char *const ATTEN_6DB_STR = "6 db";
static const char *const ATTEN_12DB_STR = "12 db";
const char *atten_str = ATTEN_AUTO_STR;
LOG_SENSOR("", "ADC Sensor", this);
LOG_PIN(" Pin: ", this->pin_);
if (this->autorange_) {
ESP_LOGCONFIG(TAG, " Attenuation: auto");
} else {
if (!this->autorange_) {
switch (this->attenuation_) {
case ADC_ATTEN_DB_0:
ESP_LOGCONFIG(TAG, " Attenuation: 0db");
atten_str = ATTEN_0DB_STR;
break;
case ADC_ATTEN_DB_2_5:
ESP_LOGCONFIG(TAG, " Attenuation: 2.5db");
atten_str = ATTEN_2_5DB_STR;
break;
case ADC_ATTEN_DB_6:
ESP_LOGCONFIG(TAG, " Attenuation: 6db");
atten_str = ATTEN_6DB_STR;
break;
case ADC_ATTEN_DB_12_COMPAT:
ESP_LOGCONFIG(TAG, " Attenuation: 12db");
atten_str = ATTEN_12DB_STR;
break;
default: // This is to satisfy the unused ADC_ATTEN_MAX
break;
}
}
ESP_LOGCONFIG(TAG,
" Attenuation: %s\n"
" Samples: %i\n"
" Sampling mode: %s",
this->sample_count_, LOG_STR_ARG(sampling_mode_to_str(this->sampling_mode_)));
atten_str, this->sample_count_, LOG_STR_ARG(sampling_mode_to_str(this->sampling_mode_)));
LOG_UPDATE_INTERVAL(this);
}

View File

@ -85,8 +85,6 @@ class ADE7880 : public i2c::I2CDevice, public PollingComponent {
void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
protected:
ADE7880Store store_{};
InternalGPIOPin *irq0_pin_{nullptr};

View File

@ -49,7 +49,6 @@ class ADS1115Component : public Component, public i2c::I2CDevice {
void setup() override;
void dump_config() override;
/// HARDWARE_LATE setup priority
float get_setup_priority() const override { return setup_priority::DATA; }
void set_continuous_mode(bool continuous_mode) { continuous_mode_ = continuous_mode; }
/// Helper method to request a measurement from a sensor.

View File

@ -34,7 +34,6 @@ class ADS1118 : public Component,
ADS1118() = default;
void setup() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
/// Helper method to request a measurement from a sensor.
float request_measurement(ADS1118Multiplexer multiplexer, ADS1118Gain gain, bool temperature_mode);

View File

@ -31,8 +31,6 @@ class AGS10Component : public PollingComponent, public i2c::I2CDevice {
void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
/**
* Modifies target address of AGS10.
*

View File

@ -66,7 +66,6 @@ class AIC3204 : public audio_dac::AudioDac, public Component, public i2c::I2CDev
public:
void setup() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
bool set_mute_off() override;
bool set_mute_on() override;

View File

@ -14,8 +14,8 @@ from esphome.const import (
CONF_WEB_SERVER,
)
from esphome.core import CORE, coroutine_with_priority
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
from esphome.cpp_generator import MockObjClass
from esphome.cpp_helpers import setup_entity
CODEOWNERS = ["@grahambrown11", "@hwstar"]
IS_PLATFORM_COMPONENT = True
@ -149,6 +149,9 @@ _ALARM_CONTROL_PANEL_SCHEMA = (
)
_ALARM_CONTROL_PANEL_SCHEMA.add_extra(entity_duplicate_validator("alarm_control_panel"))
def alarm_control_panel_schema(
class_: MockObjClass,
*,
@ -190,7 +193,7 @@ ALARM_CONTROL_PANEL_CONDITION_SCHEMA = maybe_simple_id(
async def setup_alarm_control_panel_core_(var, config):
await setup_entity(var, config)
await setup_entity(var, config, "alarm_control_panel")
for conf in config.get(CONF_ON_STATE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)

View File

@ -41,7 +41,6 @@ class Alpha3 : public esphome::ble_client::BLEClientNode, public PollingComponen
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
void set_flow_sensor(sensor::Sensor *sensor) { this->flow_sensor_ = sensor; }
void set_head_sensor(sensor::Sensor *sensor) { this->head_sensor_ = sensor; }
void set_power_sensor(sensor::Sensor *sensor) { this->power_sensor_ = sensor; }

View File

@ -22,7 +22,6 @@ class Am43Component : public cover::Cover, public esphome::ble_client::BLEClient
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
cover::CoverTraits get_traits() override;
void set_pin(uint16_t pin) { this->pin_ = pin; }
void set_invert_position(bool invert_position) { this->invert_position_ = invert_position; }

View File

@ -22,7 +22,6 @@ class Am43 : public esphome::ble_client::BLEClientNode, public PollingComponent
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
void set_battery(sensor::Sensor *battery) { battery_ = battery; }
void set_illuminance(sensor::Sensor *illuminance) { illuminance_ = illuminance; }

View File

@ -12,8 +12,6 @@ class AnalogThresholdBinarySensor : public Component, public binary_sensor::Bina
void dump_config() override;
void setup() override;
float get_setup_priority() const override { return setup_priority::DATA; }
void set_sensor(sensor::Sensor *analog_sensor);
template<typename T> void set_upper_threshold(T upper_threshold) { this->upper_threshold_ = upper_threshold; }
template<typename T> void set_lower_threshold(T lower_threshold) { this->lower_threshold_ = lower_threshold; }

View File

@ -17,7 +17,11 @@ void Anova::setup() {
this->current_request_ = 0;
}
void Anova::loop() {}
void Anova::loop() {
// Parent BLEClientNode has a loop() method, but this component uses
// polling via update() and BLE callbacks so loop isn't needed
this->disable_loop();
}
void Anova::control(const ClimateCall &call) {
if (call.get_mode().has_value()) {

View File

@ -26,7 +26,6 @@ class Anova : public climate::Climate, public esphome::ble_client::BLEClientNode
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
climate::ClimateTraits traits() override {
auto traits = climate::ClimateTraits();
traits.set_supports_current_temperature(true);

View File

@ -23,7 +23,7 @@ void APDS9960::setup() {
return;
}
if (id != 0xAB && id != 0x9C && id != 0xA8) { // APDS9960 all should have one of these IDs
if (id != 0xAB && id != 0x9C && id != 0xA8 && id != 0x9E) { // APDS9960 all should have one of these IDs
this->error_code_ = WRONG_ID;
this->mark_failed();
return;

View File

@ -3,6 +3,7 @@ import base64
from esphome import automation
from esphome.automation import Condition
import esphome.codegen as cg
from esphome.config_helpers import get_logger_level
import esphome.config_validation as cv
from esphome.const import (
CONF_ACTION,
@ -110,9 +111,10 @@ CONFIG_SCHEMA = cv.All(
): 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.positive_time_period_milliseconds,
cv.Optional(CONF_BATCH_DELAY, default="100ms"): cv.All(
cv.positive_time_period_milliseconds,
cv.Range(max=cv.TimePeriod(milliseconds=65535)),
),
cv.Optional(CONF_ON_CLIENT_CONNECTED): automation.validate_automation(
single=True
),
@ -131,27 +133,32 @@ async def to_code(config):
await cg.register_component(var, config)
cg.add(var.set_port(config[CONF_PORT]))
cg.add(var.set_password(config[CONF_PASSWORD]))
if config[CONF_PASSWORD]:
cg.add_define("USE_API_PASSWORD")
cg.add(var.set_password(config[CONF_PASSWORD]))
cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT]))
cg.add(var.set_batch_delay(config[CONF_BATCH_DELAY]))
for conf in config.get(CONF_ACTIONS, []):
template_args = []
func_args = []
service_arg_names = []
for name, var_ in conf[CONF_VARIABLES].items():
native = SERVICE_ARG_NATIVE_TYPES[var_]
template_args.append(native)
func_args.append((native, name))
service_arg_names.append(name)
templ = cg.TemplateArguments(*template_args)
trigger = cg.new_Pvariable(
conf[CONF_TRIGGER_ID], templ, conf[CONF_ACTION], service_arg_names
)
cg.add(var.register_user_service(trigger))
await automation.build_automation(trigger, func_args, conf)
if actions := config.get(CONF_ACTIONS, []):
cg.add_define("USE_API_YAML_SERVICES")
for conf in actions:
template_args = []
func_args = []
service_arg_names = []
for name, var_ in conf[CONF_VARIABLES].items():
native = SERVICE_ARG_NATIVE_TYPES[var_]
template_args.append(native)
func_args.append((native, name))
service_arg_names.append(name)
templ = cg.TemplateArguments(*template_args)
trigger = cg.new_Pvariable(
conf[CONF_TRIGGER_ID], templ, conf[CONF_ACTION], service_arg_names
)
cg.add(var.register_user_service(trigger))
await automation.build_automation(trigger, func_args, conf)
if CONF_ON_CLIENT_CONNECTED in config:
cg.add_define("USE_API_CLIENT_CONNECTED_TRIGGER")
await automation.build_automation(
var.get_client_connected_trigger(),
[(cg.std_string, "client_info"), (cg.std_string, "client_address")],
@ -159,6 +166,7 @@ async def to_code(config):
)
if CONF_ON_CLIENT_DISCONNECTED in config:
cg.add_define("USE_API_CLIENT_DISCONNECTED_TRIGGER")
await automation.build_automation(
var.get_client_disconnected_trigger(),
[(cg.std_string, "client_info"), (cg.std_string, "client_address")],
@ -177,7 +185,7 @@ async def to_code(config):
# and plaintext disabled. Only a factory reset can remove it.
cg.add_define("USE_API_PLAINTEXT")
cg.add_define("USE_API_NOISE")
cg.add_library("esphome/noise-c", "0.1.6")
cg.add_library("esphome/noise-c", "0.1.10")
else:
cg.add_define("USE_API_PLAINTEXT")
@ -306,3 +314,17 @@ async def homeassistant_tag_scanned_to_code(config, action_id, template_arg, arg
@automation.register_condition("api.connected", APIConnectedCondition, {})
async def api_connected_to_code(config, condition_id, template_arg, args):
return cg.new_Pvariable(condition_id, template_arg)
def FILTER_SOURCE_FILES() -> list[str]:
"""Filter out api_pb2_dump.cpp when proto message dumping is not enabled."""
# api_pb2_dump.cpp is only needed when HAS_PROTO_MESSAGE_DUMP is defined
# This is a particularly large file that still needs to be opened and read
# all the way to the end even when ifdef'd out
#
# HAS_PROTO_MESSAGE_DUMP is defined when ESPHOME_LOG_HAS_VERY_VERBOSE is set,
# which happens when the logger level is VERY_VERBOSE
if get_logger_level() != "VERY_VERBOSE":
return ["api_pb2_dump.cpp"]
return []

View File

@ -188,6 +188,17 @@ message DeviceInfoRequest {
// Empty
}
message AreaInfo {
uint32 area_id = 1;
string name = 2;
}
message DeviceInfo {
uint32 device_id = 1;
string name = 2;
uint32 area_id = 3;
}
message DeviceInfoResponse {
option (id) = 10;
option (source) = SOURCE_SERVER;
@ -236,6 +247,12 @@ message DeviceInfoResponse {
// Supports receiving and saving api encryption key
bool api_encryption_supported = 19;
repeated DeviceInfo devices = 20;
repeated AreaInfo areas = 21;
// Top-level area info to phase out suggested_area
AreaInfo area = 22;
}
message ListEntitiesRequest {
@ -280,6 +297,7 @@ message ListEntitiesBinarySensorResponse {
bool disabled_by_default = 7;
string icon = 8;
EntityCategory entity_category = 9;
uint32 device_id = 10;
}
message BinarySensorStateResponse {
option (id) = 21;
@ -293,6 +311,7 @@ message BinarySensorStateResponse {
// If the binary sensor does not have a valid state yet.
// Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller
bool missing_state = 3;
uint32 device_id = 4;
}
// ==================== COVER ====================
@ -315,6 +334,7 @@ message ListEntitiesCoverResponse {
string icon = 10;
EntityCategory entity_category = 11;
bool supports_stop = 12;
uint32 device_id = 13;
}
enum LegacyCoverState {
@ -341,6 +361,7 @@ message CoverStateResponse {
float position = 3;
float tilt = 4;
CoverOperation current_operation = 5;
uint32 device_id = 6;
}
enum LegacyCoverCommand {
@ -388,6 +409,7 @@ message ListEntitiesFanResponse {
string icon = 10;
EntityCategory entity_category = 11;
repeated string supported_preset_modes = 12;
uint32 device_id = 13;
}
enum FanSpeed {
FAN_SPEED_LOW = 0;
@ -412,6 +434,7 @@ message FanStateResponse {
FanDirection direction = 5;
int32 speed_level = 6;
string preset_mode = 7;
uint32 device_id = 8;
}
message FanCommandRequest {
option (id) = 31;
@ -471,6 +494,7 @@ message ListEntitiesLightResponse {
bool disabled_by_default = 13;
string icon = 14;
EntityCategory entity_category = 15;
uint32 device_id = 16;
}
message LightStateResponse {
option (id) = 24;
@ -492,6 +516,7 @@ message LightStateResponse {
float cold_white = 12;
float warm_white = 13;
string effect = 9;
uint32 device_id = 14;
}
message LightCommandRequest {
option (id) = 32;
@ -563,6 +588,7 @@ message ListEntitiesSensorResponse {
SensorLastResetType legacy_last_reset_type = 11;
bool disabled_by_default = 12;
EntityCategory entity_category = 13;
uint32 device_id = 14;
}
message SensorStateResponse {
option (id) = 25;
@ -576,6 +602,7 @@ message SensorStateResponse {
// If the sensor does not have a valid state yet.
// Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller
bool missing_state = 3;
uint32 device_id = 4;
}
// ==================== SWITCH ====================
@ -595,6 +622,7 @@ message ListEntitiesSwitchResponse {
bool disabled_by_default = 7;
EntityCategory entity_category = 8;
string device_class = 9;
uint32 device_id = 10;
}
message SwitchStateResponse {
option (id) = 26;
@ -605,6 +633,7 @@ message SwitchStateResponse {
fixed32 key = 1;
bool state = 2;
uint32 device_id = 3;
}
message SwitchCommandRequest {
option (id) = 33;
@ -632,6 +661,7 @@ message ListEntitiesTextSensorResponse {
bool disabled_by_default = 6;
EntityCategory entity_category = 7;
string device_class = 8;
uint32 device_id = 9;
}
message TextSensorStateResponse {
option (id) = 27;
@ -645,6 +675,7 @@ message TextSensorStateResponse {
// If the text sensor does not have a valid state yet.
// Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller
bool missing_state = 3;
uint32 device_id = 4;
}
// ==================== SUBSCRIBE LOGS ====================
@ -805,7 +836,7 @@ message ListEntitiesCameraResponse {
option (id) = 43;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_ESP32_CAMERA";
option (ifdef) = "USE_CAMERA";
string object_id = 1;
fixed32 key = 2;
@ -814,12 +845,13 @@ message ListEntitiesCameraResponse {
bool disabled_by_default = 5;
string icon = 6;
EntityCategory entity_category = 7;
uint32 device_id = 8;
}
message CameraImageResponse {
option (id) = 44;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_ESP32_CAMERA";
option (ifdef) = "USE_CAMERA";
fixed32 key = 1;
bytes data = 2;
@ -828,7 +860,7 @@ message CameraImageResponse {
message CameraImageRequest {
option (id) = 45;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_ESP32_CAMERA";
option (ifdef) = "USE_CAMERA";
option (no_delay) = true;
bool single = 1;
@ -916,6 +948,7 @@ message ListEntitiesClimateResponse {
bool supports_target_humidity = 23;
float visual_min_humidity = 24;
float visual_max_humidity = 25;
uint32 device_id = 26;
}
message ClimateStateResponse {
option (id) = 47;
@ -940,6 +973,7 @@ message ClimateStateResponse {
string custom_preset = 13;
float current_humidity = 14;
float target_humidity = 15;
uint32 device_id = 16;
}
message ClimateCommandRequest {
option (id) = 48;
@ -999,6 +1033,7 @@ message ListEntitiesNumberResponse {
string unit_of_measurement = 11;
NumberMode mode = 12;
string device_class = 13;
uint32 device_id = 14;
}
message NumberStateResponse {
option (id) = 50;
@ -1012,6 +1047,7 @@ message NumberStateResponse {
// If the number does not have a valid state yet.
// Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller
bool missing_state = 3;
uint32 device_id = 4;
}
message NumberCommandRequest {
option (id) = 51;
@ -1039,6 +1075,7 @@ message ListEntitiesSelectResponse {
repeated string options = 6;
bool disabled_by_default = 7;
EntityCategory entity_category = 8;
uint32 device_id = 9;
}
message SelectStateResponse {
option (id) = 53;
@ -1052,6 +1089,7 @@ message SelectStateResponse {
// If the select does not have a valid state yet.
// Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller
bool missing_state = 3;
uint32 device_id = 4;
}
message SelectCommandRequest {
option (id) = 54;
@ -1081,6 +1119,7 @@ message ListEntitiesSirenResponse {
bool supports_duration = 8;
bool supports_volume = 9;
EntityCategory entity_category = 10;
uint32 device_id = 11;
}
message SirenStateResponse {
option (id) = 56;
@ -1091,6 +1130,7 @@ message SirenStateResponse {
fixed32 key = 1;
bool state = 2;
uint32 device_id = 3;
}
message SirenCommandRequest {
option (id) = 57;
@ -1144,6 +1184,7 @@ message ListEntitiesLockResponse {
// Not yet implemented:
string code_format = 11;
uint32 device_id = 12;
}
message LockStateResponse {
option (id) = 59;
@ -1153,6 +1194,7 @@ message LockStateResponse {
option (no_delay) = true;
fixed32 key = 1;
LockState state = 2;
uint32 device_id = 3;
}
message LockCommandRequest {
option (id) = 60;
@ -1183,6 +1225,7 @@ message ListEntitiesButtonResponse {
bool disabled_by_default = 6;
EntityCategory entity_category = 7;
string device_class = 8;
uint32 device_id = 9;
}
message ButtonCommandRequest {
option (id) = 62;
@ -1238,6 +1281,8 @@ message ListEntitiesMediaPlayerResponse {
bool supports_pause = 8;
repeated MediaPlayerSupportedFormat supported_formats = 9;
uint32 device_id = 10;
}
message MediaPlayerStateResponse {
option (id) = 64;
@ -1249,6 +1294,7 @@ message MediaPlayerStateResponse {
MediaPlayerState state = 2;
float volume = 3;
bool muted = 4;
uint32 device_id = 5;
}
message MediaPlayerCommandRequest {
option (id) = 65;
@ -1778,6 +1824,7 @@ message ListEntitiesAlarmControlPanelResponse {
uint32 supported_features = 8;
bool requires_code = 9;
bool requires_code_to_arm = 10;
uint32 device_id = 11;
}
message AlarmControlPanelStateResponse {
@ -1788,6 +1835,7 @@ message AlarmControlPanelStateResponse {
option (no_delay) = true;
fixed32 key = 1;
AlarmControlPanelState state = 2;
uint32 device_id = 3;
}
message AlarmControlPanelCommandRequest {
@ -1823,6 +1871,7 @@ message ListEntitiesTextResponse {
uint32 max_length = 9;
string pattern = 10;
TextMode mode = 11;
uint32 device_id = 12;
}
message TextStateResponse {
option (id) = 98;
@ -1836,6 +1885,7 @@ message TextStateResponse {
// If the Text does not have a valid state yet.
// Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller
bool missing_state = 3;
uint32 device_id = 4;
}
message TextCommandRequest {
option (id) = 99;
@ -1863,6 +1913,7 @@ message ListEntitiesDateResponse {
string icon = 5;
bool disabled_by_default = 6;
EntityCategory entity_category = 7;
uint32 device_id = 8;
}
message DateStateResponse {
option (id) = 101;
@ -1878,6 +1929,7 @@ message DateStateResponse {
uint32 year = 3;
uint32 month = 4;
uint32 day = 5;
uint32 device_id = 6;
}
message DateCommandRequest {
option (id) = 102;
@ -1906,6 +1958,7 @@ message ListEntitiesTimeResponse {
string icon = 5;
bool disabled_by_default = 6;
EntityCategory entity_category = 7;
uint32 device_id = 8;
}
message TimeStateResponse {
option (id) = 104;
@ -1921,6 +1974,7 @@ message TimeStateResponse {
uint32 hour = 3;
uint32 minute = 4;
uint32 second = 5;
uint32 device_id = 6;
}
message TimeCommandRequest {
option (id) = 105;
@ -1952,6 +2006,7 @@ message ListEntitiesEventResponse {
string device_class = 8;
repeated string event_types = 9;
uint32 device_id = 10;
}
message EventResponse {
option (id) = 108;
@ -1961,6 +2016,7 @@ message EventResponse {
fixed32 key = 1;
string event_type = 2;
uint32 device_id = 3;
}
// ==================== VALVE ====================
@ -1983,6 +2039,7 @@ message ListEntitiesValveResponse {
bool assumed_state = 9;
bool supports_position = 10;
bool supports_stop = 11;
uint32 device_id = 12;
}
enum ValveOperation {
@ -2000,6 +2057,7 @@ message ValveStateResponse {
fixed32 key = 1;
float position = 2;
ValveOperation current_operation = 3;
uint32 device_id = 4;
}
message ValveCommandRequest {
@ -2029,6 +2087,7 @@ message ListEntitiesDateTimeResponse {
string icon = 5;
bool disabled_by_default = 6;
EntityCategory entity_category = 7;
uint32 device_id = 8;
}
message DateTimeStateResponse {
option (id) = 113;
@ -2042,6 +2101,7 @@ message DateTimeStateResponse {
// Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller
bool missing_state = 2;
fixed32 epoch_seconds = 3;
uint32 device_id = 4;
}
message DateTimeCommandRequest {
option (id) = 114;
@ -2069,6 +2129,7 @@ message ListEntitiesUpdateResponse {
bool disabled_by_default = 6;
EntityCategory entity_category = 7;
string device_class = 8;
uint32 device_id = 9;
}
message UpdateStateResponse {
option (id) = 117;
@ -2087,6 +2148,7 @@ message UpdateStateResponse {
string title = 8;
string release_summary = 9;
string release_url = 10;
uint32 device_id = 11;
}
enum UpdateCommand {
UPDATE_COMMAND_NONE = 0;

File diff suppressed because it is too large Load Diff

View File

@ -18,10 +18,13 @@ namespace api {
// Keepalive timeout in milliseconds
static constexpr uint32_t KEEPALIVE_TIMEOUT_MS = 60000;
// Maximum number of entities to process in a single batch during initial state/info sending
static constexpr size_t MAX_INITIAL_PER_BATCH = 20;
class APIConnection : public APIServerConnection {
public:
friend class APIServer;
friend class ListEntitiesIterator;
APIConnection(std::unique_ptr<socket::Socket> socket, APIServer *parent);
virtual ~APIConnection();
@ -34,98 +37,79 @@ class APIConnection : public APIServerConnection {
}
#ifdef USE_BINARY_SENSOR
bool send_binary_sensor_state(binary_sensor::BinarySensor *binary_sensor);
void send_binary_sensor_info(binary_sensor::BinarySensor *binary_sensor);
#endif
#ifdef USE_COVER
bool send_cover_state(cover::Cover *cover);
void send_cover_info(cover::Cover *cover);
void cover_command(const CoverCommandRequest &msg) override;
#endif
#ifdef USE_FAN
bool send_fan_state(fan::Fan *fan);
void send_fan_info(fan::Fan *fan);
void fan_command(const FanCommandRequest &msg) override;
#endif
#ifdef USE_LIGHT
bool send_light_state(light::LightState *light);
void send_light_info(light::LightState *light);
void light_command(const LightCommandRequest &msg) override;
#endif
#ifdef USE_SENSOR
bool send_sensor_state(sensor::Sensor *sensor);
void send_sensor_info(sensor::Sensor *sensor);
#endif
#ifdef USE_SWITCH
bool send_switch_state(switch_::Switch *a_switch);
void send_switch_info(switch_::Switch *a_switch);
void switch_command(const SwitchCommandRequest &msg) override;
#endif
#ifdef USE_TEXT_SENSOR
bool send_text_sensor_state(text_sensor::TextSensor *text_sensor);
void send_text_sensor_info(text_sensor::TextSensor *text_sensor);
#endif
#ifdef USE_ESP32_CAMERA
void set_camera_state(std::shared_ptr<esp32_camera::CameraImage> image);
void send_camera_info(esp32_camera::ESP32Camera *camera);
#ifdef USE_CAMERA
void set_camera_state(std::shared_ptr<camera::CameraImage> image);
void camera_image(const CameraImageRequest &msg) override;
#endif
#ifdef USE_CLIMATE
bool send_climate_state(climate::Climate *climate);
void send_climate_info(climate::Climate *climate);
void climate_command(const ClimateCommandRequest &msg) override;
#endif
#ifdef USE_NUMBER
bool send_number_state(number::Number *number);
void send_number_info(number::Number *number);
void number_command(const NumberCommandRequest &msg) override;
#endif
#ifdef USE_DATETIME_DATE
bool send_date_state(datetime::DateEntity *date);
void send_date_info(datetime::DateEntity *date);
void date_command(const DateCommandRequest &msg) override;
#endif
#ifdef USE_DATETIME_TIME
bool send_time_state(datetime::TimeEntity *time);
void send_time_info(datetime::TimeEntity *time);
void time_command(const TimeCommandRequest &msg) override;
#endif
#ifdef USE_DATETIME_DATETIME
bool send_datetime_state(datetime::DateTimeEntity *datetime);
void send_datetime_info(datetime::DateTimeEntity *datetime);
void datetime_command(const DateTimeCommandRequest &msg) override;
#endif
#ifdef USE_TEXT
bool send_text_state(text::Text *text);
void send_text_info(text::Text *text);
void text_command(const TextCommandRequest &msg) override;
#endif
#ifdef USE_SELECT
bool send_select_state(select::Select *select);
void send_select_info(select::Select *select);
void select_command(const SelectCommandRequest &msg) override;
#endif
#ifdef USE_BUTTON
void send_button_info(button::Button *button);
void button_command(const ButtonCommandRequest &msg) override;
#endif
#ifdef USE_LOCK
bool send_lock_state(lock::Lock *a_lock);
void send_lock_info(lock::Lock *a_lock);
void lock_command(const LockCommandRequest &msg) override;
#endif
#ifdef USE_VALVE
bool send_valve_state(valve::Valve *valve);
void send_valve_info(valve::Valve *valve);
void valve_command(const ValveCommandRequest &msg) override;
#endif
#ifdef USE_MEDIA_PLAYER
bool send_media_player_state(media_player::MediaPlayer *media_player);
void send_media_player_info(media_player::MediaPlayer *media_player);
void media_player_command(const MediaPlayerCommandRequest &msg) override;
#endif
bool try_send_log_message(int level, const char *tag, const char *line);
bool try_send_log_message(int level, const char *tag, const char *line, size_t message_len);
void send_homeassistant_service_call(const HomeassistantServiceResponse &call) {
if (!this->service_call_subscription_)
if (!this->flags_.service_call_subscription)
return;
this->send_message(call);
}
@ -167,26 +151,22 @@ class APIConnection : public APIServerConnection {
#ifdef USE_ALARM_CONTROL_PANEL
bool send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel);
void send_alarm_control_panel_info(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel);
void alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) override;
#endif
#ifdef USE_EVENT
void send_event(event::Event *event, const std::string &event_type);
void send_event_info(event::Event *event);
#endif
#ifdef USE_UPDATE
bool send_update_state(update::UpdateEntity *update);
void send_update_info(update::UpdateEntity *update);
void update_command(const UpdateCommandRequest &msg) override;
#endif
void on_disconnect_response(const DisconnectResponse &value) override;
void on_ping_response(const PingResponse &value) override {
// we initiated ping
this->ping_retries_ = 0;
this->sent_ping_ = false;
this->flags_.sent_ping = false;
}
void on_home_assistant_state_response(const HomeAssistantStateResponse &msg) override;
#ifdef USE_HOMEASSISTANT_TIME
@ -199,16 +179,16 @@ class APIConnection : public APIServerConnection {
DeviceInfoResponse device_info(const DeviceInfoRequest &msg) override;
void list_entities(const ListEntitiesRequest &msg) override { this->list_entities_iterator_.begin(); }
void subscribe_states(const SubscribeStatesRequest &msg) override {
this->state_subscription_ = true;
this->flags_.state_subscription = true;
this->initial_state_iterator_.begin();
}
void subscribe_logs(const SubscribeLogsRequest &msg) override {
this->log_subscription_ = msg.level;
this->flags_.log_subscription = msg.level;
if (msg.dump_config)
App.schedule_dump_config();
}
void subscribe_homeassistant_services(const SubscribeHomeassistantServicesRequest &msg) override {
this->service_call_subscription_ = true;
this->flags_.service_call_subscription = true;
}
void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) override;
GetTimeResponse get_time(const GetTimeRequest &msg) override {
@ -220,9 +200,12 @@ class APIConnection : public APIServerConnection {
NoiseEncryptionSetKeyResponse noise_encryption_set_key(const NoiseEncryptionSetKeyRequest &msg) override;
#endif
bool is_authenticated() override { return this->connection_state_ == ConnectionState::AUTHENTICATED; }
bool is_authenticated() override {
return static_cast<ConnectionState>(this->flags_.connection_state) == ConnectionState::AUTHENTICATED;
}
bool is_connection_setup() override {
return this->connection_state_ == ConnectionState ::CONNECTED || this->is_authenticated();
return static_cast<ConnectionState>(this->flags_.connection_state) == ConnectionState::CONNECTED ||
this->is_authenticated();
}
void on_fatal_error() override;
void on_unauthenticated_access() override;
@ -275,7 +258,13 @@ class APIConnection : public APIServerConnection {
bool try_to_clear_buffer(bool log_out_of_space);
bool send_buffer(ProtoWriteBuffer buffer, uint16_t message_type) override;
std::string get_client_combined_info() const { return this->client_combined_info_; }
std::string get_client_combined_info() const {
if (this->client_info_ == this->client_peername_) {
// Before Hello message, both are the same (just IP:port)
return this->client_info_;
}
return this->client_info_ + " (" + this->client_peername_ + ")";
}
// Buffer allocator methods for batch processing
ProtoWriteBuffer allocate_single_message_buffer(uint16_t size);
@ -295,17 +284,42 @@ class APIConnection : public APIServerConnection {
response.icon = entity->get_icon();
response.disabled_by_default = entity->is_disabled_by_default();
response.entity_category = static_cast<enums::EntityCategory>(entity->get_entity_category());
#ifdef USE_DEVICES
response.device_id = entity->get_device_id();
#endif
}
// Helper function to fill common entity state fields
static void fill_entity_state_base(esphome::EntityBase *entity, StateResponseProtoMessage &response) {
response.key = entity->get_object_id_hash();
#ifdef USE_DEVICES
response.device_id = entity->get_device_id();
#endif
}
// Non-template helper to encode any ProtoMessage
static uint16_t encode_message_to_buffer(ProtoMessage &msg, uint16_t message_type, APIConnection *conn,
uint32_t remaining_size, bool is_single);
#ifdef USE_VOICE_ASSISTANT
// Helper to check voice assistant validity and connection ownership
inline bool check_voice_assistant_api_connection_() const;
#endif
// Helper method to process multiple entities from an iterator in a batch
template<typename Iterator> void process_iterator_batch_(Iterator &iterator) {
size_t initial_size = this->deferred_batch_.size();
while (!iterator.completed() && (this->deferred_batch_.size() - initial_size) < MAX_INITIAL_PER_BATCH) {
iterator.advance();
}
// If the batch is full, process it immediately
// Note: iterator.advance() already calls schedule_batch_() via schedule_message_()
if (this->deferred_batch_.size() >= MAX_INITIAL_PER_BATCH) {
this->process_batch_();
}
}
#ifdef USE_BINARY_SENSOR
static uint16_t try_send_binary_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single);
@ -416,7 +430,7 @@ class APIConnection : public APIServerConnection {
static uint16_t try_send_update_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single);
#endif
#ifdef USE_ESP32_CAMERA
#ifdef USE_CAMERA
static uint16_t try_send_camera_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single);
#endif
@ -432,124 +446,82 @@ class APIConnection : public APIServerConnection {
// Helper function to get estimated message size for buffer pre-allocation
static uint16_t get_estimated_message_size(uint16_t message_type);
enum class ConnectionState {
WAITING_FOR_HELLO,
CONNECTED,
AUTHENTICATED,
} connection_state_{ConnectionState::WAITING_FOR_HELLO};
// Batch message method for ping requests
static uint16_t try_send_ping_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single);
bool remove_{false};
// === Optimal member ordering for 32-bit systems ===
// Group 1: Pointers (4 bytes each on 32-bit)
std::unique_ptr<APIFrameHelper> helper_;
std::string client_info_;
std::string client_peername_;
std::string client_combined_info_;
uint32_t client_api_version_major_{0};
uint32_t client_api_version_minor_{0};
#ifdef USE_ESP32_CAMERA
esp32_camera::CameraImageReader image_reader_;
#endif
bool state_subscription_{false};
int log_subscription_{ESPHOME_LOG_LEVEL_NONE};
uint32_t last_traffic_;
uint32_t next_ping_retry_{0};
uint8_t ping_retries_{0};
bool sent_ping_{false};
bool service_call_subscription_{false};
bool next_close_ = false;
APIServer *parent_;
// Group 2: Larger objects (must be 4-byte aligned)
// These contain vectors/pointers internally, so putting them early ensures good alignment
InitialStateIterator initial_state_iterator_;
ListEntitiesIterator list_entities_iterator_;
#ifdef USE_CAMERA
std::unique_ptr<camera::CameraImageReader> image_reader_;
#endif
// Group 3: Strings (12 bytes each on 32-bit, 4-byte aligned)
std::string client_info_;
std::string client_peername_;
// Group 4: 4-byte types
uint32_t last_traffic_;
int state_subs_at_ = -1;
// Function pointer type for message encoding
using MessageCreatorPtr = uint16_t (*)(EntityBase *, APIConnection *, uint32_t remaining_size, bool is_single);
// Optimized MessageCreator class using union dispatch
class MessageCreator {
public:
// Constructor for function pointer (message_type = 0)
MessageCreator(MessageCreatorPtr ptr) : message_type_(0) { data_.ptr = ptr; }
// Constructor for function pointer
MessageCreator(MessageCreatorPtr ptr) { data_.function_ptr = ptr; }
// Constructor for string state capture
MessageCreator(const std::string &value, uint16_t msg_type) : message_type_(msg_type) {
data_.string_ptr = new std::string(value);
}
explicit MessageCreator(const std::string &str_value) { data_.string_ptr = new std::string(str_value); }
// Destructor
~MessageCreator() {
// Clean up string data for string-based message types
if (uses_string_data_()) {
delete data_.string_ptr;
}
}
// No destructor - cleanup must be called explicitly with message_type
// Copy constructor
MessageCreator(const MessageCreator &other) : message_type_(other.message_type_) {
if (message_type_ == 0) {
data_.ptr = other.data_.ptr;
} else if (uses_string_data_()) {
data_.string_ptr = new std::string(*other.data_.string_ptr);
} else {
data_ = other.data_; // For POD types
}
}
// Delete copy operations - MessageCreator should only be moved
MessageCreator(const MessageCreator &other) = delete;
MessageCreator &operator=(const MessageCreator &other) = delete;
// Move constructor
MessageCreator(MessageCreator &&other) noexcept : data_(other.data_), message_type_(other.message_type_) {
other.message_type_ = 0; // Reset other to function pointer type
other.data_.ptr = nullptr;
}
// Assignment operators (needed for batch deduplication)
MessageCreator &operator=(const MessageCreator &other) {
if (this != &other) {
// Clean up current string data if needed
if (uses_string_data_()) {
delete data_.string_ptr;
}
// Copy new data
message_type_ = other.message_type_;
if (other.message_type_ == 0) {
data_.ptr = other.data_.ptr;
} else if (other.uses_string_data_()) {
data_.string_ptr = new std::string(*other.data_.string_ptr);
} else {
data_ = other.data_;
}
}
return *this;
}
MessageCreator(MessageCreator &&other) noexcept : data_(other.data_) { other.data_.function_ptr = nullptr; }
// Move assignment
MessageCreator &operator=(MessageCreator &&other) noexcept {
if (this != &other) {
// Clean up current string data if needed
if (uses_string_data_()) {
delete data_.string_ptr;
}
// Move data
message_type_ = other.message_type_;
// IMPORTANT: Caller must ensure cleanup() was called if this contains a string!
// In our usage, this happens in add_item() deduplication and vector::erase()
data_ = other.data_;
// Reset other to safe state
other.message_type_ = 0;
other.data_.ptr = nullptr;
other.data_.function_ptr = nullptr;
}
return *this;
}
// Call operator
uint16_t operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) const;
// Call operator - uses message_type to determine union type
uint16_t operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single,
uint16_t message_type) const;
// Manual cleanup method - must be called before destruction for string types
void cleanup(uint16_t message_type) {
#ifdef USE_EVENT
if (message_type == EventResponse::MESSAGE_TYPE && data_.string_ptr != nullptr) {
delete data_.string_ptr;
data_.string_ptr = nullptr;
}
#endif
}
private:
// Helper to check if this message type uses heap-allocated strings
bool uses_string_data_() const { return message_type_ == EventResponse::MESSAGE_TYPE; }
union CreatorData {
MessageCreatorPtr ptr; // 8 bytes
std::string *string_ptr; // 8 bytes
} data_; // 8 bytes
uint16_t message_type_; // 2 bytes (0 = function ptr, >0 = state capture)
union Data {
MessageCreatorPtr function_ptr;
std::string *string_ptr;
} data_; // 4 bytes on 32-bit, 8 bytes on 64-bit - same as before
};
// Generic batching mechanism for both state updates and entity info
@ -566,24 +538,86 @@ class APIConnection : public APIServerConnection {
std::vector<BatchItem> items;
uint32_t batch_start_time{0};
bool batch_scheduled{false};
private:
// Helper to cleanup items from the beginning
void cleanup_items_(size_t count) {
for (size_t i = 0; i < count; i++) {
items[i].creator.cleanup(items[i].message_type);
}
}
public:
DeferredBatch() {
// Pre-allocate capacity for typical batch sizes to avoid reallocation
items.reserve(8);
}
~DeferredBatch() {
// Ensure cleanup of any remaining items
clear();
}
// Add item to the batch
void add_item(EntityBase *entity, MessageCreator creator, uint16_t message_type);
// Add item to the front of the batch (for high priority messages like ping)
void add_item_front(EntityBase *entity, MessageCreator creator, uint16_t message_type);
// Clear all items with proper cleanup
void clear() {
cleanup_items_(items.size());
items.clear();
batch_scheduled = false;
batch_start_time = 0;
}
// Remove processed items from the front with proper cleanup
void remove_front(size_t count) {
cleanup_items_(count);
items.erase(items.begin(), items.begin() + count);
}
bool empty() const { return items.empty(); }
size_t size() const { return items.size(); }
const BatchItem &operator[](size_t index) const { return items[index]; }
};
// DeferredBatch here (16 bytes, 4-byte aligned)
DeferredBatch deferred_batch_;
// ConnectionState enum for type safety
enum class ConnectionState : uint8_t {
WAITING_FOR_HELLO = 0,
CONNECTED = 1,
AUTHENTICATED = 2,
};
// Group 5: Pack all small members together to minimize padding
// This group starts at a 4-byte boundary after DeferredBatch
struct APIFlags {
// Connection state only needs 2 bits (3 states)
uint8_t connection_state : 2;
// Log subscription needs 3 bits (log levels 0-7)
uint8_t log_subscription : 3;
// Boolean flags (1 bit each)
uint8_t remove : 1;
uint8_t state_subscription : 1;
uint8_t sent_ping : 1;
uint8_t service_call_subscription : 1;
uint8_t next_close : 1;
uint8_t batch_scheduled : 1;
uint8_t batch_first_message : 1; // For batch buffer allocation
uint8_t should_try_send_immediately : 1; // True after initial states are sent
#ifdef HAS_PROTO_MESSAGE_DUMP
uint8_t log_only_mode : 1;
#endif
} flags_{}; // 2 bytes total
// 2-byte types immediately after flags_ (no padding between them)
uint16_t client_api_version_major_{0};
uint16_t client_api_version_minor_{0};
// Total: 2 (flags) + 2 + 2 = 6 bytes, then 2 bytes padding to next 4-byte boundary
uint32_t get_batch_delay_ms_() const;
// Message will use 8 more bytes than the minimum size, and typical
// MTU is 1500. Sometimes users will see as low as 1460 MTU.
@ -600,9 +634,49 @@ class APIConnection : public APIServerConnection {
bool schedule_batch_();
void process_batch_();
void clear_batch_() {
this->deferred_batch_.clear();
this->flags_.batch_scheduled = false;
}
// State for batch buffer allocation
bool batch_first_message_{false};
#ifdef HAS_PROTO_MESSAGE_DUMP
// Helper to log a proto message from a MessageCreator object
void log_proto_message_(EntityBase *entity, const MessageCreator &creator, uint16_t message_type) {
this->flags_.log_only_mode = true;
creator(entity, this, MAX_PACKET_SIZE, true, message_type);
this->flags_.log_only_mode = false;
}
void log_batch_item_(const DeferredBatch::BatchItem &item) {
// Use the helper to log the message
this->log_proto_message_(item.entity, item.creator, item.message_type);
}
#endif
// Helper method to send a message either immediately or via batching
bool send_message_smart_(EntityBase *entity, MessageCreatorPtr creator, uint16_t message_type) {
// Try to send immediately if:
// 1. We should try to send immediately (should_try_send_immediately = true)
// 2. Batch delay is 0 (user has opted in to immediate sending)
// 3. Buffer has space available
if (this->flags_.should_try_send_immediately && this->get_batch_delay_ms_() == 0 &&
this->helper_->can_write_without_blocking()) {
// Now actually encode and send
if (creator(entity, this, MAX_PACKET_SIZE, true) &&
this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, message_type)) {
#ifdef HAS_PROTO_MESSAGE_DUMP
// Log the message in verbose mode
this->log_proto_message_(entity, MessageCreator(creator), message_type);
#endif
return true;
}
// If immediate send failed, fall through to batching
}
// Fall back to scheduled batching
return this->schedule_message_(entity, creator, message_type);
}
// Helper function to schedule a deferred message with known message type
bool schedule_message_(EntityBase *entity, MessageCreator creator, uint16_t message_type) {
@ -614,6 +688,12 @@ class APIConnection : public APIServerConnection {
bool schedule_message_(EntityBase *entity, MessageCreatorPtr function_ptr, uint16_t message_type) {
return schedule_message_(entity, MessageCreator(function_ptr), message_type);
}
// Helper function to schedule a high priority message at the front of the batch
bool schedule_message_front_(EntityBase *entity, MessageCreatorPtr function_ptr, uint16_t message_type) {
this->deferred_batch_.add_item_front(entity, MessageCreator(function_ptr), message_type);
return this->schedule_batch_();
}
};
} // namespace api

View File

@ -66,6 +66,17 @@ const char *api_error_to_str(APIError err) {
return "UNKNOWN";
}
// Default implementation for loop - handles sending buffered data
APIError APIFrameHelper::loop() {
if (!this->tx_buf_.empty()) {
APIError err = try_send_tx_buf_();
if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
return err;
}
}
return APIError::OK; // Convert WOULD_BLOCK to OK to avoid connection termination
}
// Helper method to buffer data from IOVs
void APIFrameHelper::buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len) {
SendBuffer buffer;
@ -214,6 +225,22 @@ APIError APIFrameHelper::init_common_() {
}
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, this->info_.c_str(), ##__VA_ARGS__)
APIError APIFrameHelper::handle_socket_read_result_(ssize_t received) {
if (received == -1) {
if (errno == EWOULDBLOCK || errno == EAGAIN) {
return APIError::WOULD_BLOCK;
}
state_ = State::FAILED;
HELPER_LOG("Socket read failed with errno %d", errno);
return APIError::SOCKET_READ_FAILED;
} else if (received == 0) {
state_ = State::FAILED;
HELPER_LOG("Connection closed");
return APIError::CONNECTION_CLOSED;
}
return APIError::OK;
}
// uncomment to log raw packets
//#define HELPER_LOG_PACKETS
@ -274,17 +301,21 @@ APIError APINoiseFrameHelper::init() {
}
/// Run through handshake messages (if in that phase)
APIError APINoiseFrameHelper::loop() {
APIError err = state_action_();
if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
return err;
}
if (!this->tx_buf_.empty()) {
err = try_send_tx_buf_();
// During handshake phase, process as many actions as possible until we can't progress
// socket_->ready() stays true until next main loop, but state_action() will return
// WOULD_BLOCK when no more data is available to read
while (state_ != State::DATA && this->socket_->ready()) {
APIError err = state_action_();
if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
return err;
}
if (err == APIError::WOULD_BLOCK) {
break;
}
}
return APIError::OK; // Convert WOULD_BLOCK to OK to avoid connection termination
// Use base class implementation for buffer sending
return APIFrameHelper::loop();
}
/** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter
@ -312,17 +343,9 @@ APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) {
// no header information yet
uint8_t to_read = 3 - rx_header_buf_len_;
ssize_t received = this->socket_->read(&rx_header_buf_[rx_header_buf_len_], to_read);
if (received == -1) {
if (errno == EWOULDBLOCK || errno == EAGAIN) {
return APIError::WOULD_BLOCK;
}
state_ = State::FAILED;
HELPER_LOG("Socket read failed with errno %d", errno);
return APIError::SOCKET_READ_FAILED;
} else if (received == 0) {
state_ = State::FAILED;
HELPER_LOG("Connection closed");
return APIError::CONNECTION_CLOSED;
APIError err = handle_socket_read_result_(received);
if (err != APIError::OK) {
return err;
}
rx_header_buf_len_ += static_cast<uint8_t>(received);
if (static_cast<uint8_t>(received) != to_read) {
@ -330,17 +353,15 @@ APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) {
return APIError::WOULD_BLOCK;
}
if (rx_header_buf_[0] != 0x01) {
state_ = State::FAILED;
HELPER_LOG("Bad indicator byte %u", rx_header_buf_[0]);
return APIError::BAD_INDICATOR;
}
// header reading done
}
// read body
uint8_t indicator = rx_header_buf_[0];
if (indicator != 0x01) {
state_ = State::FAILED;
HELPER_LOG("Bad indicator byte %u", indicator);
return APIError::BAD_INDICATOR;
}
uint16_t msg_size = (((uint16_t) rx_header_buf_[1]) << 8) | rx_header_buf_[2];
if (state_ != State::DATA && msg_size > 128) {
@ -359,17 +380,9 @@ APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) {
// more data to read
uint16_t to_read = msg_size - rx_buf_len_;
ssize_t received = this->socket_->read(&rx_buf_[rx_buf_len_], to_read);
if (received == -1) {
if (errno == EWOULDBLOCK || errno == EAGAIN) {
return APIError::WOULD_BLOCK;
}
state_ = State::FAILED;
HELPER_LOG("Socket read failed with errno %d", errno);
return APIError::SOCKET_READ_FAILED;
} else if (received == 0) {
state_ = State::FAILED;
HELPER_LOG("Connection closed");
return APIError::CONNECTION_CLOSED;
APIError err = handle_socket_read_result_(received);
if (err != APIError::OK) {
return err;
}
rx_buf_len_ += static_cast<uint16_t>(received);
if (static_cast<uint16_t>(received) != to_read) {
@ -586,10 +599,6 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
return APIError::BAD_DATA_PACKET;
}
// uint16_t type;
// uint16_t data_len;
// uint8_t *data;
// uint8_t *padding; zero or more bytes to fill up the rest of the packet
uint16_t type = (((uint16_t) msg_data[0]) << 8) | msg_data[1];
uint16_t data_len = (((uint16_t) msg_data[2]) << 8) | msg_data[3];
if (data_len > msg_size - 4) {
@ -605,20 +614,14 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
return APIError::OK;
}
APIError APINoiseFrameHelper::write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) {
std::vector<uint8_t> *raw_buffer = buffer.get_buffer();
uint16_t payload_len = static_cast<uint16_t>(raw_buffer->size() - frame_header_padding_);
// Resize to include MAC space (required for Noise encryption)
raw_buffer->resize(raw_buffer->size() + frame_footer_size_);
// Use write_protobuf_packets with a single packet
std::vector<PacketInfo> packets;
packets.emplace_back(type, 0, payload_len);
return write_protobuf_packets(buffer, packets);
buffer.get_buffer()->resize(buffer.get_buffer()->size() + frame_footer_size_);
PacketInfo packet{type, 0,
static_cast<uint16_t>(buffer.get_buffer()->size() - frame_header_padding_ - frame_footer_size_)};
return write_protobuf_packets(buffer, std::span<const PacketInfo>(&packet, 1));
}
APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, const std::vector<PacketInfo> &packets) {
APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) {
APIError aerr = state_action_();
if (aerr != APIError::OK) {
return aerr;
@ -633,18 +636,15 @@ APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, co
}
std::vector<uint8_t> *raw_buffer = buffer.get_buffer();
uint8_t *buffer_data = raw_buffer->data(); // Cache buffer pointer
this->reusable_iovs_.clear();
this->reusable_iovs_.reserve(packets.size());
// We need to encrypt each packet in place
for (const auto &packet : packets) {
uint16_t type = packet.message_type;
uint16_t offset = packet.offset;
uint16_t payload_len = packet.payload_size;
uint16_t msg_len = 4 + payload_len; // type(2) + data_len(2) + payload
// The buffer already has padding at offset
uint8_t *buf_start = raw_buffer->data() + offset;
uint8_t *buf_start = buffer_data + packet.offset;
// Write noise header
buf_start[0] = 0x01; // indicator
@ -652,10 +652,10 @@ APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, co
// Write message header (to be encrypted)
const uint8_t msg_offset = 3;
buf_start[msg_offset + 0] = (uint8_t) (type >> 8); // type high byte
buf_start[msg_offset + 1] = (uint8_t) type; // type low byte
buf_start[msg_offset + 2] = (uint8_t) (payload_len >> 8); // data_len high byte
buf_start[msg_offset + 3] = (uint8_t) payload_len; // data_len low byte
buf_start[msg_offset] = static_cast<uint8_t>(packet.message_type >> 8); // type high byte
buf_start[msg_offset + 1] = static_cast<uint8_t>(packet.message_type); // type low byte
buf_start[msg_offset + 2] = static_cast<uint8_t>(packet.payload_size >> 8); // data_len high byte
buf_start[msg_offset + 3] = static_cast<uint8_t>(packet.payload_size); // data_len low byte
// payload data is already in the buffer starting at offset + 7
// Make sure we have space for MAC
@ -664,7 +664,8 @@ APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, co
// Encrypt the message in place
NoiseBuffer mbuf;
noise_buffer_init(mbuf);
noise_buffer_set_inout(mbuf, buf_start + msg_offset, msg_len, msg_len + frame_footer_size_);
noise_buffer_set_inout(mbuf, buf_start + msg_offset, 4 + packet.payload_size,
4 + packet.payload_size + frame_footer_size_);
int err = noise_cipherstate_encrypt(send_cipher_, &mbuf);
if (err != 0) {
@ -674,14 +675,12 @@ APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, co
}
// Fill in the encrypted size
buf_start[1] = (uint8_t) (mbuf.size >> 8);
buf_start[2] = (uint8_t) mbuf.size;
buf_start[1] = static_cast<uint8_t>(mbuf.size >> 8);
buf_start[2] = static_cast<uint8_t>(mbuf.size);
// Add iovec for this encrypted packet
struct iovec iov;
iov.iov_base = buf_start;
iov.iov_len = 3 + mbuf.size; // indicator + size + encrypted data
this->reusable_iovs_.push_back(iov);
this->reusable_iovs_.push_back(
{buf_start, static_cast<size_t>(3 + mbuf.size)}); // indicator + size + encrypted data
}
// Send all encrypted packets in one writev call
@ -822,18 +821,12 @@ APIError APIPlaintextFrameHelper::init() {
state_ = State::DATA;
return APIError::OK;
}
/// Not used for plaintext
APIError APIPlaintextFrameHelper::loop() {
if (state_ != State::DATA) {
return APIError::BAD_STATE;
}
if (!this->tx_buf_.empty()) {
APIError err = try_send_tx_buf_();
if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
return err;
}
}
return APIError::OK; // Convert WOULD_BLOCK to OK to avoid connection termination
// Use base class implementation for buffer sending
return APIFrameHelper::loop();
}
/** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter
@ -862,17 +855,9 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) {
// Try to get to at least 3 bytes total (indicator + 2 varint bytes), then read one byte at a time
ssize_t received =
this->socket_->read(&rx_header_buf_[rx_header_buf_pos_], rx_header_buf_pos_ < 3 ? 3 - rx_header_buf_pos_ : 1);
if (received == -1) {
if (errno == EWOULDBLOCK || errno == EAGAIN) {
return APIError::WOULD_BLOCK;
}
state_ = State::FAILED;
HELPER_LOG("Socket read failed with errno %d", errno);
return APIError::SOCKET_READ_FAILED;
} else if (received == 0) {
state_ = State::FAILED;
HELPER_LOG("Connection closed");
return APIError::CONNECTION_CLOSED;
APIError err = handle_socket_read_result_(received);
if (err != APIError::OK) {
return err;
}
// If this was the first read, validate the indicator byte
@ -956,17 +941,9 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) {
// more data to read
uint16_t to_read = rx_header_parsed_len_ - rx_buf_len_;
ssize_t received = this->socket_->read(&rx_buf_[rx_buf_len_], to_read);
if (received == -1) {
if (errno == EWOULDBLOCK || errno == EAGAIN) {
return APIError::WOULD_BLOCK;
}
state_ = State::FAILED;
HELPER_LOG("Socket read failed with errno %d", errno);
return APIError::SOCKET_READ_FAILED;
} else if (received == 0) {
state_ = State::FAILED;
HELPER_LOG("Connection closed");
return APIError::CONNECTION_CLOSED;
APIError err = handle_socket_read_result_(received);
if (err != APIError::OK) {
return err;
}
rx_buf_len_ += static_cast<uint16_t>(received);
if (static_cast<uint16_t>(received) != to_read) {
@ -1026,18 +1003,11 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
return APIError::OK;
}
APIError APIPlaintextFrameHelper::write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) {
std::vector<uint8_t> *raw_buffer = buffer.get_buffer();
uint16_t payload_len = static_cast<uint16_t>(raw_buffer->size() - frame_header_padding_);
// Use write_protobuf_packets with a single packet
std::vector<PacketInfo> packets;
packets.emplace_back(type, 0, payload_len);
return write_protobuf_packets(buffer, packets);
PacketInfo packet{type, 0, static_cast<uint16_t>(buffer.get_buffer()->size() - frame_header_padding_)};
return write_protobuf_packets(buffer, std::span<const PacketInfo>(&packet, 1));
}
APIError APIPlaintextFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer,
const std::vector<PacketInfo> &packets) {
APIError APIPlaintextFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) {
if (state_ != State::DATA) {
return APIError::BAD_STATE;
}
@ -1047,17 +1017,15 @@ APIError APIPlaintextFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer
}
std::vector<uint8_t> *raw_buffer = buffer.get_buffer();
uint8_t *buffer_data = raw_buffer->data(); // Cache buffer pointer
this->reusable_iovs_.clear();
this->reusable_iovs_.reserve(packets.size());
for (const auto &packet : packets) {
uint16_t type = packet.message_type;
uint16_t offset = packet.offset;
uint16_t payload_len = packet.payload_size;
// Calculate varint sizes for header layout
uint8_t size_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(payload_len));
uint8_t type_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(type));
uint8_t size_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(packet.payload_size));
uint8_t type_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(packet.message_type));
uint8_t total_header_len = 1 + size_varint_len + type_varint_len;
// Calculate where to start writing the header
@ -1085,23 +1053,20 @@ APIError APIPlaintextFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer
//
// The message starts at offset + frame_header_padding_
// So we write the header starting at offset + frame_header_padding_ - total_header_len
uint8_t *buf_start = raw_buffer->data() + offset;
uint8_t *buf_start = buffer_data + packet.offset;
uint32_t header_offset = frame_header_padding_ - total_header_len;
// Write the plaintext header
buf_start[header_offset] = 0x00; // indicator
// Encode size varint directly into buffer
ProtoVarInt(payload_len).encode_to_buffer_unchecked(buf_start + header_offset + 1, size_varint_len);
// Encode type varint directly into buffer
ProtoVarInt(type).encode_to_buffer_unchecked(buf_start + header_offset + 1 + size_varint_len, type_varint_len);
// Encode varints directly into buffer
ProtoVarInt(packet.payload_size).encode_to_buffer_unchecked(buf_start + header_offset + 1, size_varint_len);
ProtoVarInt(packet.message_type)
.encode_to_buffer_unchecked(buf_start + header_offset + 1 + size_varint_len, type_varint_len);
// Add iovec for this packet (header + payload)
struct iovec iov;
iov.iov_base = buf_start + header_offset;
iov.iov_len = total_header_len + payload_len;
this->reusable_iovs_.push_back(iov);
this->reusable_iovs_.push_back(
{buf_start + header_offset, static_cast<size_t>(total_header_len + packet.payload_size)});
}
// Send all packets in one writev call

View File

@ -2,6 +2,7 @@
#include <cstdint>
#include <deque>
#include <limits>
#include <span>
#include <utility>
#include <vector>
@ -38,7 +39,7 @@ struct PacketInfo {
: message_type(type), offset(off), payload_size(size), padding(0) {}
};
enum class APIError : int {
enum class APIError : uint16_t {
OK = 0,
WOULD_BLOCK = 1001,
BAD_HANDSHAKE_PACKET_LEN = 1002,
@ -74,7 +75,7 @@ class APIFrameHelper {
}
virtual ~APIFrameHelper() = default;
virtual APIError init() = 0;
virtual APIError loop() = 0;
virtual APIError loop();
virtual APIError read_packet(ReadPacketBuffer *buffer) = 0;
bool can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); }
std::string getpeername() { return socket_->getpeername(); }
@ -101,7 +102,7 @@ class APIFrameHelper {
// Write multiple protobuf packets in a single operation
// packets contains (message_type, offset, length) for each message in the buffer
// The buffer contains all messages with appropriate padding before each
virtual APIError write_protobuf_packets(ProtoWriteBuffer buffer, const std::vector<PacketInfo> &packets) = 0;
virtual APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) = 0;
// Get the frame header padding required by this protocol
virtual uint8_t frame_header_padding() = 0;
// Get the frame footer size required by this protocol
@ -125,38 +126,6 @@ class APIFrameHelper {
const uint8_t *current_data() const { return data.data() + offset; }
};
// Queue of data buffers to be sent
std::deque<SendBuffer> tx_buf_;
// Common state enum for all frame helpers
// Note: Not all states are used by all implementations
// - INITIALIZE: Used by both Noise and Plaintext
// - CLIENT_HELLO, SERVER_HELLO, HANDSHAKE: Only used by Noise protocol
// - DATA: Used by both Noise and Plaintext
// - CLOSED: Used by both Noise and Plaintext
// - FAILED: Used by both Noise and Plaintext
// - EXPLICIT_REJECT: Only used by Noise protocol
enum class State {
INITIALIZE = 1,
CLIENT_HELLO = 2, // Noise only
SERVER_HELLO = 3, // Noise only
HANDSHAKE = 4, // Noise only
DATA = 5,
CLOSED = 6,
FAILED = 7,
EXPLICIT_REJECT = 8, // Noise only
};
// Current state of the frame helper
State state_{State::INITIALIZE};
// Helper name for logging
std::string info_;
// Socket for communication
socket::Socket *socket_{nullptr};
std::unique_ptr<socket::Socket> socket_owned_;
// Common implementation for writing raw data to socket
APIError write_raw_(const struct iovec *iov, int iovcnt);
@ -169,18 +138,47 @@ class APIFrameHelper {
APIError write_raw_(const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector<uint8_t> &tx_buf,
const std::string &info, StateEnum &state, StateEnum failed_state);
// Pointers first (4 bytes each)
socket::Socket *socket_{nullptr};
std::unique_ptr<socket::Socket> socket_owned_;
// Common state enum for all frame helpers
// Note: Not all states are used by all implementations
// - INITIALIZE: Used by both Noise and Plaintext
// - CLIENT_HELLO, SERVER_HELLO, HANDSHAKE: Only used by Noise protocol
// - DATA: Used by both Noise and Plaintext
// - CLOSED: Used by both Noise and Plaintext
// - FAILED: Used by both Noise and Plaintext
// - EXPLICIT_REJECT: Only used by Noise protocol
enum class State : uint8_t {
INITIALIZE = 1,
CLIENT_HELLO = 2, // Noise only
SERVER_HELLO = 3, // Noise only
HANDSHAKE = 4, // Noise only
DATA = 5,
CLOSED = 6,
FAILED = 7,
EXPLICIT_REJECT = 8, // Noise only
};
// Containers (size varies, but typically 12+ bytes on 32-bit)
std::deque<SendBuffer> tx_buf_;
std::string info_;
std::vector<struct iovec> reusable_iovs_;
std::vector<uint8_t> rx_buf_;
// Group smaller types together
uint16_t rx_buf_len_ = 0;
State state_{State::INITIALIZE};
uint8_t frame_header_padding_{0};
uint8_t frame_footer_size_{0};
// Reusable IOV array for write_protobuf_packets to avoid repeated allocations
std::vector<struct iovec> reusable_iovs_;
// Receive buffer for reading frame data
std::vector<uint8_t> rx_buf_;
uint16_t rx_buf_len_ = 0;
// 5 bytes total, 3 bytes padding
// Common initialization for both plaintext and noise protocols
APIError init_common_();
// Helper method to handle socket read results
APIError handle_socket_read_result_(ssize_t received);
};
#ifdef USE_API_NOISE
@ -200,7 +198,7 @@ class APINoiseFrameHelper : public APIFrameHelper {
APIError loop() override;
APIError read_packet(ReadPacketBuffer *buffer) override;
APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) override;
APIError write_protobuf_packets(ProtoWriteBuffer buffer, const std::vector<PacketInfo> &packets) override;
APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) override;
// Get the frame header padding required by this protocol
uint8_t frame_header_padding() override { return frame_header_padding_; }
// Get the frame footer size required by this protocol
@ -213,19 +211,28 @@ class APINoiseFrameHelper : public APIFrameHelper {
APIError init_handshake_();
APIError check_handshake_finished_();
void send_explicit_handshake_reject_(const std::string &reason);
// Pointers first (4 bytes each)
NoiseHandshakeState *handshake_{nullptr};
NoiseCipherState *send_cipher_{nullptr};
NoiseCipherState *recv_cipher_{nullptr};
// Shared pointer (8 bytes on 32-bit = 4 bytes control block pointer + 4 bytes object pointer)
std::shared_ptr<APINoiseContext> ctx_;
// Vector (12 bytes on 32-bit)
std::vector<uint8_t> prologue_;
// NoiseProtocolId (size depends on implementation)
NoiseProtocolId nid_;
// Group small types together
// Fixed-size header buffer for noise protocol:
// 1 byte for indicator + 2 bytes for message size (16-bit value, not varint)
// Note: Maximum message size is UINT16_MAX (65535), with a limit of 128 bytes during handshake phase
uint8_t rx_header_buf_[3];
uint8_t rx_header_buf_len_ = 0;
std::vector<uint8_t> prologue_;
std::shared_ptr<APINoiseContext> ctx_;
NoiseHandshakeState *handshake_{nullptr};
NoiseCipherState *send_cipher_{nullptr};
NoiseCipherState *recv_cipher_{nullptr};
NoiseProtocolId nid_;
// 4 bytes total, no padding
};
#endif // USE_API_NOISE
@ -245,13 +252,19 @@ class APIPlaintextFrameHelper : public APIFrameHelper {
APIError loop() override;
APIError read_packet(ReadPacketBuffer *buffer) override;
APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) override;
APIError write_protobuf_packets(ProtoWriteBuffer buffer, const std::vector<PacketInfo> &packets) override;
APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) override;
uint8_t frame_header_padding() override { return frame_header_padding_; }
// Get the frame footer size required by this protocol
uint8_t frame_footer_size() override { return frame_footer_size_; }
protected:
APIError try_read_frame_(ParsedFrame *frame);
// Group 2-byte aligned types
uint16_t rx_header_parsed_type_ = 0;
uint16_t rx_header_parsed_len_ = 0;
// Group 1-byte types together
// Fixed-size header buffer for plaintext protocol:
// We now store the indicator byte + the two varints.
// To match noise protocol's maximum message size (UINT16_MAX = 65535), we need:
@ -263,8 +276,7 @@ class APIPlaintextFrameHelper : public APIFrameHelper {
uint8_t rx_header_buf_[6]; // 1 byte indicator + 5 bytes for varints (3 for size + 2 for type)
uint8_t rx_header_buf_pos_ = 0;
bool rx_header_parsed_ = false;
uint16_t rx_header_parsed_type_ = 0;
uint16_t rx_header_parsed_len_ = 0;
// 8 bytes total, no padding needed
};
#endif

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -2,9 +2,10 @@
// See script/api_protobuf/api_protobuf.py
#pragma once
#include "api_pb2.h"
#include "esphome/core/defines.h"
#include "api_pb2.h"
namespace esphome {
namespace api {
@ -19,7 +20,7 @@ class APIServerConnectionBase : public ProtoService {
template<typename T> bool send_message(const T &msg) {
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_send_message_(T::message_name(), msg.dump());
this->log_send_message_(msg.message_name(), msg.dump());
#endif
return this->send_message_(msg, T::MESSAGE_TYPE);
}
@ -70,7 +71,7 @@ class APIServerConnectionBase : public ProtoService {
virtual void on_execute_service_request(const ExecuteServiceRequest &value){};
#ifdef USE_ESP32_CAMERA
#ifdef USE_CAMERA
virtual void on_camera_image_request(const CameraImageRequest &value){};
#endif
@ -199,7 +200,7 @@ class APIServerConnectionBase : public ProtoService {
virtual void on_update_command_request(const UpdateCommandRequest &value){};
#endif
protected:
bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override;
void read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override;
};
class APIServerConnection : public APIServerConnectionBase {
@ -222,7 +223,7 @@ class APIServerConnection : public APIServerConnectionBase {
#ifdef USE_BUTTON
virtual void button_command(const ButtonCommandRequest &msg) = 0;
#endif
#ifdef USE_ESP32_CAMERA
#ifdef USE_CAMERA
virtual void camera_image(const CameraImageRequest &msg) = 0;
#endif
#ifdef USE_CLIMATE
@ -339,7 +340,7 @@ class APIServerConnection : public APIServerConnectionBase {
#ifdef USE_BUTTON
void on_button_command_request(const ButtonCommandRequest &msg) override;
#endif
#ifdef USE_ESP32_CAMERA
#ifdef USE_CAMERA
void on_camera_image_request(const CameraImageRequest &msg) override;
#endif
#ifdef USE_CLIMATE

View File

@ -316,15 +316,13 @@ class ProtoSize {
/**
* @brief Calculates and adds the size of a nested message field to the total message size
*
* This templated version directly takes a message object, calculates its size internally,
* This version takes a ProtoMessage object, calculates its size internally,
* and updates the total_size reference. This eliminates the need for a temporary variable
* at the call site.
*
* @tparam MessageType The type of the nested message (inferred from parameter)
* @param message The nested message object
*/
template<typename MessageType>
static inline void add_message_object(uint32_t &total_size, uint32_t field_id_size, const MessageType &message,
static inline void add_message_object(uint32_t &total_size, uint32_t field_id_size, const ProtoMessage &message,
bool force = false) {
uint32_t nested_size = 0;
message.calculate_size(nested_size);

View File

@ -24,6 +24,14 @@ static const char *const TAG = "api";
// APIServer
APIServer *global_api_server = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
#ifndef USE_API_YAML_SERVICES
// Global empty vector to avoid guard variables (saves 8 bytes)
// This is initialized at program startup before any threads
static const std::vector<UserServiceDescriptor *> empty_user_services{};
const std::vector<UserServiceDescriptor *> &get_empty_user_services_instance() { return empty_user_services; }
#endif
APIServer::APIServer() {
global_api_server = this;
// Pre-allocate shared write buffer
@ -47,6 +55,11 @@ void APIServer::setup() {
}
#endif
// Schedule reboot if no clients connect within timeout
if (this->reboot_timeout_ != 0) {
this->schedule_reboot_timeout_();
}
this->socket_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0); // monitored for incoming connections
if (this->socket_ == nullptr) {
ESP_LOGW(TAG, "Could not create socket");
@ -91,34 +104,42 @@ void APIServer::setup() {
#ifdef USE_LOGGER
if (logger::global_logger != nullptr) {
logger::global_logger->add_on_log_callback([this](int level, const char *tag, const char *message) {
if (this->shutting_down_) {
// Don't try to send logs during shutdown
// as it could result in a recursion and
// we would be filling a buffer we are trying to clear
return;
}
for (auto &c : this->clients_) {
if (!c->remove_)
c->try_send_log_message(level, tag, message);
}
});
}
#endif
this->last_connected_ = millis();
#ifdef USE_ESP32_CAMERA
if (esp32_camera::global_esp32_camera != nullptr && !esp32_camera::global_esp32_camera->is_internal()) {
esp32_camera::global_esp32_camera->add_image_callback(
[this](const std::shared_ptr<esp32_camera::CameraImage> &image) {
logger::global_logger->add_on_log_callback(
[this](int level, const char *tag, const char *message, size_t message_len) {
if (this->shutting_down_) {
// Don't try to send logs during shutdown
// as it could result in a recursion and
// we would be filling a buffer we are trying to clear
return;
}
for (auto &c : this->clients_) {
if (!c->remove_)
c->set_camera_state(image);
if (!c->flags_.remove)
c->try_send_log_message(level, tag, message, message_len);
}
});
}
#endif
#ifdef USE_CAMERA
if (camera::Camera::instance() != nullptr && !camera::Camera::instance()->is_internal()) {
camera::Camera::instance()->add_image_callback([this](const std::shared_ptr<camera::CameraImage> &image) {
for (auto &c : this->clients_) {
if (!c->flags_.remove)
c->set_camera_state(image);
}
});
}
#endif
}
void APIServer::schedule_reboot_timeout_() {
this->status_set_warning();
this->set_timeout("api_reboot", this->reboot_timeout_, []() {
if (!global_api_server->is_connected()) {
ESP_LOGE(TAG, "No clients; rebooting");
App.reboot();
}
});
}
void APIServer::loop() {
@ -130,51 +151,63 @@ void APIServer::loop() {
auto sock = this->socket_->accept_loop_monitored((struct sockaddr *) &source_addr, &addr_len);
if (!sock)
break;
ESP_LOGD(TAG, "Accepted %s", sock->getpeername().c_str());
ESP_LOGD(TAG, "Accept %s", sock->getpeername().c_str());
auto *conn = new APIConnection(std::move(sock), this);
this->clients_.emplace_back(conn);
conn->start();
// Clear warning status and cancel reboot when first client connects
if (this->clients_.size() == 1 && this->reboot_timeout_ != 0) {
this->status_clear_warning();
this->cancel_timeout("api_reboot");
}
}
}
if (this->clients_.empty()) {
return;
}
// Process clients and remove disconnected ones in a single pass
if (!this->clients_.empty()) {
size_t client_index = 0;
while (client_index < this->clients_.size()) {
auto &client = this->clients_[client_index];
if (client->remove_) {
// Handle disconnection
this->client_disconnected_trigger_->trigger(client->client_info_, client->client_peername_);
ESP_LOGV(TAG, "Removing connection to %s", client->client_info_.c_str());
// Swap with the last element and pop (avoids expensive vector shifts)
if (client_index < this->clients_.size() - 1) {
std::swap(this->clients_[client_index], this->clients_.back());
}
this->clients_.pop_back();
// Don't increment client_index since we need to process the swapped element
} else {
// Process active client
client->loop();
client_index++; // Move to next client
}
// Check network connectivity once for all clients
if (!network::is_connected()) {
// Network is down - disconnect all clients
for (auto &client : this->clients_) {
client->on_fatal_error();
ESP_LOGW(TAG, "%s: Network down; disconnect", client->get_client_combined_info().c_str());
}
// Continue to process and clean up the clients below
}
if (this->reboot_timeout_ != 0) {
const uint32_t now = millis();
if (!this->is_connected()) {
if (now - this->last_connected_ > this->reboot_timeout_) {
ESP_LOGE(TAG, "No client connected; rebooting");
App.reboot();
}
this->status_set_warning();
} else {
this->last_connected_ = now;
this->status_clear_warning();
size_t client_index = 0;
while (client_index < this->clients_.size()) {
auto &client = this->clients_[client_index];
if (!client->flags_.remove) {
// Common case: process active client
client->loop();
client_index++;
continue;
}
// Rare case: handle disconnection
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
this->client_disconnected_trigger_->trigger(client->client_info_, client->client_peername_);
#endif
ESP_LOGV(TAG, "Remove connection %s", client->client_info_.c_str());
// Swap with the last element and pop (avoids expensive vector shifts)
if (client_index < this->clients_.size() - 1) {
std::swap(this->clients_[client_index], this->clients_.back());
}
this->clients_.pop_back();
// Schedule reboot when last client disconnects
if (this->clients_.empty() && this->reboot_timeout_ != 0) {
this->schedule_reboot_timeout_();
}
// Don't increment client_index since we need to process the swapped element
}
}
@ -193,6 +226,7 @@ void APIServer::dump_config() {
#endif
}
#ifdef USE_API_PASSWORD
bool APIServer::uses_password() const { return !this->password_.empty(); }
bool APIServer::check_password(const std::string &password) const {
@ -223,192 +257,129 @@ bool APIServer::check_password(const std::string &password) const {
return result == 0;
}
#endif
void APIServer::handle_disconnect(APIConnection *conn) {}
// Macro for entities without extra parameters
#define API_DISPATCH_UPDATE(entity_type, entity_name) \
void APIServer::on_##entity_name##_update(entity_type *obj) { /* NOLINT(bugprone-macro-parentheses) */ \
if (obj->is_internal()) \
return; \
for (auto &c : this->clients_) \
c->send_##entity_name##_state(obj); \
}
// Macro for entities with extra parameters (but parameters not used in send)
#define API_DISPATCH_UPDATE_IGNORE_PARAMS(entity_type, entity_name, ...) \
void APIServer::on_##entity_name##_update(entity_type *obj, __VA_ARGS__) { /* NOLINT(bugprone-macro-parentheses) */ \
if (obj->is_internal()) \
return; \
for (auto &c : this->clients_) \
c->send_##entity_name##_state(obj); \
}
#ifdef USE_BINARY_SENSOR
void APIServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state) {
if (obj->is_internal())
return;
for (auto &c : this->clients_)
c->send_binary_sensor_state(obj);
}
API_DISPATCH_UPDATE(binary_sensor::BinarySensor, binary_sensor)
#endif
#ifdef USE_COVER
void APIServer::on_cover_update(cover::Cover *obj) {
if (obj->is_internal())
return;
for (auto &c : this->clients_)
c->send_cover_state(obj);
}
API_DISPATCH_UPDATE(cover::Cover, cover)
#endif
#ifdef USE_FAN
void APIServer::on_fan_update(fan::Fan *obj) {
if (obj->is_internal())
return;
for (auto &c : this->clients_)
c->send_fan_state(obj);
}
API_DISPATCH_UPDATE(fan::Fan, fan)
#endif
#ifdef USE_LIGHT
void APIServer::on_light_update(light::LightState *obj) {
if (obj->is_internal())
return;
for (auto &c : this->clients_)
c->send_light_state(obj);
}
API_DISPATCH_UPDATE(light::LightState, light)
#endif
#ifdef USE_SENSOR
void APIServer::on_sensor_update(sensor::Sensor *obj, float state) {
if (obj->is_internal())
return;
for (auto &c : this->clients_)
c->send_sensor_state(obj);
}
API_DISPATCH_UPDATE_IGNORE_PARAMS(sensor::Sensor, sensor, float state)
#endif
#ifdef USE_SWITCH
void APIServer::on_switch_update(switch_::Switch *obj, bool state) {
if (obj->is_internal())
return;
for (auto &c : this->clients_)
c->send_switch_state(obj);
}
API_DISPATCH_UPDATE_IGNORE_PARAMS(switch_::Switch, switch, bool state)
#endif
#ifdef USE_TEXT_SENSOR
void APIServer::on_text_sensor_update(text_sensor::TextSensor *obj, const std::string &state) {
if (obj->is_internal())
return;
for (auto &c : this->clients_)
c->send_text_sensor_state(obj);
}
API_DISPATCH_UPDATE_IGNORE_PARAMS(text_sensor::TextSensor, text_sensor, const std::string &state)
#endif
#ifdef USE_CLIMATE
void APIServer::on_climate_update(climate::Climate *obj) {
if (obj->is_internal())
return;
for (auto &c : this->clients_)
c->send_climate_state(obj);
}
API_DISPATCH_UPDATE(climate::Climate, climate)
#endif
#ifdef USE_NUMBER
void APIServer::on_number_update(number::Number *obj, float state) {
if (obj->is_internal())
return;
for (auto &c : this->clients_)
c->send_number_state(obj);
}
API_DISPATCH_UPDATE_IGNORE_PARAMS(number::Number, number, float state)
#endif
#ifdef USE_DATETIME_DATE
void APIServer::on_date_update(datetime::DateEntity *obj) {
if (obj->is_internal())
return;
for (auto &c : this->clients_)
c->send_date_state(obj);
}
API_DISPATCH_UPDATE(datetime::DateEntity, date)
#endif
#ifdef USE_DATETIME_TIME
void APIServer::on_time_update(datetime::TimeEntity *obj) {
if (obj->is_internal())
return;
for (auto &c : this->clients_)
c->send_time_state(obj);
}
API_DISPATCH_UPDATE(datetime::TimeEntity, time)
#endif
#ifdef USE_DATETIME_DATETIME
void APIServer::on_datetime_update(datetime::DateTimeEntity *obj) {
if (obj->is_internal())
return;
for (auto &c : this->clients_)
c->send_datetime_state(obj);
}
API_DISPATCH_UPDATE(datetime::DateTimeEntity, datetime)
#endif
#ifdef USE_TEXT
void APIServer::on_text_update(text::Text *obj, const std::string &state) {
if (obj->is_internal())
return;
for (auto &c : this->clients_)
c->send_text_state(obj);
}
API_DISPATCH_UPDATE_IGNORE_PARAMS(text::Text, text, const std::string &state)
#endif
#ifdef USE_SELECT
void APIServer::on_select_update(select::Select *obj, const std::string &state, size_t index) {
if (obj->is_internal())
return;
for (auto &c : this->clients_)
c->send_select_state(obj);
}
API_DISPATCH_UPDATE_IGNORE_PARAMS(select::Select, select, const std::string &state, size_t index)
#endif
#ifdef USE_LOCK
void APIServer::on_lock_update(lock::Lock *obj) {
if (obj->is_internal())
return;
for (auto &c : this->clients_)
c->send_lock_state(obj);
}
API_DISPATCH_UPDATE(lock::Lock, lock)
#endif
#ifdef USE_VALVE
void APIServer::on_valve_update(valve::Valve *obj) {
if (obj->is_internal())
return;
for (auto &c : this->clients_)
c->send_valve_state(obj);
}
API_DISPATCH_UPDATE(valve::Valve, valve)
#endif
#ifdef USE_MEDIA_PLAYER
void APIServer::on_media_player_update(media_player::MediaPlayer *obj) {
if (obj->is_internal())
return;
for (auto &c : this->clients_)
c->send_media_player_state(obj);
}
API_DISPATCH_UPDATE(media_player::MediaPlayer, media_player)
#endif
#ifdef USE_EVENT
// Event is a special case - it's the only entity that passes extra parameters to the send method
void APIServer::on_event(event::Event *obj, const std::string &event_type) {
if (obj->is_internal())
return;
for (auto &c : this->clients_)
c->send_event(obj, event_type);
}
#endif
#ifdef USE_UPDATE
// Update is a special case - the method is called on_update, not on_update_update
void APIServer::on_update(update::UpdateEntity *obj) {
if (obj->is_internal())
return;
for (auto &c : this->clients_)
c->send_update_state(obj);
}
#endif
#ifdef USE_ALARM_CONTROL_PANEL
void APIServer::on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) {
if (obj->is_internal())
return;
for (auto &c : this->clients_)
c->send_alarm_control_panel_state(obj);
}
API_DISPATCH_UPDATE(alarm_control_panel::AlarmControlPanel, alarm_control_panel)
#endif
float APIServer::get_setup_priority() const { return setup_priority::AFTER_WIFI; }
void APIServer::set_port(uint16_t port) { this->port_ = port; }
#ifdef USE_API_PASSWORD
void APIServer::set_password(const std::string &password) { this->password_ = password; }
#endif
void APIServer::set_batch_delay(uint32_t batch_delay) { this->batch_delay_ = batch_delay; }
void APIServer::set_batch_delay(uint16_t batch_delay) { this->batch_delay_ = batch_delay; }
void APIServer::send_homeassistant_service_call(const HomeassistantServiceResponse &call) {
for (auto &client : this->clients_) {
@ -479,7 +450,7 @@ bool APIServer::save_noise_psk(psk_t psk, bool make_active) {
#ifdef USE_HOMEASSISTANT_TIME
void APIServer::request_time() {
for (auto &client : this->clients_) {
if (!client->remove_ && client->is_authenticated())
if (!client->flags_.remove && client->is_authenticated())
client->send_time_request();
}
}
@ -503,8 +474,8 @@ void APIServer::on_shutdown() {
for (auto &c : this->clients_) {
if (!c->send_message(DisconnectRequest())) {
// If we can't send the disconnect request directly (tx_buffer full),
// schedule it in the batch so it will be sent with the 5ms timer
c->schedule_message_(nullptr, &APIConnection::try_send_disconnect_request, DisconnectRequest::MESSAGE_TYPE);
// schedule it at the front of the batch so it will be sent with priority
c->schedule_message_front_(nullptr, &APIConnection::try_send_disconnect_request, DisconnectRequest::MESSAGE_TYPE);
}
}
}

View File

@ -25,6 +25,11 @@ struct SavedNoisePsk {
} PACKED; // NOLINT
#endif
#ifndef USE_API_YAML_SERVICES
// Forward declaration of helper function
const std::vector<UserServiceDescriptor *> &get_empty_user_services_instance();
#endif
class APIServer : public Component, public Controller {
public:
APIServer();
@ -35,13 +40,15 @@ class APIServer : public Component, public Controller {
void dump_config() override;
void on_shutdown() override;
bool teardown() override;
#ifdef USE_API_PASSWORD
bool check_password(const std::string &password) const;
bool uses_password() const;
void set_port(uint16_t port);
void set_password(const std::string &password);
#endif
void set_port(uint16_t port);
void set_reboot_timeout(uint32_t reboot_timeout);
void set_batch_delay(uint32_t batch_delay);
uint32_t get_batch_delay() const { return batch_delay_; }
void set_batch_delay(uint16_t batch_delay);
uint16_t get_batch_delay() const { return batch_delay_; }
// Get reference to shared buffer for API connections
std::vector<uint8_t> &get_shared_buffer_ref() { return shared_write_buffer_; }
@ -54,7 +61,7 @@ class APIServer : public Component, public Controller {
void handle_disconnect(APIConnection *conn);
#ifdef USE_BINARY_SENSOR
void on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state) override;
void on_binary_sensor_update(binary_sensor::BinarySensor *obj) override;
#endif
#ifdef USE_COVER
void on_cover_update(cover::Cover *obj) override;
@ -105,7 +112,18 @@ class APIServer : public Component, public Controller {
void on_media_player_update(media_player::MediaPlayer *obj) override;
#endif
void send_homeassistant_service_call(const HomeassistantServiceResponse &call);
void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); }
void register_user_service(UserServiceDescriptor *descriptor) {
#ifdef USE_API_YAML_SERVICES
// Vector is pre-allocated when services are defined in YAML
this->user_services_.push_back(descriptor);
#else
// Lazy allocate vector on first use for CustomAPIDevice
if (!this->user_services_) {
this->user_services_ = std::make_unique<std::vector<UserServiceDescriptor *>>();
}
this->user_services_->push_back(descriptor);
#endif
}
#ifdef USE_HOMEASSISTANT_TIME
void request_time();
#endif
@ -134,27 +152,63 @@ class APIServer : public Component, public Controller {
void get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(std::string)> f);
const std::vector<HomeAssistantStateSubscription> &get_state_subs() const;
const std::vector<UserServiceDescriptor *> &get_user_services() const { return this->user_services_; }
const std::vector<UserServiceDescriptor *> &get_user_services() const {
#ifdef USE_API_YAML_SERVICES
return this->user_services_;
#else
if (this->user_services_) {
return *this->user_services_;
}
// Return reference to global empty instance (no guard needed)
return get_empty_user_services_instance();
#endif
}
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER
Trigger<std::string, std::string> *get_client_connected_trigger() const { return this->client_connected_trigger_; }
#endif
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
Trigger<std::string, std::string> *get_client_disconnected_trigger() const {
return this->client_disconnected_trigger_;
}
#endif
protected:
bool shutting_down_ = false;
void schedule_reboot_timeout_();
// Pointers and pointer-like types first (4 bytes each)
std::unique_ptr<socket::Socket> socket_ = nullptr;
uint16_t port_{6053};
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER
Trigger<std::string, std::string> *client_connected_trigger_ = new Trigger<std::string, std::string>();
#endif
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
Trigger<std::string, std::string> *client_disconnected_trigger_ = new Trigger<std::string, std::string>();
#endif
// 4-byte aligned types
uint32_t reboot_timeout_{300000};
uint32_t batch_delay_{100};
uint32_t last_connected_{0};
// Vectors and strings (12 bytes each on 32-bit)
std::vector<std::unique_ptr<APIConnection>> clients_;
#ifdef USE_API_PASSWORD
std::string password_;
#endif
std::vector<uint8_t> shared_write_buffer_; // Shared proto write buffer for all connections
std::vector<HomeAssistantStateSubscription> state_subs_;
#ifdef USE_API_YAML_SERVICES
// When services are defined in YAML, we know at compile time that services will be registered
std::vector<UserServiceDescriptor *> user_services_;
Trigger<std::string, std::string> *client_connected_trigger_ = new Trigger<std::string, std::string>();
Trigger<std::string, std::string> *client_disconnected_trigger_ = new Trigger<std::string, std::string>();
#else
// Services can still be registered at runtime by CustomAPIDevice components even when not
// defined in YAML. Using unique_ptr allows lazy allocation, saving 12 bytes in the common
// case where no services (YAML or custom) are used.
std::unique_ptr<std::vector<UserServiceDescriptor *>> user_services_;
#endif
// Group smaller types together
uint16_t port_{6053};
uint16_t batch_delay_{100};
bool shutting_down_ = false;
// 5 bytes used, 3 bytes padding
#ifdef USE_API_NOISE
std::shared_ptr<APINoiseContext> noise_ctx_ = std::make_shared<APINoiseContext>();

View File

@ -4,9 +4,15 @@ import asyncio
from datetime import datetime
import logging
from typing import TYPE_CHECKING, Any
import warnings
from aioesphomeapi import APIClient, parse_log_message
from aioesphomeapi.log_runner import async_run
# Suppress protobuf version warnings
with warnings.catch_warnings():
warnings.filterwarnings(
"ignore", category=UserWarning, message=".*Protobuf gencode version.*"
)
from aioesphomeapi import APIClient, parse_log_message
from aioesphomeapi.log_runner import async_run
from esphome.const import CONF_KEY, CONF_PASSWORD, CONF_PORT, __version__
from esphome.core import CORE

View File

@ -1,6 +1,7 @@
#include "list_entities.h"
#ifdef USE_API
#include "api_connection.h"
#include "api_pb2.h"
#include "esphome/core/application.h"
#include "esphome/core/log.h"
#include "esphome/core/util.h"
@ -8,155 +9,85 @@
namespace esphome {
namespace api {
// Generate entity handler implementations using macros
#ifdef USE_BINARY_SENSOR
bool ListEntitiesIterator::on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) {
this->client_->send_binary_sensor_info(binary_sensor);
return true;
}
LIST_ENTITIES_HANDLER(binary_sensor, binary_sensor::BinarySensor, ListEntitiesBinarySensorResponse)
#endif
#ifdef USE_COVER
bool ListEntitiesIterator::on_cover(cover::Cover *cover) {
this->client_->send_cover_info(cover);
return true;
}
LIST_ENTITIES_HANDLER(cover, cover::Cover, ListEntitiesCoverResponse)
#endif
#ifdef USE_FAN
bool ListEntitiesIterator::on_fan(fan::Fan *fan) {
this->client_->send_fan_info(fan);
return true;
}
LIST_ENTITIES_HANDLER(fan, fan::Fan, ListEntitiesFanResponse)
#endif
#ifdef USE_LIGHT
bool ListEntitiesIterator::on_light(light::LightState *light) {
this->client_->send_light_info(light);
return true;
}
LIST_ENTITIES_HANDLER(light, light::LightState, ListEntitiesLightResponse)
#endif
#ifdef USE_SENSOR
bool ListEntitiesIterator::on_sensor(sensor::Sensor *sensor) {
this->client_->send_sensor_info(sensor);
return true;
}
LIST_ENTITIES_HANDLER(sensor, sensor::Sensor, ListEntitiesSensorResponse)
#endif
#ifdef USE_SWITCH
bool ListEntitiesIterator::on_switch(switch_::Switch *a_switch) {
this->client_->send_switch_info(a_switch);
return true;
}
LIST_ENTITIES_HANDLER(switch, switch_::Switch, ListEntitiesSwitchResponse)
#endif
#ifdef USE_BUTTON
bool ListEntitiesIterator::on_button(button::Button *button) {
this->client_->send_button_info(button);
return true;
}
LIST_ENTITIES_HANDLER(button, button::Button, ListEntitiesButtonResponse)
#endif
#ifdef USE_TEXT_SENSOR
bool ListEntitiesIterator::on_text_sensor(text_sensor::TextSensor *text_sensor) {
this->client_->send_text_sensor_info(text_sensor);
return true;
}
LIST_ENTITIES_HANDLER(text_sensor, text_sensor::TextSensor, ListEntitiesTextSensorResponse)
#endif
#ifdef USE_LOCK
bool ListEntitiesIterator::on_lock(lock::Lock *a_lock) {
this->client_->send_lock_info(a_lock);
return true;
}
LIST_ENTITIES_HANDLER(lock, lock::Lock, ListEntitiesLockResponse)
#endif
#ifdef USE_VALVE
bool ListEntitiesIterator::on_valve(valve::Valve *valve) {
this->client_->send_valve_info(valve);
return true;
}
LIST_ENTITIES_HANDLER(valve, valve::Valve, ListEntitiesValveResponse)
#endif
#ifdef USE_CAMERA
LIST_ENTITIES_HANDLER(camera, camera::Camera, ListEntitiesCameraResponse)
#endif
#ifdef USE_CLIMATE
LIST_ENTITIES_HANDLER(climate, climate::Climate, ListEntitiesClimateResponse)
#endif
#ifdef USE_NUMBER
LIST_ENTITIES_HANDLER(number, number::Number, ListEntitiesNumberResponse)
#endif
#ifdef USE_DATETIME_DATE
LIST_ENTITIES_HANDLER(date, datetime::DateEntity, ListEntitiesDateResponse)
#endif
#ifdef USE_DATETIME_TIME
LIST_ENTITIES_HANDLER(time, datetime::TimeEntity, ListEntitiesTimeResponse)
#endif
#ifdef USE_DATETIME_DATETIME
LIST_ENTITIES_HANDLER(datetime, datetime::DateTimeEntity, ListEntitiesDateTimeResponse)
#endif
#ifdef USE_TEXT
LIST_ENTITIES_HANDLER(text, text::Text, ListEntitiesTextResponse)
#endif
#ifdef USE_SELECT
LIST_ENTITIES_HANDLER(select, select::Select, ListEntitiesSelectResponse)
#endif
#ifdef USE_MEDIA_PLAYER
LIST_ENTITIES_HANDLER(media_player, media_player::MediaPlayer, ListEntitiesMediaPlayerResponse)
#endif
#ifdef USE_ALARM_CONTROL_PANEL
LIST_ENTITIES_HANDLER(alarm_control_panel, alarm_control_panel::AlarmControlPanel,
ListEntitiesAlarmControlPanelResponse)
#endif
#ifdef USE_EVENT
LIST_ENTITIES_HANDLER(event, event::Event, ListEntitiesEventResponse)
#endif
#ifdef USE_UPDATE
LIST_ENTITIES_HANDLER(update, update::UpdateEntity, ListEntitiesUpdateResponse)
#endif
// Special cases that don't follow the pattern
bool ListEntitiesIterator::on_end() { return this->client_->send_list_info_done(); }
ListEntitiesIterator::ListEntitiesIterator(APIConnection *client) : client_(client) {}
bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) {
auto resp = service->encode_list_service_response();
return this->client_->send_message(resp);
}
#ifdef USE_ESP32_CAMERA
bool ListEntitiesIterator::on_camera(esp32_camera::ESP32Camera *camera) {
this->client_->send_camera_info(camera);
return true;
}
#endif
#ifdef USE_CLIMATE
bool ListEntitiesIterator::on_climate(climate::Climate *climate) {
this->client_->send_climate_info(climate);
return true;
}
#endif
#ifdef USE_NUMBER
bool ListEntitiesIterator::on_number(number::Number *number) {
this->client_->send_number_info(number);
return true;
}
#endif
#ifdef USE_DATETIME_DATE
bool ListEntitiesIterator::on_date(datetime::DateEntity *date) {
this->client_->send_date_info(date);
return true;
}
#endif
#ifdef USE_DATETIME_TIME
bool ListEntitiesIterator::on_time(datetime::TimeEntity *time) {
this->client_->send_time_info(time);
return true;
}
#endif
#ifdef USE_DATETIME_DATETIME
bool ListEntitiesIterator::on_datetime(datetime::DateTimeEntity *datetime) {
this->client_->send_datetime_info(datetime);
return true;
}
#endif
#ifdef USE_TEXT
bool ListEntitiesIterator::on_text(text::Text *text) {
this->client_->send_text_info(text);
return true;
}
#endif
#ifdef USE_SELECT
bool ListEntitiesIterator::on_select(select::Select *select) {
this->client_->send_select_info(select);
return true;
}
#endif
#ifdef USE_MEDIA_PLAYER
bool ListEntitiesIterator::on_media_player(media_player::MediaPlayer *media_player) {
this->client_->send_media_player_info(media_player);
return true;
}
#endif
#ifdef USE_ALARM_CONTROL_PANEL
bool ListEntitiesIterator::on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) {
this->client_->send_alarm_control_panel_info(a_alarm_control_panel);
return true;
}
#endif
#ifdef USE_EVENT
bool ListEntitiesIterator::on_event(event::Event *event) {
this->client_->send_event_info(event);
return true;
}
#endif
#ifdef USE_UPDATE
bool ListEntitiesIterator::on_update(update::UpdateEntity *update) {
this->client_->send_update_info(update);
return true;
}
#endif
} // namespace api
} // namespace esphome
#endif

View File

@ -9,75 +9,83 @@ namespace api {
class APIConnection;
// Macro for generating ListEntitiesIterator handlers
// Calls schedule_message_ with try_send_*_info
#define LIST_ENTITIES_HANDLER(entity_type, EntityClass, ResponseType) \
bool ListEntitiesIterator::on_##entity_type(EntityClass *entity) { /* NOLINT(bugprone-macro-parentheses) */ \
return this->client_->schedule_message_(entity, &APIConnection::try_send_##entity_type##_info, \
ResponseType::MESSAGE_TYPE); \
}
class ListEntitiesIterator : public ComponentIterator {
public:
ListEntitiesIterator(APIConnection *client);
#ifdef USE_BINARY_SENSOR
bool on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) override;
bool on_binary_sensor(binary_sensor::BinarySensor *entity) override;
#endif
#ifdef USE_COVER
bool on_cover(cover::Cover *cover) override;
bool on_cover(cover::Cover *entity) override;
#endif
#ifdef USE_FAN
bool on_fan(fan::Fan *fan) override;
bool on_fan(fan::Fan *entity) override;
#endif
#ifdef USE_LIGHT
bool on_light(light::LightState *light) override;
bool on_light(light::LightState *entity) override;
#endif
#ifdef USE_SENSOR
bool on_sensor(sensor::Sensor *sensor) override;
bool on_sensor(sensor::Sensor *entity) override;
#endif
#ifdef USE_SWITCH
bool on_switch(switch_::Switch *a_switch) override;
bool on_switch(switch_::Switch *entity) override;
#endif
#ifdef USE_BUTTON
bool on_button(button::Button *button) override;
bool on_button(button::Button *entity) override;
#endif
#ifdef USE_TEXT_SENSOR
bool on_text_sensor(text_sensor::TextSensor *text_sensor) override;
bool on_text_sensor(text_sensor::TextSensor *entity) override;
#endif
bool on_service(UserServiceDescriptor *service) override;
#ifdef USE_ESP32_CAMERA
bool on_camera(esp32_camera::ESP32Camera *camera) override;
#ifdef USE_CAMERA
bool on_camera(camera::Camera *entity) override;
#endif
#ifdef USE_CLIMATE
bool on_climate(climate::Climate *climate) override;
bool on_climate(climate::Climate *entity) override;
#endif
#ifdef USE_NUMBER
bool on_number(number::Number *number) override;
bool on_number(number::Number *entity) override;
#endif
#ifdef USE_DATETIME_DATE
bool on_date(datetime::DateEntity *date) override;
bool on_date(datetime::DateEntity *entity) override;
#endif
#ifdef USE_DATETIME_TIME
bool on_time(datetime::TimeEntity *time) override;
bool on_time(datetime::TimeEntity *entity) override;
#endif
#ifdef USE_DATETIME_DATETIME
bool on_datetime(datetime::DateTimeEntity *datetime) override;
bool on_datetime(datetime::DateTimeEntity *entity) override;
#endif
#ifdef USE_TEXT
bool on_text(text::Text *text) override;
bool on_text(text::Text *entity) override;
#endif
#ifdef USE_SELECT
bool on_select(select::Select *select) override;
bool on_select(select::Select *entity) override;
#endif
#ifdef USE_LOCK
bool on_lock(lock::Lock *a_lock) override;
bool on_lock(lock::Lock *entity) override;
#endif
#ifdef USE_VALVE
bool on_valve(valve::Valve *valve) override;
bool on_valve(valve::Valve *entity) override;
#endif
#ifdef USE_MEDIA_PLAYER
bool on_media_player(media_player::MediaPlayer *media_player) override;
bool on_media_player(media_player::MediaPlayer *entity) override;
#endif
#ifdef USE_ALARM_CONTROL_PANEL
bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) override;
bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *entity) override;
#endif
#ifdef USE_EVENT
bool on_event(event::Event *event) override;
bool on_event(event::Event *entity) override;
#endif
#ifdef USE_UPDATE
bool on_update(update::UpdateEntity *update) override;
bool on_update(update::UpdateEntity *entity) override;
#endif
bool on_end() override;
bool completed() { return this->state_ == IteratorState::NONE; }

View File

@ -327,12 +327,15 @@ class ProtoWriteBuffer {
class ProtoMessage {
public:
virtual ~ProtoMessage() = default;
virtual void encode(ProtoWriteBuffer buffer) const = 0;
// Default implementation for messages with no fields
virtual void encode(ProtoWriteBuffer buffer) const {}
void decode(const uint8_t *buffer, size_t length);
virtual void calculate_size(uint32_t &total_size) const = 0;
// Default implementation for messages with no fields
virtual void calculate_size(uint32_t &total_size) const {}
#ifdef HAS_PROTO_MESSAGE_DUMP
std::string dump() const;
virtual void dump_to(std::string &out) const = 0;
virtual const char *message_name() const { return "unknown"; }
#endif
protected:
@ -361,7 +364,7 @@ class ProtoService {
*/
virtual ProtoWriteBuffer create_buffer(uint32_t reserve_size) = 0;
virtual bool send_buffer(ProtoWriteBuffer buffer, uint16_t message_type) = 0;
virtual bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) = 0;
virtual void read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) = 0;
// Optimized method that pre-allocates buffer based on message size
bool send_message_(const ProtoMessage &msg, uint16_t message_type) {
@ -377,6 +380,26 @@ class ProtoService {
// Send the buffer
return this->send_buffer(buffer, message_type);
}
// Authentication helper methods
bool check_connection_setup_() {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return false;
}
return true;
}
bool check_authenticated_() {
if (!this->check_connection_setup_()) {
return false;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return false;
}
return true;
}
};
} // namespace api

View File

@ -6,73 +6,67 @@
namespace esphome {
namespace api {
// Generate entity handler implementations using macros
#ifdef USE_BINARY_SENSOR
bool InitialStateIterator::on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) {
return this->client_->send_binary_sensor_state(binary_sensor);
}
INITIAL_STATE_HANDLER(binary_sensor, binary_sensor::BinarySensor)
#endif
#ifdef USE_COVER
bool InitialStateIterator::on_cover(cover::Cover *cover) { return this->client_->send_cover_state(cover); }
INITIAL_STATE_HANDLER(cover, cover::Cover)
#endif
#ifdef USE_FAN
bool InitialStateIterator::on_fan(fan::Fan *fan) { return this->client_->send_fan_state(fan); }
INITIAL_STATE_HANDLER(fan, fan::Fan)
#endif
#ifdef USE_LIGHT
bool InitialStateIterator::on_light(light::LightState *light) { return this->client_->send_light_state(light); }
INITIAL_STATE_HANDLER(light, light::LightState)
#endif
#ifdef USE_SENSOR
bool InitialStateIterator::on_sensor(sensor::Sensor *sensor) { return this->client_->send_sensor_state(sensor); }
INITIAL_STATE_HANDLER(sensor, sensor::Sensor)
#endif
#ifdef USE_SWITCH
bool InitialStateIterator::on_switch(switch_::Switch *a_switch) { return this->client_->send_switch_state(a_switch); }
INITIAL_STATE_HANDLER(switch, switch_::Switch)
#endif
#ifdef USE_TEXT_SENSOR
bool InitialStateIterator::on_text_sensor(text_sensor::TextSensor *text_sensor) {
return this->client_->send_text_sensor_state(text_sensor);
}
INITIAL_STATE_HANDLER(text_sensor, text_sensor::TextSensor)
#endif
#ifdef USE_CLIMATE
bool InitialStateIterator::on_climate(climate::Climate *climate) { return this->client_->send_climate_state(climate); }
INITIAL_STATE_HANDLER(climate, climate::Climate)
#endif
#ifdef USE_NUMBER
bool InitialStateIterator::on_number(number::Number *number) { return this->client_->send_number_state(number); }
INITIAL_STATE_HANDLER(number, number::Number)
#endif
#ifdef USE_DATETIME_DATE
bool InitialStateIterator::on_date(datetime::DateEntity *date) { return this->client_->send_date_state(date); }
INITIAL_STATE_HANDLER(date, datetime::DateEntity)
#endif
#ifdef USE_DATETIME_TIME
bool InitialStateIterator::on_time(datetime::TimeEntity *time) { return this->client_->send_time_state(time); }
INITIAL_STATE_HANDLER(time, datetime::TimeEntity)
#endif
#ifdef USE_DATETIME_DATETIME
bool InitialStateIterator::on_datetime(datetime::DateTimeEntity *datetime) {
return this->client_->send_datetime_state(datetime);
}
INITIAL_STATE_HANDLER(datetime, datetime::DateTimeEntity)
#endif
#ifdef USE_TEXT
bool InitialStateIterator::on_text(text::Text *text) { return this->client_->send_text_state(text); }
INITIAL_STATE_HANDLER(text, text::Text)
#endif
#ifdef USE_SELECT
bool InitialStateIterator::on_select(select::Select *select) { return this->client_->send_select_state(select); }
INITIAL_STATE_HANDLER(select, select::Select)
#endif
#ifdef USE_LOCK
bool InitialStateIterator::on_lock(lock::Lock *a_lock) { return this->client_->send_lock_state(a_lock); }
INITIAL_STATE_HANDLER(lock, lock::Lock)
#endif
#ifdef USE_VALVE
bool InitialStateIterator::on_valve(valve::Valve *valve) { return this->client_->send_valve_state(valve); }
INITIAL_STATE_HANDLER(valve, valve::Valve)
#endif
#ifdef USE_MEDIA_PLAYER
bool InitialStateIterator::on_media_player(media_player::MediaPlayer *media_player) {
return this->client_->send_media_player_state(media_player);
}
INITIAL_STATE_HANDLER(media_player, media_player::MediaPlayer)
#endif
#ifdef USE_ALARM_CONTROL_PANEL
bool InitialStateIterator::on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) {
return this->client_->send_alarm_control_panel_state(a_alarm_control_panel);
}
INITIAL_STATE_HANDLER(alarm_control_panel, alarm_control_panel::AlarmControlPanel)
#endif
#ifdef USE_UPDATE
bool InitialStateIterator::on_update(update::UpdateEntity *update) { return this->client_->send_update_state(update); }
INITIAL_STATE_HANDLER(update, update::UpdateEntity)
#endif
// Special cases (button and event) are already defined inline in subscribe_state.h
InitialStateIterator::InitialStateIterator(APIConnection *client) : client_(client) {}
} // namespace api

View File

@ -10,71 +10,78 @@ namespace api {
class APIConnection;
// Macro for generating InitialStateIterator handlers
// Calls send_*_state
#define INITIAL_STATE_HANDLER(entity_type, EntityClass) \
bool InitialStateIterator::on_##entity_type(EntityClass *entity) { /* NOLINT(bugprone-macro-parentheses) */ \
return this->client_->send_##entity_type##_state(entity); \
}
class InitialStateIterator : public ComponentIterator {
public:
InitialStateIterator(APIConnection *client);
#ifdef USE_BINARY_SENSOR
bool on_binary_sensor(binary_sensor::BinarySensor *binary_sensor) override;
bool on_binary_sensor(binary_sensor::BinarySensor *entity) override;
#endif
#ifdef USE_COVER
bool on_cover(cover::Cover *cover) override;
bool on_cover(cover::Cover *entity) override;
#endif
#ifdef USE_FAN
bool on_fan(fan::Fan *fan) override;
bool on_fan(fan::Fan *entity) override;
#endif
#ifdef USE_LIGHT
bool on_light(light::LightState *light) override;
bool on_light(light::LightState *entity) override;
#endif
#ifdef USE_SENSOR
bool on_sensor(sensor::Sensor *sensor) override;
bool on_sensor(sensor::Sensor *entity) override;
#endif
#ifdef USE_SWITCH
bool on_switch(switch_::Switch *a_switch) override;
bool on_switch(switch_::Switch *entity) override;
#endif
#ifdef USE_BUTTON
bool on_button(button::Button *button) override { return true; };
#endif
#ifdef USE_TEXT_SENSOR
bool on_text_sensor(text_sensor::TextSensor *text_sensor) override;
bool on_text_sensor(text_sensor::TextSensor *entity) override;
#endif
#ifdef USE_CLIMATE
bool on_climate(climate::Climate *climate) override;
bool on_climate(climate::Climate *entity) override;
#endif
#ifdef USE_NUMBER
bool on_number(number::Number *number) override;
bool on_number(number::Number *entity) override;
#endif
#ifdef USE_DATETIME_DATE
bool on_date(datetime::DateEntity *date) override;
bool on_date(datetime::DateEntity *entity) override;
#endif
#ifdef USE_DATETIME_TIME
bool on_time(datetime::TimeEntity *time) override;
bool on_time(datetime::TimeEntity *entity) override;
#endif
#ifdef USE_DATETIME_DATETIME
bool on_datetime(datetime::DateTimeEntity *datetime) override;
bool on_datetime(datetime::DateTimeEntity *entity) override;
#endif
#ifdef USE_TEXT
bool on_text(text::Text *text) override;
bool on_text(text::Text *entity) override;
#endif
#ifdef USE_SELECT
bool on_select(select::Select *select) override;
bool on_select(select::Select *entity) override;
#endif
#ifdef USE_LOCK
bool on_lock(lock::Lock *a_lock) override;
bool on_lock(lock::Lock *entity) override;
#endif
#ifdef USE_VALVE
bool on_valve(valve::Valve *valve) override;
bool on_valve(valve::Valve *entity) override;
#endif
#ifdef USE_MEDIA_PLAYER
bool on_media_player(media_player::MediaPlayer *media_player) override;
bool on_media_player(media_player::MediaPlayer *entity) override;
#endif
#ifdef USE_ALARM_CONTROL_PANEL
bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) override;
bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *entity) override;
#endif
#ifdef USE_EVENT
bool on_event(event::Event *event) override { return true; };
#endif
#ifdef USE_UPDATE
bool on_update(update::UpdateEntity *update) override;
bool on_update(update::UpdateEntity *entity) override;
#endif
bool completed() { return this->state_ == IteratorState::NONE; }

View File

@ -50,7 +50,6 @@ class AS5600Component : public Component, public i2c::I2CDevice {
void setup() override;
void dump_config() override;
/// HARDWARE_LATE setup priority
float get_setup_priority() const override { return setup_priority::DATA; }
// configuration setters
void set_dir_pin(InternalGPIOPin *pin) { this->dir_pin_ = pin; }

View File

@ -5,6 +5,7 @@ from esphome.const import (
PLATFORM_BK72XX,
PLATFORM_ESP32,
PLATFORM_ESP8266,
PLATFORM_LN882X,
PLATFORM_RTL87XX,
)
from esphome.core import CORE, coroutine_with_priority
@ -14,15 +15,23 @@ CODEOWNERS = ["@OttoWinter"]
CONFIG_SCHEMA = cv.All(
cv.Schema({}),
cv.only_with_arduino,
cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_BK72XX, PLATFORM_RTL87XX]),
cv.only_on(
[
PLATFORM_ESP32,
PLATFORM_ESP8266,
PLATFORM_BK72XX,
PLATFORM_LN882X,
PLATFORM_RTL87XX,
]
),
)
@coroutine_with_priority(200.0)
async def to_code(config):
if CORE.is_esp32 or CORE.is_libretiny:
# https://github.com/esphome/AsyncTCP/blob/master/library.json
cg.add_library("esphome/AsyncTCP-esphome", "2.1.4")
# https://github.com/ESP32Async/AsyncTCP
cg.add_library("ESP32Async/AsyncTCP", "3.4.4")
elif CORE.is_esp8266:
# https://github.com/esphome/ESPAsyncTCP
cg.add_library("esphome/ESPAsyncTCP-esphome", "2.0.0")
# https://github.com/ESP32Async/ESPAsyncTCP
cg.add_library("ESP32Async/ESPAsyncTCP", "2.0.0")

View File

@ -25,7 +25,6 @@ class ATCMiThermometer : public Component, public esp32_ble_tracker::ESPBTDevice
bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; }
void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; }
void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; }

View File

@ -1,6 +1,7 @@
#include "atm90e32.h"
#include <cinttypes>
#include <cmath>
#include <numbers>
#include "esphome/core/log.h"
namespace esphome {
@ -848,7 +849,7 @@ uint16_t ATM90E32Component::calculate_voltage_threshold(int line_freq, uint16_t
float nominal_voltage = (line_freq == 60) ? 120.0f : 220.0f;
float target_voltage = nominal_voltage * multiplier;
float peak_01v = target_voltage * 100.0f * std::sqrt(2.0f); // convert RMS → peak, scale to 0.01V
float peak_01v = target_voltage * 100.0f * std::numbers::sqrt2_v<float>; // convert RMS → peak, scale to 0.01V
float divider = (2.0f * ugain) / 32768.0f;
float threshold = peak_01v / divider;

View File

@ -312,7 +312,7 @@ FileDecoderState AudioDecoder::decode_mp3_() {
if (err) {
switch (err) {
case esp_audio_libs::helix_decoder::ERR_MP3_OUT_OF_MEMORY:
// Intentional fallthrough
[[fallthrough]];
case esp_audio_libs::helix_decoder::ERR_MP3_NULL_POINTER:
return FileDecoderState::FAILED;
break;

View File

@ -86,7 +86,7 @@ bool AudioTransferBuffer::reallocate(size_t new_buffer_size) {
bool AudioTransferBuffer::allocate_buffer_(size_t buffer_size) {
this->buffer_size_ = buffer_size;
RAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
RAMAllocator<uint8_t> allocator;
this->buffer_ = allocator.allocate(this->buffer_size_);
if (this->buffer_ == nullptr) {
@ -101,7 +101,7 @@ bool AudioTransferBuffer::allocate_buffer_(size_t buffer_size) {
void AudioTransferBuffer::deallocate_buffer_() {
if (this->buffer_ != nullptr) {
RAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
RAMAllocator<uint8_t> allocator;
allocator.deallocate(this->buffer_, this->buffer_size_);
this->buffer_ = nullptr;
this->data_start_ = nullptr;

View File

@ -16,7 +16,6 @@ class BParasite : public Component, public esp32_ble_tracker::ESPBTDeviceListene
bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
void set_battery_voltage(sensor::Sensor *battery_voltage) { battery_voltage_ = battery_voltage; }
void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; }

View File

@ -480,7 +480,11 @@ void BedJetHub::set_clock(uint8_t hour, uint8_t minute) {
/* Internal */
void BedJetHub::loop() {}
void BedJetHub::loop() {
// Parent BLEClientNode has a loop() method, but this component uses
// polling via update() and BLE callbacks so loop isn't needed
this->disable_loop();
}
void BedJetHub::update() { this->dispatch_status_(); }
void BedJetHub::dump_config() {

View File

@ -83,7 +83,11 @@ void BedJetClimate::reset_state_() {
this->publish_state();
}
void BedJetClimate::loop() {}
void BedJetClimate::loop() {
// This component is controlled via the parent BedJetHub
// Empty loop not needed, disable to save CPU cycles
this->disable_loop();
}
void BedJetClimate::control(const ClimateCall &call) {
ESP_LOGD(TAG, "Received BedJetClimate::control");

View File

@ -7,11 +7,13 @@
extern "C" {
#include "rtos_pub.h"
#include "spi.h"
// rtos_pub.h must be included before the rest of the includes
#include "arm_arch.h"
#include "general_dma_pub.h"
#include "gpio_pub.h"
#include "icu_pub.h"
#include "spi.h"
#undef SPI_DAT
#undef SPI_BASE
};
@ -124,7 +126,7 @@ void BekenSPILEDStripLightOutput::setup() {
size_t buffer_size = this->get_buffer_size_();
size_t dma_buffer_size = (buffer_size * 8) + (2 * 64);
ExternalRAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
RAMAllocator<uint8_t> allocator;
this->buf_ = allocator.allocate(buffer_size);
if (this->buf_ == nullptr) {
ESP_LOGE(TAG, "Cannot allocate LED buffer!");

View File

@ -50,7 +50,7 @@ void BH1750Sensor::read_lx_(BH1750Mode mode, uint8_t mtreg, const std::function<
// turn on (after one-shot sensor automatically powers down)
uint8_t turn_on = BH1750_COMMAND_POWER_ON;
if (this->write(&turn_on, 1) != i2c::ERROR_OK) {
ESP_LOGW(TAG, "Turning on BH1750 failed");
ESP_LOGW(TAG, "Power on failed");
f(NAN);
return;
}
@ -60,7 +60,7 @@ void BH1750Sensor::read_lx_(BH1750Mode mode, uint8_t mtreg, const std::function<
uint8_t mtreg_hi = BH1750_COMMAND_MT_REG_HI | ((mtreg >> 5) & 0b111);
uint8_t mtreg_lo = BH1750_COMMAND_MT_REG_LO | ((mtreg >> 0) & 0b11111);
if (this->write(&mtreg_hi, 1) != i2c::ERROR_OK || this->write(&mtreg_lo, 1) != i2c::ERROR_OK) {
ESP_LOGW(TAG, "Setting measurement time for BH1750 failed");
ESP_LOGW(TAG, "Set measurement time failed");
active_mtreg_ = 0;
f(NAN);
return;
@ -88,7 +88,7 @@ void BH1750Sensor::read_lx_(BH1750Mode mode, uint8_t mtreg, const std::function<
return;
}
if (this->write(&cmd, 1) != i2c::ERROR_OK) {
ESP_LOGW(TAG, "Starting measurement for BH1750 failed");
ESP_LOGW(TAG, "Start measurement failed");
f(NAN);
return;
}
@ -99,7 +99,7 @@ void BH1750Sensor::read_lx_(BH1750Mode mode, uint8_t mtreg, const std::function<
this->set_timeout("read", meas_time, [this, mode, mtreg, f]() {
uint16_t raw_value;
if (this->read(reinterpret_cast<uint8_t *>(&raw_value), 2) != i2c::ERROR_OK) {
ESP_LOGW(TAG, "Reading BH1750 data failed");
ESP_LOGW(TAG, "Read data failed");
f(NAN);
return;
}
@ -156,7 +156,7 @@ void BH1750Sensor::update() {
this->publish_state(NAN);
return;
}
ESP_LOGD(TAG, "'%s': Got illuminance=%.1flx", this->get_name().c_str(), val);
ESP_LOGD(TAG, "'%s': Illuminance=%.1flx", this->get_name().c_str(), val);
this->status_clear_warning();
this->publish_state(val);
});

View File

@ -1,7 +1,10 @@
from logging import getLogger
from esphome import automation, core
from esphome.automation import Condition, maybe_simple_id
import esphome.codegen as cg
from esphome.components import mqtt, web_server
from esphome.components.const import CONF_ON_STATE_CHANGE
import esphome.config_validation as cv
from esphome.const import (
CONF_DELAY,
@ -57,8 +60,8 @@ from esphome.const import (
DEVICE_CLASS_WINDOW,
)
from esphome.core import CORE, coroutine_with_priority
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
from esphome.cpp_generator import MockObjClass
from esphome.cpp_helpers import setup_entity
from esphome.util import Registry
CODEOWNERS = ["@esphome/core"]
@ -98,6 +101,7 @@ IS_PLATFORM_COMPONENT = True
CONF_TIME_OFF = "time_off"
CONF_TIME_ON = "time_on"
CONF_TRIGGER_ON_INITIAL_STATE = "trigger_on_initial_state"
DEFAULT_DELAY = "1s"
DEFAULT_TIME_OFF = "100ms"
@ -127,15 +131,24 @@ MultiClickTriggerEvent = binary_sensor_ns.struct("MultiClickTriggerEvent")
StateTrigger = binary_sensor_ns.class_(
"StateTrigger", automation.Trigger.template(bool)
)
StateChangeTrigger = binary_sensor_ns.class_(
"StateChangeTrigger",
automation.Trigger.template(cg.optional.template(bool), cg.optional.template(bool)),
)
BinarySensorPublishAction = binary_sensor_ns.class_(
"BinarySensorPublishAction", automation.Action
)
BinarySensorInvalidateAction = binary_sensor_ns.class_(
"BinarySensorInvalidateAction", automation.Action
)
# Condition
BinarySensorCondition = binary_sensor_ns.class_("BinarySensorCondition", Condition)
# Filters
Filter = binary_sensor_ns.class_("Filter")
TimeoutFilter = binary_sensor_ns.class_("TimeoutFilter", Filter, cg.Component)
DelayedOnOffFilter = binary_sensor_ns.class_("DelayedOnOffFilter", Filter, cg.Component)
DelayedOnFilter = binary_sensor_ns.class_("DelayedOnFilter", Filter, cg.Component)
DelayedOffFilter = binary_sensor_ns.class_("DelayedOffFilter", Filter, cg.Component)
@ -144,6 +157,8 @@ AutorepeatFilter = binary_sensor_ns.class_("AutorepeatFilter", Filter, cg.Compon
LambdaFilter = binary_sensor_ns.class_("LambdaFilter", Filter)
SettleFilter = binary_sensor_ns.class_("SettleFilter", Filter, cg.Component)
_LOGGER = getLogger(__name__)
FILTER_REGISTRY = Registry()
validate_filters = cv.validate_registry("filter", FILTER_REGISTRY)
@ -157,6 +172,19 @@ async def invert_filter_to_code(config, filter_id):
return cg.new_Pvariable(filter_id)
@register_filter(
"timeout",
TimeoutFilter,
cv.templatable(cv.positive_time_period_milliseconds),
)
async def timeout_filter_to_code(config, filter_id):
var = cg.new_Pvariable(filter_id)
await cg.register_component(var, {})
template_ = await cg.templatable(config, [], cg.uint32)
cg.add(var.set_timeout_value(template_))
return var
@register_filter(
"delayed_on_off",
DelayedOnOffFilter,
@ -386,6 +414,14 @@ def validate_click_timing(value):
return value
def validate_publish_initial_state(value):
value = cv.boolean(value)
_LOGGER.warning(
"The 'publish_initial_state' option has been replaced by 'trigger_on_initial_state' and will be removed in a future release"
)
return value
_BINARY_SENSOR_SCHEMA = (
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
.extend(cv.MQTT_COMPONENT_SCHEMA)
@ -395,7 +431,12 @@ _BINARY_SENSOR_SCHEMA = (
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(
mqtt.MQTTBinarySensorComponent
),
cv.Optional(CONF_PUBLISH_INITIAL_STATE): cv.boolean,
cv.Exclusive(
CONF_PUBLISH_INITIAL_STATE, CONF_TRIGGER_ON_INITIAL_STATE
): validate_publish_initial_state,
cv.Exclusive(
CONF_TRIGGER_ON_INITIAL_STATE, CONF_TRIGGER_ON_INITIAL_STATE
): cv.boolean,
cv.Optional(CONF_DEVICE_CLASS): validate_device_class,
cv.Optional(CONF_FILTERS): validate_filters,
cv.Optional(CONF_ON_PRESS): automation.validate_automation(
@ -454,11 +495,19 @@ _BINARY_SENSOR_SCHEMA = (
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateTrigger),
}
),
cv.Optional(CONF_ON_STATE_CHANGE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateChangeTrigger),
}
),
}
)
)
_BINARY_SENSOR_SCHEMA.add_extra(entity_duplicate_validator("binary_sensor"))
def binary_sensor_schema(
class_: MockObjClass = cv.UNDEFINED,
*,
@ -489,12 +538,14 @@ BINARY_SENSOR_SCHEMA.add_extra(cv.deprecated_schema_constant("binary_sensor"))
async def setup_binary_sensor_core_(var, config):
await setup_entity(var, config)
await setup_entity(var, config, "binary_sensor")
if (device_class := config.get(CONF_DEVICE_CLASS)) is not None:
cg.add(var.set_device_class(device_class))
if publish_initial_state := config.get(CONF_PUBLISH_INITIAL_STATE):
cg.add(var.set_publish_initial_state(publish_initial_state))
trigger = config.get(CONF_TRIGGER_ON_INITIAL_STATE, False) or config.get(
CONF_PUBLISH_INITIAL_STATE, False
)
cg.add(var.set_trigger_on_initial_state(trigger))
if inverted := config.get(CONF_INVERTED):
cg.add(var.set_inverted(inverted))
if filters_config := config.get(CONF_FILTERS):
@ -542,6 +593,17 @@ async def setup_binary_sensor_core_(var, config):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [(bool, "x")], conf)
for conf in config.get(CONF_ON_STATE_CHANGE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(
trigger,
[
(cg.optional.template(bool), "x_previous"),
(cg.optional.template(bool), "x"),
],
conf,
)
if mqtt_id := config.get(CONF_MQTT_ID):
mqtt_ = cg.new_Pvariable(mqtt_id, var)
await mqtt.register_mqtt_component(mqtt_, config)
@ -591,3 +653,18 @@ async def binary_sensor_is_off_to_code(config, condition_id, template_arg, args)
async def to_code(config):
cg.add_define("USE_BINARY_SENSOR")
cg.add_global(binary_sensor_ns.using)
@automation.register_action(
"binary_sensor.invalidate_state",
BinarySensorInvalidateAction,
cv.maybe_simple_value(
{
cv.Required(CONF_ID): cv.use_id(BinarySensor),
},
key=CONF_ID,
),
)
async def binary_sensor_invalidate_state_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)

View File

@ -96,7 +96,7 @@ class MultiClickTrigger : public Trigger<>, public Component {
: parent_(parent), timing_(std::move(timing)) {}
void setup() override {
this->last_state_ = this->parent_->state;
this->last_state_ = this->parent_->get_state_default(false);
auto f = std::bind(&MultiClickTrigger::on_state_, this, std::placeholders::_1);
this->parent_->add_on_state_callback(f);
}
@ -130,6 +130,14 @@ class StateTrigger : public Trigger<bool> {
}
};
class StateChangeTrigger : public Trigger<optional<bool>, optional<bool> > {
public:
explicit StateChangeTrigger(BinarySensor *parent) {
parent->add_full_state_callback(
[this](optional<bool> old_state, optional<bool> state) { this->trigger(old_state, state); });
}
};
template<typename... Ts> class BinarySensorCondition : public Condition<Ts...> {
public:
BinarySensorCondition(BinarySensor *parent, bool state) : parent_(parent), state_(state) {}
@ -154,5 +162,15 @@ template<typename... Ts> class BinarySensorPublishAction : public Action<Ts...>
BinarySensor *sensor_;
};
template<typename... Ts> class BinarySensorInvalidateAction : public Action<Ts...> {
public:
explicit BinarySensorInvalidateAction(BinarySensor *sensor) : sensor_(sensor) {}
void play(Ts... x) override { this->sensor_->invalidate_state(); }
protected:
BinarySensor *sensor_;
};
} // namespace binary_sensor
} // namespace esphome

View File

@ -7,42 +7,25 @@ namespace binary_sensor {
static const char *const TAG = "binary_sensor";
void BinarySensor::add_on_state_callback(std::function<void(bool)> &&callback) {
this->state_callback_.add(std::move(callback));
}
void BinarySensor::publish_state(bool state) {
if (!this->publish_dedup_.next(state))
return;
void BinarySensor::publish_state(bool new_state) {
if (this->filter_list_ == nullptr) {
this->send_state_internal(state, false);
this->send_state_internal(new_state);
} else {
this->filter_list_->input(state, false);
this->filter_list_->input(new_state);
}
}
void BinarySensor::publish_initial_state(bool state) {
if (!this->publish_dedup_.next(state))
return;
if (this->filter_list_ == nullptr) {
this->send_state_internal(state, true);
} else {
this->filter_list_->input(state, true);
void BinarySensor::publish_initial_state(bool new_state) {
this->invalidate_state();
this->publish_state(new_state);
}
void BinarySensor::send_state_internal(bool new_state) {
// copy the new state to the visible property for backwards compatibility, before any callbacks
this->state = new_state;
// Note that set_state_ de-dups and will only trigger callbacks if the state has actually changed
if (this->set_state_(new_state)) {
ESP_LOGD(TAG, "'%s': New state is %s", this->get_name().c_str(), ONOFF(new_state));
}
}
void BinarySensor::send_state_internal(bool state, bool is_initial) {
if (is_initial) {
ESP_LOGD(TAG, "'%s': Sending initial state %s", this->get_name().c_str(), ONOFF(state));
} else {
ESP_LOGD(TAG, "'%s': Sending state %s", this->get_name().c_str(), ONOFF(state));
}
this->has_state_ = true;
this->state = state;
if (!is_initial || this->publish_initial_state_) {
this->state_callback_.call(state);
}
}
BinarySensor::BinarySensor() : state(false) {}
void BinarySensor::add_filter(Filter *filter) {
filter->parent_ = this;
@ -60,7 +43,6 @@ void BinarySensor::add_filters(const std::vector<Filter *> &filters) {
this->add_filter(filter);
}
}
bool BinarySensor::has_state() const { return this->has_state_; }
bool BinarySensor::is_status_binary_sensor() const { return false; }
} // namespace binary_sensor

View File

@ -1,6 +1,5 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/entity_base.h"
#include "esphome/core/helpers.h"
#include "esphome/components/binary_sensor/filter.h"
@ -34,52 +33,39 @@ namespace binary_sensor {
* The sub classes should notify the front-end of new states via the publish_state() method which
* handles inverted inputs for you.
*/
class BinarySensor : public EntityBase, public EntityBase_DeviceClass {
class BinarySensor : public StatefulEntityBase<bool>, public EntityBase_DeviceClass {
public:
explicit BinarySensor();
/** Add a callback to be notified of state changes.
*
* @param callback The void(bool) callback.
*/
void add_on_state_callback(std::function<void(bool)> &&callback);
explicit BinarySensor(){};
/** Publish a new state to the front-end.
*
* @param state The new state.
* @param new_state The new state.
*/
void publish_state(bool state);
void publish_state(bool new_state);
/** Publish the initial state, this will not make the callback manager send callbacks
* and is meant only for the initial state on boot.
*
* @param state The new state.
* @param new_state The new state.
*/
void publish_initial_state(bool state);
/// The current reported state of the binary sensor.
bool state{false};
void publish_initial_state(bool new_state);
void add_filter(Filter *filter);
void add_filters(const std::vector<Filter *> &filters);
void set_publish_initial_state(bool publish_initial_state) { this->publish_initial_state_ = publish_initial_state; }
// ========== INTERNAL METHODS ==========
// (In most use cases you won't need these)
void send_state_internal(bool state, bool is_initial);
void send_state_internal(bool new_state);
/// Return whether this binary sensor has outputted a state.
virtual bool has_state() const;
virtual bool is_status_binary_sensor() const;
// For backward compatibility, provide an accessible property
bool state{};
protected:
CallbackManager<void(bool)> state_callback_{};
Filter *filter_list_{nullptr};
bool has_state_{false};
bool publish_initial_state_{false};
Deduplicator<bool> publish_dedup_;
};
class BinarySensorInitiallyOff : public BinarySensor {

View File

@ -9,37 +9,42 @@ namespace binary_sensor {
static const char *const TAG = "sensor.filter";
void Filter::output(bool value, bool is_initial) {
void Filter::output(bool value) {
if (this->next_ == nullptr) {
this->parent_->send_state_internal(value);
} else {
this->next_->input(value);
}
}
void Filter::input(bool value) {
if (!this->dedup_.next(value))
return;
if (this->next_ == nullptr) {
this->parent_->send_state_internal(value, is_initial);
} else {
this->next_->input(value, is_initial);
}
}
void Filter::input(bool value, bool is_initial) {
auto b = this->new_value(value, is_initial);
auto b = this->new_value(value);
if (b.has_value()) {
this->output(*b, is_initial);
this->output(*b);
}
}
optional<bool> DelayedOnOffFilter::new_value(bool value, bool is_initial) {
void TimeoutFilter::input(bool value) {
this->set_timeout("timeout", this->timeout_delay_.value(), [this]() { this->parent_->invalidate_state(); });
// we do not de-dup here otherwise changes from invalid to valid state will not be output
this->output(value);
}
optional<bool> DelayedOnOffFilter::new_value(bool value) {
if (value) {
this->set_timeout("ON_OFF", this->on_delay_.value(), [this, is_initial]() { this->output(true, is_initial); });
this->set_timeout("ON_OFF", this->on_delay_.value(), [this]() { this->output(true); });
} else {
this->set_timeout("ON_OFF", this->off_delay_.value(), [this, is_initial]() { this->output(false, is_initial); });
this->set_timeout("ON_OFF", this->off_delay_.value(), [this]() { this->output(false); });
}
return {};
}
float DelayedOnOffFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
optional<bool> DelayedOnFilter::new_value(bool value, bool is_initial) {
optional<bool> DelayedOnFilter::new_value(bool value) {
if (value) {
this->set_timeout("ON", this->delay_.value(), [this, is_initial]() { this->output(true, is_initial); });
this->set_timeout("ON", this->delay_.value(), [this]() { this->output(true); });
return {};
} else {
this->cancel_timeout("ON");
@ -49,9 +54,9 @@ optional<bool> DelayedOnFilter::new_value(bool value, bool is_initial) {
float DelayedOnFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
optional<bool> DelayedOffFilter::new_value(bool value, bool is_initial) {
optional<bool> DelayedOffFilter::new_value(bool value) {
if (!value) {
this->set_timeout("OFF", this->delay_.value(), [this, is_initial]() { this->output(false, is_initial); });
this->set_timeout("OFF", this->delay_.value(), [this]() { this->output(false); });
return {};
} else {
this->cancel_timeout("OFF");
@ -61,11 +66,11 @@ optional<bool> DelayedOffFilter::new_value(bool value, bool is_initial) {
float DelayedOffFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
optional<bool> InvertFilter::new_value(bool value, bool is_initial) { return !value; }
optional<bool> InvertFilter::new_value(bool value) { return !value; }
AutorepeatFilter::AutorepeatFilter(std::vector<AutorepeatFilterTiming> timings) : timings_(std::move(timings)) {}
optional<bool> AutorepeatFilter::new_value(bool value, bool is_initial) {
optional<bool> AutorepeatFilter::new_value(bool value) {
if (value) {
// Ignore if already running
if (this->active_timing_ != 0)
@ -101,7 +106,7 @@ void AutorepeatFilter::next_timing_() {
void AutorepeatFilter::next_value_(bool val) {
const AutorepeatFilterTiming &timing = this->timings_[this->active_timing_ - 2];
this->output(val, false); // This is at least the second one so not initial
this->output(val); // This is at least the second one so not initial
this->set_timeout("ON_OFF", val ? timing.time_on : timing.time_off, [this, val]() { this->next_value_(!val); });
}
@ -109,18 +114,18 @@ float AutorepeatFilter::get_setup_priority() const { return setup_priority::HARD
LambdaFilter::LambdaFilter(std::function<optional<bool>(bool)> f) : f_(std::move(f)) {}
optional<bool> LambdaFilter::new_value(bool value, bool is_initial) { return this->f_(value); }
optional<bool> LambdaFilter::new_value(bool value) { return this->f_(value); }
optional<bool> SettleFilter::new_value(bool value, bool is_initial) {
optional<bool> SettleFilter::new_value(bool value) {
if (!this->steady_) {
this->set_timeout("SETTLE", this->delay_.value(), [this, value, is_initial]() {
this->set_timeout("SETTLE", this->delay_.value(), [this, value]() {
this->steady_ = true;
this->output(value, is_initial);
this->output(value);
});
return {};
} else {
this->steady_ = false;
this->output(value, is_initial);
this->output(value);
this->set_timeout("SETTLE", this->delay_.value(), [this]() { this->steady_ = true; });
return value;
}

View File

@ -14,11 +14,11 @@ class BinarySensor;
class Filter {
public:
virtual optional<bool> new_value(bool value, bool is_initial) = 0;
virtual optional<bool> new_value(bool value) = 0;
void input(bool value, bool is_initial);
virtual void input(bool value);
void output(bool value, bool is_initial);
void output(bool value);
protected:
friend BinarySensor;
@ -28,9 +28,19 @@ class Filter {
Deduplicator<bool> dedup_;
};
class TimeoutFilter : public Filter, public Component {
public:
optional<bool> new_value(bool value) override { return value; }
void input(bool value) override;
template<typename T> void set_timeout_value(T timeout) { this->timeout_delay_ = timeout; }
protected:
TemplatableValue<uint32_t> timeout_delay_{};
};
class DelayedOnOffFilter : public Filter, public Component {
public:
optional<bool> new_value(bool value, bool is_initial) override;
optional<bool> new_value(bool value) override;
float get_setup_priority() const override;
@ -44,7 +54,7 @@ class DelayedOnOffFilter : public Filter, public Component {
class DelayedOnFilter : public Filter, public Component {
public:
optional<bool> new_value(bool value, bool is_initial) override;
optional<bool> new_value(bool value) override;
float get_setup_priority() const override;
@ -56,7 +66,7 @@ class DelayedOnFilter : public Filter, public Component {
class DelayedOffFilter : public Filter, public Component {
public:
optional<bool> new_value(bool value, bool is_initial) override;
optional<bool> new_value(bool value) override;
float get_setup_priority() const override;
@ -68,7 +78,7 @@ class DelayedOffFilter : public Filter, public Component {
class InvertFilter : public Filter {
public:
optional<bool> new_value(bool value, bool is_initial) override;
optional<bool> new_value(bool value) override;
};
struct AutorepeatFilterTiming {
@ -86,7 +96,7 @@ class AutorepeatFilter : public Filter, public Component {
public:
explicit AutorepeatFilter(std::vector<AutorepeatFilterTiming> timings);
optional<bool> new_value(bool value, bool is_initial) override;
optional<bool> new_value(bool value) override;
float get_setup_priority() const override;
@ -102,7 +112,7 @@ class LambdaFilter : public Filter {
public:
explicit LambdaFilter(std::function<optional<bool>(bool)> f);
optional<bool> new_value(bool value, bool is_initial) override;
optional<bool> new_value(bool value) override;
protected:
std::function<optional<bool>(bool)> f_;
@ -110,7 +120,7 @@ class LambdaFilter : public Filter {
class SettleFilter : public Filter, public Component {
public:
optional<bool> new_value(bool value, bool is_initial) override;
optional<bool> new_value(bool value) override;
float get_setup_priority() const override;

View File

@ -16,7 +16,6 @@ class BLEBinaryOutput : public output::BinaryOutput, public BLEClientNode, publi
public:
void dump_config() override;
void loop() override {}
float get_setup_priority() const override { return setup_priority::DATA; }
void set_service_uuid16(uint16_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); }
void set_service_uuid32(uint32_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); }
void set_service_uuid128(uint8_t *uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_raw(uuid); }

View File

@ -11,7 +11,11 @@ namespace ble_client {
static const char *const TAG = "ble_rssi_sensor";
void BLEClientRSSISensor::loop() {}
void BLEClientRSSISensor::loop() {
// Parent BLEClientNode has a loop() method, but this component uses
// polling via update() and BLE GAP callbacks so loop isn't needed
this->disable_loop();
}
void BLEClientRSSISensor::dump_config() {
LOG_SENSOR("", "BLE Client RSSI Sensor", this);

View File

@ -18,7 +18,6 @@ class BLEClientRSSISensor : public sensor::Sensor, public PollingComponent, publ
void loop() override;
void update() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override;

View File

@ -11,7 +11,11 @@ namespace ble_client {
static const char *const TAG = "ble_sensor";
void BLESensor::loop() {}
void BLESensor::loop() {
// Parent BLEClientNode has a loop() method, but this component uses
// polling via update() and BLE callbacks so loop isn't needed
this->disable_loop();
}
void BLESensor::dump_config() {
LOG_SENSOR("", "BLE Sensor", this);

View File

@ -24,7 +24,6 @@ class BLESensor : public sensor::Sensor, public PollingComponent, public BLEClie
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
void set_service_uuid16(uint16_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); }
void set_service_uuid32(uint32_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); }
void set_service_uuid128(uint8_t *uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_raw(uuid); }

View File

@ -19,7 +19,6 @@ class BLEClientSwitch : public switch_::Switch, public Component, public BLEClie
void loop() override {}
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) override;
float get_setup_priority() const override { return setup_priority::DATA; }
protected:
void write_state(bool state) override;

View File

@ -14,7 +14,11 @@ static const char *const TAG = "ble_text_sensor";
static const std::string EMPTY = "";
void BLETextSensor::loop() {}
void BLETextSensor::loop() {
// Parent BLEClientNode has a loop() method, but this component uses
// polling via update() and BLE callbacks so loop isn't needed
this->disable_loop();
}
void BLETextSensor::dump_config() {
LOG_TEXT_SENSOR("", "BLE Text Sensor", this);

View File

@ -20,7 +20,6 @@ class BLETextSensor : public text_sensor::TextSensor, public PollingComponent, p
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
void set_service_uuid16(uint16_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); }
void set_service_uuid32(uint32_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); }
void set_service_uuid128(uint8_t *uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_raw(uuid); }

View File

@ -105,7 +105,6 @@ class BLEPresenceDevice : public binary_sensor::BinarySensorInitiallyOff,
this->set_found_(false);
}
void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
protected:
void set_found_(bool state) {

View File

@ -99,7 +99,6 @@ class BLERSSISensor : public sensor::Sensor, public esp32_ble_tracker::ESPBTDevi
return false;
}
void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
protected:
enum MatchType { MATCH_BY_MAC_ADDRESS, MATCH_BY_IRK, MATCH_BY_SERVICE_UUID, MATCH_BY_IBEACON_UUID };

View File

@ -29,7 +29,6 @@ class BLEScanner : public text_sensor::TextSensor, public esp32_ble_tracker::ESP
return true;
}
void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
};
} // namespace ble_scanner

View File

@ -26,10 +26,17 @@ class BluetoothConnection : public esp32_ble_client::BLEClientBase {
protected:
friend class BluetoothProxy;
bool seen_mtu_or_services_{false};
int16_t send_service_{-2};
// Memory optimized layout for 32-bit systems
// Group 1: Pointers (4 bytes each, naturally aligned)
BluetoothProxy *proxy_;
// Group 2: 2-byte types
int16_t send_service_{-2}; // Needs to handle negative values and service count
// Group 3: 1-byte types
bool seen_mtu_or_services_{false};
// 1 byte used, 1 byte padding
};
} // namespace bluetooth_proxy

View File

@ -52,11 +52,21 @@ bool BluetoothProxy::parse_device(const esp32_ble_tracker::ESPBTDevice &device)
return true;
}
static constexpr size_t FLUSH_BATCH_SIZE = 8;
static std::vector<api::BluetoothLERawAdvertisement> &get_batch_buffer() {
static std::vector<api::BluetoothLERawAdvertisement> batch_buffer;
return batch_buffer;
}
// Batch size for BLE advertisements to maximize WiFi efficiency
// Each advertisement is up to 80 bytes when packaged (including protocol overhead)
// Most advertisements are 20-30 bytes, allowing even more to fit per packet
// 16 advertisements × 80 bytes (worst case) = 1280 bytes out of ~1320 bytes usable payload
// This achieves ~97% WiFi MTU utilization while staying under the limit
static constexpr size_t FLUSH_BATCH_SIZE = 16;
namespace {
// Batch buffer in anonymous namespace to avoid guard variable (saves 8 bytes)
// This is initialized at program startup before any threads
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
std::vector<api::BluetoothLERawAdvertisement> batch_buffer;
} // namespace
static std::vector<api::BluetoothLERawAdvertisement> &get_batch_buffer() { return batch_buffer; }
bool BluetoothProxy::parse_devices(const esp32_ble::BLEScanResult *scan_results, size_t count) {
if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr || !this->raw_advertisements_)
@ -170,7 +180,7 @@ int BluetoothProxy::get_bluetooth_connections_free() {
void BluetoothProxy::loop() {
if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr) {
for (auto *connection : this->connections_) {
if (connection->get_address() != 0) {
if (connection->get_address() != 0 && !connection->disconnect_pending()) {
connection->disconnect();
}
}

View File

@ -134,11 +134,17 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com
BluetoothConnection *get_connection_(uint64_t address, bool reserve);
bool active_;
std::vector<BluetoothConnection *> connections_{};
// Memory optimized layout for 32-bit systems
// Group 1: Pointers (4 bytes each, naturally aligned)
api::APIConnection *api_connection_{nullptr};
// Group 2: Container types (typically 12 bytes on 32-bit)
std::vector<BluetoothConnection *> connections_{};
// Group 3: 1-byte types grouped together
bool active_;
bool raw_advertisements_{false};
// 2 bytes used, 2 bytes padding
};
extern BluetoothProxy *global_bluetooth_proxy; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)

View File

@ -12,8 +12,8 @@ from esphome.const import (
CONF_OVERSAMPLING,
CONF_PRESSURE,
CONF_TEMPERATURE,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_ATMOSPHERIC_PRESSURE,
DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_TEMPERATURE,
ICON_GAS_CYLINDER,
STATE_CLASS_MEASUREMENT,

View File

@ -61,8 +61,6 @@ enum IIRFilter {
class BMP581Component : public PollingComponent, public i2c::I2CDevice {
public:
float get_setup_priority() const override { return setup_priority::DATA; }
void dump_config() override;
void setup() override;

View File

@ -18,8 +18,8 @@ from esphome.const import (
DEVICE_CLASS_UPDATE,
)
from esphome.core import CORE, coroutine_with_priority
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
from esphome.cpp_generator import MockObjClass
from esphome.cpp_helpers import setup_entity
CODEOWNERS = ["@esphome/core"]
IS_PLATFORM_COMPONENT = True
@ -61,6 +61,9 @@ _BUTTON_SCHEMA = (
)
_BUTTON_SCHEMA.add_extra(entity_duplicate_validator("button"))
def button_schema(
class_: MockObjClass,
*,
@ -87,7 +90,7 @@ BUTTON_SCHEMA.add_extra(cv.deprecated_schema_constant("button"))
async def setup_button_core_(var, config):
await setup_entity(var, config)
await setup_entity(var, config, "button")
for conf in config.get(CONF_ON_PRESS, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)

View File

@ -0,0 +1 @@
CODEOWNERS = ["@DT-art1", "@bdraco"]

View File

@ -0,0 +1,22 @@
#include "camera.h"
namespace esphome {
namespace camera {
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
Camera *Camera::global_camera = nullptr;
Camera::Camera() {
if (global_camera != nullptr) {
this->status_set_error("Multiple cameras are configured, but only one is supported.");
this->mark_failed();
return;
}
global_camera = this;
}
Camera *Camera::instance() { return global_camera; }
} // namespace camera
} // namespace esphome

View File

@ -0,0 +1,80 @@
#pragma once
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/entity_base.h"
#include "esphome/core/helpers.h"
namespace esphome {
namespace camera {
/** Different sources for filtering.
* IDLE: Camera requests to send an image to the API.
* API_REQUESTER: API requests a new image.
* WEB_REQUESTER: ESP32 web server request an image. Ignored by API.
*/
enum CameraRequester : uint8_t { IDLE, API_REQUESTER, WEB_REQUESTER };
/** Abstract camera image base class.
* Encapsulates the JPEG encoded data and it is shared among
* all connected clients.
*/
class CameraImage {
public:
virtual uint8_t *get_data_buffer() = 0;
virtual size_t get_data_length() = 0;
virtual bool was_requested_by(CameraRequester requester) const = 0;
virtual ~CameraImage() {}
};
/** Abstract image reader base class.
* Keeps track of the data offset of the camera image and
* how many bytes are remaining to read. When the image
* is returned, the shared_ptr is reset and the camera can
* reuse the memory of the camera image.
*/
class CameraImageReader {
public:
virtual void set_image(std::shared_ptr<CameraImage> image) = 0;
virtual size_t available() const = 0;
virtual uint8_t *peek_data_buffer() = 0;
virtual void consume_data(size_t consumed) = 0;
virtual void return_image() = 0;
virtual ~CameraImageReader() {}
};
/** Abstract camera base class. Collaborates with API.
* 1) API server starts and installs callback (add_image_callback)
* which is called by the camera when a new image is available.
* 2) New API client connects and creates a new image reader (create_image_reader).
* 3) API connection receives protobuf CameraImageRequest and calls request_image.
* 3.a) API connection receives protobuf CameraImageRequest and calls start_stream.
* 4) Camera implementation provides JPEG data in the CameraImage and calls callback.
* 5) API connection sets the image in the image reader.
* 6) API connection consumes data from the image reader and returns the image when finished.
* 7.a) Camera captures a new image and continues with 4) until start_stream is called.
*/
class Camera : public EntityBase, public Component {
public:
Camera();
// Camera implementation invokes callback to publish a new image.
virtual void add_image_callback(std::function<void(std::shared_ptr<CameraImage>)> &&callback) = 0;
/// Returns a new camera image reader that keeps track of the JPEG data in the camera image.
virtual CameraImageReader *create_image_reader() = 0;
// Connection, camera or web server requests one new JPEG image.
virtual void request_image(CameraRequester requester) = 0;
// Connection, camera or web server requests a stream of images.
virtual void start_stream(CameraRequester requester) = 0;
// Connection or web server stops the previously started stream.
virtual void stop_stream(CameraRequester requester) = 0;
virtual ~Camera() {}
/// The singleton instance of the camera implementation.
static Camera *instance();
protected:
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
static Camera *global_camera;
};
} // namespace camera
} // namespace esphome

View File

@ -1,4 +1,5 @@
import re
from esphome import automation
import esphome.codegen as cg
import esphome.config_validation as cv

View File

@ -46,7 +46,6 @@ class CAP1188Component : public Component, public i2c::I2CDevice {
void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; }
void setup() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
void loop() override;
protected:

View File

@ -7,11 +7,12 @@ from esphome.const import (
PLATFORM_BK72XX,
PLATFORM_ESP32,
PLATFORM_ESP8266,
PLATFORM_LN882X,
PLATFORM_RTL87XX,
)
from esphome.core import CORE, coroutine_with_priority
AUTO_LOAD = ["web_server_base"]
AUTO_LOAD = ["web_server_base", "ota.web_server"]
DEPENDENCIES = ["wifi"]
CODEOWNERS = ["@OttoWinter"]
@ -27,7 +28,15 @@ CONFIG_SCHEMA = cv.All(
),
}
).extend(cv.COMPONENT_SCHEMA),
cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_BK72XX, PLATFORM_RTL87XX]),
cv.only_on(
[
PLATFORM_ESP32,
PLATFORM_ESP8266,
PLATFORM_BK72XX,
PLATFORM_LN882X,
PLATFORM_RTL87XX,
]
),
)
@ -41,6 +50,7 @@ async def to_code(config):
if CORE.using_arduino:
if CORE.is_esp32:
cg.add_library("ESP32 Async UDP", None)
cg.add_library("DNSServer", None)
cg.add_library("WiFi", None)
if CORE.is_esp8266:

View File

@ -37,12 +37,16 @@ void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) {
request->redirect("/?save");
}
void CaptivePortal::setup() {}
void CaptivePortal::setup() {
#ifndef USE_ARDUINO
// No DNS server needed for non-Arduino frameworks
this->disable_loop();
#endif
}
void CaptivePortal::start() {
this->base_->init();
if (!this->initialized_) {
this->base_->add_handler(this);
this->base_->add_ota_handler();
}
#ifdef USE_ARDUINO
@ -50,6 +54,8 @@ void CaptivePortal::start() {
this->dns_server_->setErrorReplyCode(DNSReplyCode::NoError);
network::IPAddress ip = wifi::global_wifi_component->wifi_soft_ap_ip();
this->dns_server_->start(53, "*", ip);
// Re-enable loop() when DNS server is started
this->enable_loop();
#endif
this->base_->get_server()->onNotFound([this](AsyncWebServerRequest *req) {
@ -68,7 +74,11 @@ void CaptivePortal::start() {
void CaptivePortal::handleRequest(AsyncWebServerRequest *req) {
if (req->url() == "/") {
#ifndef USE_ESP8266
auto *response = req->beginResponse(200, "text/html", INDEX_GZ, sizeof(INDEX_GZ));
#else
auto *response = req->beginResponse_P(200, "text/html", INDEX_GZ, sizeof(INDEX_GZ));
#endif
response->addHeader("Content-Encoding", "gzip");
req->send(response);
return;

View File

@ -21,8 +21,11 @@ class CaptivePortal : public AsyncWebHandler, public Component {
void dump_config() override;
#ifdef USE_ARDUINO
void loop() override {
if (this->dns_server_ != nullptr)
if (this->dns_server_ != nullptr) {
this->dns_server_->processNextRequest();
} else {
this->disable_loop();
}
}
#endif
float get_setup_priority() const override;
@ -37,7 +40,7 @@ class CaptivePortal : public AsyncWebHandler, public Component {
#endif
}
bool canHandle(AsyncWebServerRequest *request) override {
bool canHandle(AsyncWebServerRequest *request) const override {
if (!this->active_)
return false;

View File

@ -25,8 +25,6 @@ class CCS811Component : public PollingComponent, public i2c::I2CDevice {
void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
protected:
optional<uint8_t> read_status_() { return this->read_byte(0x00); }
bool status_has_error_() { return this->read_status_().value_or(1) & 1; }

View File

@ -48,8 +48,8 @@ from esphome.const import (
CONF_WEB_SERVER,
)
from esphome.core import CORE, coroutine_with_priority
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
from esphome.cpp_generator import MockObjClass
from esphome.cpp_helpers import setup_entity
IS_PLATFORM_COMPONENT = True
@ -247,6 +247,9 @@ _CLIMATE_SCHEMA = (
)
_CLIMATE_SCHEMA.add_extra(entity_duplicate_validator("climate"))
def climate_schema(
class_: MockObjClass,
*,
@ -273,7 +276,7 @@ CLIMATE_SCHEMA.add_extra(cv.deprecated_schema_constant("climate"))
async def setup_climate_core_(var, config):
await setup_entity(var, config)
await setup_entity(var, config, "climate")
visual = config[CONF_VISUAL]
if (min_temp := visual.get(CONF_MIN_TEMPERATURE)) is not None:

View File

@ -1,10 +1,10 @@
"""CM1106 Sensor component for ESPHome."""
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import automation
from esphome.automation import maybe_simple_id
import esphome.codegen as cg
from esphome.components import sensor, uart
import esphome.config_validation as cv
from esphome.const import (
CONF_CO2,
CONF_ID,

View File

@ -2,5 +2,7 @@
CODEOWNERS = ["@esphome/core"]
CONF_BYTE_ORDER = "byte_order"
CONF_DRAW_ROUNDING = "draw_rounding"
CONF_ON_STATE_CHANGE = "on_state_change"
CONF_REQUEST_HEADERS = "request_headers"

View File

@ -11,7 +11,6 @@ class CopyBinarySensor : public binary_sensor::BinarySensor, public Component {
void set_source(binary_sensor::BinarySensor *source) { source_ = source; }
void setup() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
protected:
binary_sensor::BinarySensor *source_;

View File

@ -10,7 +10,6 @@ class CopyButton : public button::Button, public Component {
public:
void set_source(button::Button *source) { source_ = source; }
void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
protected:
void press_action() override;

View File

@ -11,7 +11,6 @@ class CopyCover : public cover::Cover, public Component {
void set_source(cover::Cover *source) { source_ = source; }
void setup() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
cover::CoverTraits get_traits() override;

View File

@ -11,7 +11,6 @@ class CopyFan : public fan::Fan, public Component {
void set_source(fan::Fan *source) { source_ = source; }
void setup() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
fan::FanTraits get_traits() override;

View File

@ -11,7 +11,6 @@ class CopyLock : public lock::Lock, public Component {
void set_source(lock::Lock *source) { source_ = source; }
void setup() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
protected:
void control(const lock::LockCall &call) override;

View File

@ -11,7 +11,6 @@ class CopyNumber : public number::Number, public Component {
void set_source(number::Number *source) { source_ = source; }
void setup() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
protected:
void control(float value) override;

View File

@ -11,7 +11,6 @@ class CopySelect : public select::Select, public Component {
void set_source(select::Select *source) { source_ = source; }
void setup() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
protected:
void control(const std::string &value) override;

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