mirror of
				https://github.com/esphome/esphome.git
				synced 2025-11-03 16:41:50 +00:00 
			
		
		
		
	Compare commits
	
		
			142 Commits
		
	
	
		
			2025.6.0b3
			...
			jesserockz
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					a6e7e48b73 | ||
| 
						 | 
					f80610d958 | ||
| 
						 | 
					1aacf13888 | ||
| 
						 | 
					23b1e428de | ||
| 
						 | 
					f029f4f20e | ||
| 
						 | 
					79e3d2b2d7 | ||
| 
						 | 
					c74e5e0f04 | ||
| 
						 | 
					15ef93ccc9 | ||
| 
						 | 
					e017250445 | ||
| 
						 | 
					17497eec43 | ||
| 
						 | 
					6d0c6329ad | ||
| 
						 | 
					f35be6b5cc | ||
| 
						 | 
					b18ff48b4a | ||
| 
						 | 
					7c28134214 | ||
| 
						 | 
					16860e8a30 | ||
| 
						 | 
					5362d1a89f | ||
| 
						 | 
					5531296ee0 | ||
| 
						 | 
					47db5e26f3 | ||
| 
						 | 
					cf5197b68a | ||
| 
						 | 
					9f831e91b3 | ||
| 
						 | 
					2df0ebd895 | ||
| 
						 | 
					7ad6dab383 | ||
| 
						 | 
					612c8d5841 | ||
| 
						 | 
					a35e476be5 | ||
| 
						 | 
					87a7157fc4 | ||
| 
						 | 
					fa34adbf6c | ||
| 
						 | 
					ac942e0670 | ||
| 
						 | 
					22e360d479 | ||
| 
						 | 
					649936200e | ||
| 
						 | 
					5d6e690c12 | ||
| 
						 | 
					2f2ecadae7 | ||
| 
						 | 
					6dfb9eba61 | ||
| 
						 | 
					24587fe875 | ||
| 
						 | 
					a1aebe6a2c | ||
| 
						 | 
					2ad266582f | ||
| 
						 | 
					1a47164876 | ||
| 
						 | 
					cd22723623 | ||
| 
						 | 
					aecaffa2f5 | ||
| 
						 | 
					87df3596a2 | ||
| 
						 | 
					41c7852128 | ||
| 
						 | 
					78ec9856fb | ||
| 
						 | 
					2a45467bf6 | ||
| 
						 | 
					7fc5bfd787 | ||
| 
						 | 
					04f592ba6d | ||
| 
						 | 
					59889a6286 | ||
| 
						 | 
					dc5cbd4df8 | ||
| 
						 | 
					7ab9083d77 | ||
| 
						 | 
					788803d588 | ||
| 
						 | 
					cbfd904b9f | ||
| 
						 | 
					c81dbf9d59 | ||
| 
						 | 
					ac9c608542 | ||
| 
						 | 
					a6c20853ca | ||
| 
						 | 
					4ef0264ed3 | ||
| 
						 | 
					169db9cc0a | ||
| 
						 | 
					b693b8ccb1 | ||
| 
						 | 
					3e98cceb00 | ||
| 
						 | 
					46d962dcf1 | ||
| 
						 | 
					7dbad42470 | ||
| 
						 | 
					eb97781f68 | ||
| 
						 | 
					4d0f8528d2 | ||
| 
						 | 
					2c17b2bacc | ||
| 
						 | 
					30bea20f7a | ||
| 
						 | 
					d4cb4ef994 | ||
| 
						 | 
					9c90ca297a | ||
| 
						 | 
					a9e1a4cef3 | ||
| 
						 | 
					0ce3621ac0 | ||
| 
						 | 
					d527398dae | ||
| 
						 | 
					2e9ac8945d | ||
| 
						 | 
					40a5638005 | ||
| 
						 | 
					8ba22183b9 | ||
| 
						 | 
					2e11e66db4 | ||
| 
						 | 
					eeb0710ad4 | ||
| 
						 | 
					43c677ef37 | ||
| 
						 | 
					95544e489d | ||
| 
						 | 
					a08d021f77 | ||
| 
						 | 
					b7b1d17ecb | ||
| 
						 | 
					aa180b9581 | ||
| 
						 | 
					57388254c4 | ||
| 
						 | 
					f16f4e2c4c | ||
| 
						 | 
					89b70e4352 | ||
| 
						 | 
					6667336bd8 | ||
| 
						 | 
					669ef7a0b1 | ||
| 
						 | 
					c612985930 | ||
| 
						 | 
					2e534ce41e | ||
| 
						 | 
					fedb54bb38 | ||
| 
						 | 
					68f5144084 | ||
| 
						 | 
					fd3c22945b | ||
| 
						 | 
					53496a1ecd | ||
| 
						 | 
					da5cf99549 | ||
| 
						 | 
					849c858495 | ||
| 
						 | 
					808f964841 | ||
| 
						 | 
					3bc5db4fd7 | ||
| 
						 | 
					0bf613bd34 | ||
| 
						 | 
					43ab63455b | ||
| 
						 | 
					47e7988c8e | ||
| 
						 | 
					7ed095e635 | ||
| 
						 | 
					cb8b0ec62e | ||
| 
						 | 
					bf161f1eaa | ||
| 
						 | 
					78c8447d1e | ||
| 
						 | 
					5ffe50381a | ||
| 
						 | 
					b08bd0c24a | ||
| 
						 | 
					738ad8e9d3 | ||
| 
						 | 
					fa7c42511a | ||
| 
						 | 
					68ef9cb3dc | ||
| 
						 | 
					8e176b9c61 | ||
| 
						 | 
					c4f7c2d259 | ||
| 
						 | 
					882bfc79c7 | ||
| 
						 | 
					c17a3b6fcc | ||
| 
						 | 
					28d11553e0 | ||
| 
						 | 
					1dbebe90ba | ||
| 
						 | 
					06810e8e6a | ||
| 
						 | 
					bd85ba9b6a | ||
| 
						 | 
					be58cdda3b | ||
| 
						 | 
					fcce4a8be6 | ||
| 
						 | 
					61a558a062 | ||
| 
						 | 
					59f69ac5ca | ||
| 
						 | 
					f82ac34784 | ||
| 
						 | 
					07cf6e723b | ||
| 
						 | 
					78e3c6333f | ||
| 
						 | 
					98e2684107 | ||
| 
						 | 
					cb019fff9a | ||
| 
						 | 
					4305c44440 | ||
| 
						 | 
					a1e4143600 | ||
| 
						 | 
					374c33e8dc | ||
| 
						 | 
					dcfe7af9d3 | ||
| 
						 | 
					049c7e00ca | ||
| 
						 | 
					ee37d2f9c8 | ||
| 
						 | 
					92ea697119 | ||
| 
						 | 
					1c488d375f | ||
| 
						 | 
					1a03b4949f | ||
| 
						 | 
					731b7808cd | ||
| 
						 | 
					d9da4cf24d | ||
| 
						 | 
					666a3ee5e9 | ||
| 
						 | 
					02469c2d4c | ||
| 
						 | 
					2a629cae93 | ||
| 
						 | 
					1f14c316a3 | ||
| 
						 | 
					dac738a916 | ||
| 
						 | 
					261b561bb2 | ||
| 
						 | 
					0228379a2e | ||
| 
						 | 
					da79215bc3 | ||
| 
						 | 
					a59e1c7011 | ||
| 
						 | 
					f467c79a20 | 
							
								
								
									
										2
									
								
								.github/workflows/ci-docker.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/ci-docker.yml
									
									
									
									
										vendored
									
									
								
							@@ -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: |
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										23
									
								
								.github/workflows/lock.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										23
									
								
								.github/workflows/lock.yml
									
									
									
									
										vendored
									
									
								
							@@ -1,28 +1,13 @@
 | 
			
		||||
---
 | 
			
		||||
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
 | 
			
		||||
    uses: esphome/workflows/.github/workflows/lock.yml@main
 | 
			
		||||
    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
 | 
			
		||||
      since-days: 3650
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							@@ -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'
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@
 | 
			
		||||
repos:
 | 
			
		||||
  - repo: https://github.com/astral-sh/ruff-pre-commit
 | 
			
		||||
    # Ruff version.
 | 
			
		||||
    rev: v0.11.10
 | 
			
		||||
    rev: v0.12.0
 | 
			
		||||
    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:
 | 
			
		||||
 
 | 
			
		||||
@@ -323,6 +323,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
 | 
			
		||||
@@ -520,6 +521,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
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								Doxyfile
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								Doxyfile
									
									
									
									
									
								
							@@ -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.0b3
 | 
			
		||||
PROJECT_NUMBER         = 2025.7.0-dev
 | 
			
		||||
 | 
			
		||||
# 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
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
 
 | 
			
		||||
@@ -193,14 +193,13 @@ 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) {
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
@@ -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()) {
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
@@ -315,6 +333,7 @@ message ListEntitiesCoverResponse {
 | 
			
		||||
  string icon = 10;
 | 
			
		||||
  EntityCategory entity_category = 11;
 | 
			
		||||
  bool supports_stop = 12;
 | 
			
		||||
  uint32 device_id = 13;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
enum LegacyCoverState {
 | 
			
		||||
@@ -388,6 +407,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;
 | 
			
		||||
@@ -471,6 +491,7 @@ message ListEntitiesLightResponse {
 | 
			
		||||
  bool disabled_by_default = 13;
 | 
			
		||||
  string icon = 14;
 | 
			
		||||
  EntityCategory entity_category = 15;
 | 
			
		||||
  uint32 device_id = 16;
 | 
			
		||||
}
 | 
			
		||||
message LightStateResponse {
 | 
			
		||||
  option (id) = 24;
 | 
			
		||||
@@ -563,6 +584,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;
 | 
			
		||||
@@ -595,6 +617,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;
 | 
			
		||||
@@ -632,6 +655,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;
 | 
			
		||||
@@ -814,6 +838,7 @@ message ListEntitiesCameraResponse {
 | 
			
		||||
  bool disabled_by_default = 5;
 | 
			
		||||
  string icon = 6;
 | 
			
		||||
  EntityCategory entity_category = 7;
 | 
			
		||||
  uint32 device_id = 8;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
message CameraImageResponse {
 | 
			
		||||
@@ -916,6 +941,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;
 | 
			
		||||
@@ -999,6 +1025,7 @@ message ListEntitiesNumberResponse {
 | 
			
		||||
  string unit_of_measurement = 11;
 | 
			
		||||
  NumberMode mode = 12;
 | 
			
		||||
  string device_class = 13;
 | 
			
		||||
  uint32 device_id = 14;
 | 
			
		||||
}
 | 
			
		||||
message NumberStateResponse {
 | 
			
		||||
  option (id) = 50;
 | 
			
		||||
@@ -1039,6 +1066,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;
 | 
			
		||||
@@ -1081,6 +1109,7 @@ message ListEntitiesSirenResponse {
 | 
			
		||||
  bool supports_duration = 8;
 | 
			
		||||
  bool supports_volume = 9;
 | 
			
		||||
  EntityCategory entity_category = 10;
 | 
			
		||||
  uint32 device_id = 11;
 | 
			
		||||
}
 | 
			
		||||
message SirenStateResponse {
 | 
			
		||||
  option (id) = 56;
 | 
			
		||||
@@ -1144,6 +1173,7 @@ message ListEntitiesLockResponse {
 | 
			
		||||
 | 
			
		||||
  // Not yet implemented:
 | 
			
		||||
  string code_format = 11;
 | 
			
		||||
  uint32 device_id = 12;
 | 
			
		||||
}
 | 
			
		||||
message LockStateResponse {
 | 
			
		||||
  option (id) = 59;
 | 
			
		||||
@@ -1183,6 +1213,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 +1269,8 @@ message ListEntitiesMediaPlayerResponse {
 | 
			
		||||
  bool supports_pause = 8;
 | 
			
		||||
 | 
			
		||||
  repeated MediaPlayerSupportedFormat supported_formats = 9;
 | 
			
		||||
 | 
			
		||||
  uint32 device_id = 10;
 | 
			
		||||
}
 | 
			
		||||
message MediaPlayerStateResponse {
 | 
			
		||||
  option (id) = 64;
 | 
			
		||||
@@ -1643,6 +1676,7 @@ enum VoiceAssistantEvent {
 | 
			
		||||
  VOICE_ASSISTANT_STT_VAD_END = 12;
 | 
			
		||||
  VOICE_ASSISTANT_TTS_STREAM_START = 98;
 | 
			
		||||
  VOICE_ASSISTANT_TTS_STREAM_END = 99;
 | 
			
		||||
  VOICE_ASSISTANT_INTENT_PROGRESS = 100;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
message VoiceAssistantEventData {
 | 
			
		||||
@@ -1777,6 +1811,7 @@ message ListEntitiesAlarmControlPanelResponse {
 | 
			
		||||
  uint32 supported_features = 8;
 | 
			
		||||
  bool requires_code = 9;
 | 
			
		||||
  bool requires_code_to_arm = 10;
 | 
			
		||||
  uint32 device_id = 11;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
message AlarmControlPanelStateResponse {
 | 
			
		||||
@@ -1822,6 +1857,7 @@ message ListEntitiesTextResponse {
 | 
			
		||||
  uint32 max_length = 9;
 | 
			
		||||
  string pattern = 10;
 | 
			
		||||
  TextMode mode = 11;
 | 
			
		||||
  uint32 device_id = 12;
 | 
			
		||||
}
 | 
			
		||||
message TextStateResponse {
 | 
			
		||||
  option (id) = 98;
 | 
			
		||||
@@ -1862,6 +1898,7 @@ message ListEntitiesDateResponse {
 | 
			
		||||
  string icon = 5;
 | 
			
		||||
  bool disabled_by_default = 6;
 | 
			
		||||
  EntityCategory entity_category = 7;
 | 
			
		||||
  uint32 device_id = 8;
 | 
			
		||||
}
 | 
			
		||||
message DateStateResponse {
 | 
			
		||||
  option (id) = 101;
 | 
			
		||||
@@ -1905,6 +1942,7 @@ message ListEntitiesTimeResponse {
 | 
			
		||||
  string icon = 5;
 | 
			
		||||
  bool disabled_by_default = 6;
 | 
			
		||||
  EntityCategory entity_category = 7;
 | 
			
		||||
  uint32 device_id = 8;
 | 
			
		||||
}
 | 
			
		||||
message TimeStateResponse {
 | 
			
		||||
  option (id) = 104;
 | 
			
		||||
@@ -1951,6 +1989,7 @@ message ListEntitiesEventResponse {
 | 
			
		||||
  string device_class = 8;
 | 
			
		||||
 | 
			
		||||
  repeated string event_types = 9;
 | 
			
		||||
  uint32 device_id = 10;
 | 
			
		||||
}
 | 
			
		||||
message EventResponse {
 | 
			
		||||
  option (id) = 108;
 | 
			
		||||
@@ -1982,6 +2021,7 @@ message ListEntitiesValveResponse {
 | 
			
		||||
  bool assumed_state = 9;
 | 
			
		||||
  bool supports_position = 10;
 | 
			
		||||
  bool supports_stop = 11;
 | 
			
		||||
  uint32 device_id = 12;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
enum ValveOperation {
 | 
			
		||||
@@ -2028,6 +2068,7 @@ message ListEntitiesDateTimeResponse {
 | 
			
		||||
  string icon = 5;
 | 
			
		||||
  bool disabled_by_default = 6;
 | 
			
		||||
  EntityCategory entity_category = 7;
 | 
			
		||||
  uint32 device_id = 8;
 | 
			
		||||
}
 | 
			
		||||
message DateTimeStateResponse {
 | 
			
		||||
  option (id) = 113;
 | 
			
		||||
@@ -2068,6 +2109,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;
 | 
			
		||||
 
 | 
			
		||||
@@ -28,8 +28,19 @@
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace api {
 | 
			
		||||
 | 
			
		||||
// Read a maximum of 5 messages per loop iteration to prevent starving other components.
 | 
			
		||||
// This is a balance between API responsiveness and allowing other components to run.
 | 
			
		||||
// Since each message could contain multiple protobuf messages when using packet batching,
 | 
			
		||||
// this limits the number of messages processed, not the number of TCP packets.
 | 
			
		||||
static constexpr uint8_t MAX_MESSAGES_PER_LOOP = 5;
 | 
			
		||||
static constexpr uint8_t MAX_PING_RETRIES = 60;
 | 
			
		||||
static constexpr uint16_t PING_RETRY_INTERVAL = 1000;
 | 
			
		||||
static constexpr uint32_t KEEPALIVE_DISCONNECT_TIMEOUT = (KEEPALIVE_TIMEOUT_MS * 5) / 2;
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "api.connection";
 | 
			
		||||
#ifdef USE_ESP32_CAMERA
 | 
			
		||||
static const int ESP32_CAMERA_STOP_STREAM = 5000;
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *parent)
 | 
			
		||||
    : parent_(parent), initial_state_iterator_(this), list_entities_iterator_(this) {
 | 
			
		||||
@@ -61,8 +72,8 @@ void APIConnection::start() {
 | 
			
		||||
  APIError err = this->helper_->init();
 | 
			
		||||
  if (err != APIError::OK) {
 | 
			
		||||
    on_fatal_error();
 | 
			
		||||
    ESP_LOGW(TAG, "%s: Helper init failed: %s errno=%d", this->client_combined_info_.c_str(), api_error_to_str(err),
 | 
			
		||||
             errno);
 | 
			
		||||
    ESP_LOGW(TAG, "%s: Helper init failed: %s errno=%d", this->get_client_combined_info().c_str(),
 | 
			
		||||
             api_error_to_str(err), errno);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  this->client_info_ = helper_->getpeername();
 | 
			
		||||
@@ -84,16 +95,6 @@ APIConnection::~APIConnection() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void APIConnection::loop() {
 | 
			
		||||
  if (this->remove_)
 | 
			
		||||
    return;
 | 
			
		||||
 | 
			
		||||
  if (!network::is_connected()) {
 | 
			
		||||
    // when network is disconnected force disconnect immediately
 | 
			
		||||
    // don't wait for timeout
 | 
			
		||||
    this->on_fatal_error();
 | 
			
		||||
    ESP_LOGW(TAG, "%s: Network unavailable; disconnecting", this->client_combined_info_.c_str());
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->next_close_) {
 | 
			
		||||
    // requested a disconnect
 | 
			
		||||
    this->helper_->close();
 | 
			
		||||
@@ -104,30 +105,34 @@ void APIConnection::loop() {
 | 
			
		||||
  APIError err = this->helper_->loop();
 | 
			
		||||
  if (err != APIError::OK) {
 | 
			
		||||
    on_fatal_error();
 | 
			
		||||
    ESP_LOGW(TAG, "%s: Socket operation failed: %s errno=%d", this->client_combined_info_.c_str(),
 | 
			
		||||
    ESP_LOGW(TAG, "%s: Socket operation failed: %s errno=%d", this->get_client_combined_info().c_str(),
 | 
			
		||||
             api_error_to_str(err), errno);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const uint32_t now = App.get_loop_component_start_time();
 | 
			
		||||
  // Check if socket has data ready before attempting to read
 | 
			
		||||
  if (this->helper_->is_socket_ready()) {
 | 
			
		||||
    // Read up to MAX_MESSAGES_PER_LOOP messages per loop to improve throughput
 | 
			
		||||
    for (uint8_t message_count = 0; message_count < MAX_MESSAGES_PER_LOOP; message_count++) {
 | 
			
		||||
      ReadPacketBuffer buffer;
 | 
			
		||||
      err = this->helper_->read_packet(&buffer);
 | 
			
		||||
      if (err == APIError::WOULD_BLOCK) {
 | 
			
		||||
      // pass
 | 
			
		||||
        // No more data available
 | 
			
		||||
        break;
 | 
			
		||||
      } else if (err != APIError::OK) {
 | 
			
		||||
        on_fatal_error();
 | 
			
		||||
        if (err == APIError::SOCKET_READ_FAILED && errno == ECONNRESET) {
 | 
			
		||||
        ESP_LOGW(TAG, "%s: Connection reset", this->client_combined_info_.c_str());
 | 
			
		||||
          ESP_LOGW(TAG, "%s: Connection reset", this->get_client_combined_info().c_str());
 | 
			
		||||
        } else if (err == APIError::CONNECTION_CLOSED) {
 | 
			
		||||
        ESP_LOGW(TAG, "%s: Connection closed", this->client_combined_info_.c_str());
 | 
			
		||||
          ESP_LOGW(TAG, "%s: Connection closed", this->get_client_combined_info().c_str());
 | 
			
		||||
        } else {
 | 
			
		||||
        ESP_LOGW(TAG, "%s: Reading failed: %s errno=%d", this->client_combined_info_.c_str(), api_error_to_str(err),
 | 
			
		||||
                 errno);
 | 
			
		||||
          ESP_LOGW(TAG, "%s: Reading failed: %s errno=%d", this->get_client_combined_info().c_str(),
 | 
			
		||||
                   api_error_to_str(err), errno);
 | 
			
		||||
        }
 | 
			
		||||
        return;
 | 
			
		||||
      } else {
 | 
			
		||||
      this->last_traffic_ = App.get_loop_component_start_time();
 | 
			
		||||
        this->last_traffic_ = now;
 | 
			
		||||
        // read a packet
 | 
			
		||||
        if (buffer.data_len > 0) {
 | 
			
		||||
          this->read_message(buffer.data_len, buffer.type, &buffer.container[buffer.data_offset]);
 | 
			
		||||
@@ -138,42 +143,39 @@ void APIConnection::loop() {
 | 
			
		||||
          return;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Process deferred batch if scheduled
 | 
			
		||||
  if (this->deferred_batch_.batch_scheduled &&
 | 
			
		||||
      App.get_loop_component_start_time() - this->deferred_batch_.batch_start_time >= this->get_batch_delay_ms_()) {
 | 
			
		||||
      now - this->deferred_batch_.batch_start_time >= this->get_batch_delay_ms_()) {
 | 
			
		||||
    this->process_batch_();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!this->list_entities_iterator_.completed())
 | 
			
		||||
  if (!this->list_entities_iterator_.completed()) {
 | 
			
		||||
    this->list_entities_iterator_.advance();
 | 
			
		||||
  if (!this->initial_state_iterator_.completed() && this->list_entities_iterator_.completed())
 | 
			
		||||
  } else if (!this->initial_state_iterator_.completed()) {
 | 
			
		||||
    this->initial_state_iterator_.advance();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static uint8_t max_ping_retries = 60;
 | 
			
		||||
  static uint16_t ping_retry_interval = 1000;
 | 
			
		||||
  const uint32_t now = App.get_loop_component_start_time();
 | 
			
		||||
  if (this->sent_ping_) {
 | 
			
		||||
    // Disconnect if not responded within 2.5*keepalive
 | 
			
		||||
    if (now - this->last_traffic_ > (KEEPALIVE_TIMEOUT_MS * 5) / 2) {
 | 
			
		||||
    if (now - this->last_traffic_ > KEEPALIVE_DISCONNECT_TIMEOUT) {
 | 
			
		||||
      on_fatal_error();
 | 
			
		||||
      ESP_LOGW(TAG, "%s is unresponsive; disconnecting", this->client_combined_info_.c_str());
 | 
			
		||||
      ESP_LOGW(TAG, "%s is unresponsive; disconnecting", this->get_client_combined_info().c_str());
 | 
			
		||||
    }
 | 
			
		||||
  } else if (now - this->last_traffic_ > KEEPALIVE_TIMEOUT_MS && now > this->next_ping_retry_) {
 | 
			
		||||
    ESP_LOGVV(TAG, "Sending keepalive PING");
 | 
			
		||||
    this->sent_ping_ = this->send_message(PingRequest());
 | 
			
		||||
    if (!this->sent_ping_) {
 | 
			
		||||
      this->next_ping_retry_ = now + ping_retry_interval;
 | 
			
		||||
      this->next_ping_retry_ = now + PING_RETRY_INTERVAL;
 | 
			
		||||
      this->ping_retries_++;
 | 
			
		||||
      std::string warn_str = str_sprintf("%s: Sending keepalive failed %u time(s);",
 | 
			
		||||
                                         this->client_combined_info_.c_str(), this->ping_retries_);
 | 
			
		||||
      if (this->ping_retries_ >= max_ping_retries) {
 | 
			
		||||
      if (this->ping_retries_ >= MAX_PING_RETRIES) {
 | 
			
		||||
        on_fatal_error();
 | 
			
		||||
        ESP_LOGE(TAG, "%s disconnecting", warn_str.c_str());
 | 
			
		||||
        ESP_LOGE(TAG, "%s: Ping failed %u times", this->get_client_combined_info().c_str(), this->ping_retries_);
 | 
			
		||||
      } else if (this->ping_retries_ >= 10) {
 | 
			
		||||
        ESP_LOGW(TAG, "%s retrying in %u ms", warn_str.c_str(), ping_retry_interval);
 | 
			
		||||
        ESP_LOGW(TAG, "%s: Ping retry %u", this->get_client_combined_info().c_str(), this->ping_retries_);
 | 
			
		||||
      } else {
 | 
			
		||||
        ESP_LOGD(TAG, "%s retrying in %u ms", warn_str.c_str(), ping_retry_interval);
 | 
			
		||||
        ESP_LOGD(TAG, "%s: Ping retry %u", this->get_client_combined_info().c_str(), this->ping_retries_);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@@ -197,22 +199,20 @@ void APIConnection::loop() {
 | 
			
		||||
    // bool done = 3;
 | 
			
		||||
    buffer.encode_bool(3, done);
 | 
			
		||||
 | 
			
		||||
    bool success = this->send_buffer(buffer, 44);
 | 
			
		||||
    bool success = this->send_buffer(buffer, CameraImageResponse::MESSAGE_TYPE);
 | 
			
		||||
 | 
			
		||||
    if (success) {
 | 
			
		||||
      this->image_reader_.consume_data(to_send);
 | 
			
		||||
    }
 | 
			
		||||
    if (success && done) {
 | 
			
		||||
      if (done) {
 | 
			
		||||
        this->image_reader_.return_image();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  if (state_subs_at_ != -1) {
 | 
			
		||||
  if (state_subs_at_ >= 0) {
 | 
			
		||||
    const auto &subs = this->parent_->get_state_subs();
 | 
			
		||||
    if (state_subs_at_ >= (int) subs.size()) {
 | 
			
		||||
      state_subs_at_ = -1;
 | 
			
		||||
    } else {
 | 
			
		||||
    if (state_subs_at_ < static_cast<int>(subs.size())) {
 | 
			
		||||
      auto &it = subs[state_subs_at_];
 | 
			
		||||
      SubscribeHomeAssistantStateResponse resp;
 | 
			
		||||
      resp.entity_id = it.entity_id;
 | 
			
		||||
@@ -221,6 +221,8 @@ void APIConnection::loop() {
 | 
			
		||||
      if (this->send_message(resp)) {
 | 
			
		||||
        state_subs_at_++;
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      state_subs_at_ = -1;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -233,7 +235,7 @@ DisconnectResponse APIConnection::disconnect(const DisconnectRequest &msg) {
 | 
			
		||||
  // remote initiated disconnect_client
 | 
			
		||||
  // don't close yet, we still need to send the disconnect response
 | 
			
		||||
  // close will happen on next loop
 | 
			
		||||
  ESP_LOGD(TAG, "%s disconnected", this->client_combined_info_.c_str());
 | 
			
		||||
  ESP_LOGD(TAG, "%s disconnected", this->get_client_combined_info().c_str());
 | 
			
		||||
  this->next_close_ = true;
 | 
			
		||||
  DisconnectResponse resp;
 | 
			
		||||
  return resp;
 | 
			
		||||
@@ -274,6 +276,11 @@ uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint16_t mes
 | 
			
		||||
  // Encode directly into buffer
 | 
			
		||||
  msg.encode(buffer);
 | 
			
		||||
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  // Log the message for VV debugging
 | 
			
		||||
  conn->log_send_message_(msg.message_name(), msg.dump());
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  // Calculate actual encoded size (not including header that was already added)
 | 
			
		||||
  size_t actual_payload_size = shared_buf.size() - size_before_encode;
 | 
			
		||||
 | 
			
		||||
@@ -1430,7 +1437,7 @@ void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRe
 | 
			
		||||
 | 
			
		||||
#ifdef USE_EVENT
 | 
			
		||||
void APIConnection::send_event(event::Event *event, const std::string &event_type) {
 | 
			
		||||
  this->schedule_message_(event, MessageCreator(event_type, EventResponse::MESSAGE_TYPE), EventResponse::MESSAGE_TYPE);
 | 
			
		||||
  this->schedule_message_(event, MessageCreator(event_type), EventResponse::MESSAGE_TYPE);
 | 
			
		||||
}
 | 
			
		||||
void APIConnection::send_event_info(event::Event *event) {
 | 
			
		||||
  this->schedule_message_(event, &APIConnection::try_send_event_info, ListEntitiesEventResponse::MESSAGE_TYPE);
 | 
			
		||||
@@ -1544,8 +1551,7 @@ bool APIConnection::try_send_log_message(int level, const char *tag, const char
 | 
			
		||||
HelloResponse APIConnection::hello(const HelloRequest &msg) {
 | 
			
		||||
  this->client_info_ = msg.client_info;
 | 
			
		||||
  this->client_peername_ = this->helper_->getpeername();
 | 
			
		||||
  this->client_combined_info_ = this->client_info_ + " (" + this->client_peername_ + ")";
 | 
			
		||||
  this->helper_->set_log_info(this->client_combined_info_);
 | 
			
		||||
  this->helper_->set_log_info(this->get_client_combined_info());
 | 
			
		||||
  this->client_api_version_major_ = msg.api_version_major;
 | 
			
		||||
  this->client_api_version_minor_ = msg.api_version_minor;
 | 
			
		||||
  ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu32 ".%" PRIu32, this->client_info_.c_str(),
 | 
			
		||||
@@ -1567,7 +1573,7 @@ ConnectResponse APIConnection::connect(const ConnectRequest &msg) {
 | 
			
		||||
  // bool invalid_password = 1;
 | 
			
		||||
  resp.invalid_password = !correct;
 | 
			
		||||
  if (correct) {
 | 
			
		||||
    ESP_LOGD(TAG, "%s connected", this->client_combined_info_.c_str());
 | 
			
		||||
    ESP_LOGD(TAG, "%s connected", this->get_client_combined_info().c_str());
 | 
			
		||||
    this->connection_state_ = ConnectionState::AUTHENTICATED;
 | 
			
		||||
    this->parent_->get_client_connected_trigger()->trigger(this->client_info_, this->client_peername_);
 | 
			
		||||
#ifdef USE_HOMEASSISTANT_TIME
 | 
			
		||||
@@ -1620,6 +1626,23 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) {
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_API_NOISE
 | 
			
		||||
  resp.api_encryption_supported = true;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_DEVICES
 | 
			
		||||
  for (auto const &device : App.get_devices()) {
 | 
			
		||||
    DeviceInfo device_info;
 | 
			
		||||
    device_info.device_id = device->get_device_id();
 | 
			
		||||
    device_info.name = device->get_name();
 | 
			
		||||
    device_info.area_id = device->get_area_id();
 | 
			
		||||
    resp.devices.push_back(device_info);
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_AREAS
 | 
			
		||||
  for (auto const &area : App.get_areas()) {
 | 
			
		||||
    AreaInfo area_info;
 | 
			
		||||
    area_info.area_id = area->get_area_id();
 | 
			
		||||
    area_info.name = area->get_name();
 | 
			
		||||
    resp.areas.push_back(area_info);
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
  return resp;
 | 
			
		||||
}
 | 
			
		||||
@@ -1673,7 +1696,7 @@ bool APIConnection::try_to_clear_buffer(bool log_out_of_space) {
 | 
			
		||||
  APIError err = this->helper_->loop();
 | 
			
		||||
  if (err != APIError::OK) {
 | 
			
		||||
    on_fatal_error();
 | 
			
		||||
    ESP_LOGW(TAG, "%s: Socket operation failed: %s errno=%d", this->client_combined_info_.c_str(),
 | 
			
		||||
    ESP_LOGW(TAG, "%s: Socket operation failed: %s errno=%d", this->get_client_combined_info().c_str(),
 | 
			
		||||
             api_error_to_str(err), errno);
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
@@ -1695,10 +1718,10 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint16_t message_type)
 | 
			
		||||
  if (err != APIError::OK) {
 | 
			
		||||
    on_fatal_error();
 | 
			
		||||
    if (err == APIError::SOCKET_WRITE_FAILED && errno == ECONNRESET) {
 | 
			
		||||
      ESP_LOGW(TAG, "%s: Connection reset", this->client_combined_info_.c_str());
 | 
			
		||||
      ESP_LOGW(TAG, "%s: Connection reset", this->get_client_combined_info().c_str());
 | 
			
		||||
    } else {
 | 
			
		||||
      ESP_LOGW(TAG, "%s: Packet write failed %s errno=%d", this->client_combined_info_.c_str(), api_error_to_str(err),
 | 
			
		||||
               errno);
 | 
			
		||||
      ESP_LOGW(TAG, "%s: Packet write failed %s errno=%d", this->get_client_combined_info().c_str(),
 | 
			
		||||
               api_error_to_str(err), errno);
 | 
			
		||||
    }
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
@@ -1707,11 +1730,11 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint16_t message_type)
 | 
			
		||||
}
 | 
			
		||||
void APIConnection::on_unauthenticated_access() {
 | 
			
		||||
  this->on_fatal_error();
 | 
			
		||||
  ESP_LOGD(TAG, "%s requested access without authentication", this->client_combined_info_.c_str());
 | 
			
		||||
  ESP_LOGD(TAG, "%s requested access without authentication", this->get_client_combined_info().c_str());
 | 
			
		||||
}
 | 
			
		||||
void APIConnection::on_no_setup_connection() {
 | 
			
		||||
  this->on_fatal_error();
 | 
			
		||||
  ESP_LOGD(TAG, "%s requested access without full connection", this->client_combined_info_.c_str());
 | 
			
		||||
  ESP_LOGD(TAG, "%s requested access without full connection", this->get_client_combined_info().c_str());
 | 
			
		||||
}
 | 
			
		||||
void APIConnection::on_fatal_error() {
 | 
			
		||||
  this->helper_->close();
 | 
			
		||||
@@ -1769,7 +1792,8 @@ void APIConnection::process_batch_() {
 | 
			
		||||
    const auto &item = this->deferred_batch_.items[0];
 | 
			
		||||
 | 
			
		||||
    // Let the creator calculate size and encode if it fits
 | 
			
		||||
    uint16_t payload_size = item.creator(item.entity, this, std::numeric_limits<uint16_t>::max(), true);
 | 
			
		||||
    uint16_t payload_size =
 | 
			
		||||
        item.creator(item.entity, this, std::numeric_limits<uint16_t>::max(), true, item.message_type);
 | 
			
		||||
 | 
			
		||||
    if (payload_size > 0 &&
 | 
			
		||||
        this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, item.message_type)) {
 | 
			
		||||
@@ -1819,7 +1843,7 @@ void APIConnection::process_batch_() {
 | 
			
		||||
  for (const auto &item : this->deferred_batch_.items) {
 | 
			
		||||
    // Try to encode message
 | 
			
		||||
    // The creator will calculate overhead to determine if the message fits
 | 
			
		||||
    uint16_t payload_size = item.creator(item.entity, this, remaining_size, false);
 | 
			
		||||
    uint16_t payload_size = item.creator(item.entity, this, remaining_size, false, item.message_type);
 | 
			
		||||
 | 
			
		||||
    if (payload_size == 0) {
 | 
			
		||||
      // Message won't fit, stop processing
 | 
			
		||||
@@ -1860,10 +1884,10 @@ void APIConnection::process_batch_() {
 | 
			
		||||
  if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
 | 
			
		||||
    on_fatal_error();
 | 
			
		||||
    if (err == APIError::SOCKET_WRITE_FAILED && errno == ECONNRESET) {
 | 
			
		||||
      ESP_LOGW(TAG, "%s: Connection reset during batch write", this->client_combined_info_.c_str());
 | 
			
		||||
      ESP_LOGW(TAG, "%s: Connection reset during batch write", this->get_client_combined_info().c_str());
 | 
			
		||||
    } else {
 | 
			
		||||
      ESP_LOGW(TAG, "%s: Batch write failed %s errno=%d", this->client_combined_info_.c_str(), api_error_to_str(err),
 | 
			
		||||
               errno);
 | 
			
		||||
      ESP_LOGW(TAG, "%s: Batch write failed %s errno=%d", this->get_client_combined_info().c_str(),
 | 
			
		||||
               api_error_to_str(err), errno);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -1882,22 +1906,24 @@ void APIConnection::process_batch_() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
uint16_t APIConnection::MessageCreator::operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
 | 
			
		||||
                                                   bool is_single) const {
 | 
			
		||||
  switch (message_type_) {
 | 
			
		||||
    case 0:  // Function pointer
 | 
			
		||||
      return data_.ptr(entity, conn, remaining_size, is_single);
 | 
			
		||||
 | 
			
		||||
                                                   bool is_single, uint16_t message_type) const {
 | 
			
		||||
  if (has_tagged_string_ptr_()) {
 | 
			
		||||
    // Handle string-based messages
 | 
			
		||||
    switch (message_type) {
 | 
			
		||||
#ifdef USE_EVENT
 | 
			
		||||
      case EventResponse::MESSAGE_TYPE: {
 | 
			
		||||
        auto *e = static_cast<event::Event *>(entity);
 | 
			
		||||
      return APIConnection::try_send_event_response(e, *data_.string_ptr, conn, remaining_size, is_single);
 | 
			
		||||
        return APIConnection::try_send_event_response(e, *get_string_ptr_(), conn, remaining_size, is_single);
 | 
			
		||||
      }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
      default:
 | 
			
		||||
        // Should not happen, return 0 to indicate no message
 | 
			
		||||
        return 0;
 | 
			
		||||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    // Function pointer case
 | 
			
		||||
    return data_.ptr(entity, conn, remaining_size, is_single);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
uint16_t APIConnection::try_send_list_info_done(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
 | 
			
		||||
 
 | 
			
		||||
@@ -275,7 +275,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,6 +301,9 @@ 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
 | 
			
		||||
@@ -432,90 +441,99 @@ 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 {
 | 
			
		||||
  // Pointers first (4 bytes each, naturally aligned)
 | 
			
		||||
  std::unique_ptr<APIFrameHelper> helper_;
 | 
			
		||||
  APIServer *parent_;
 | 
			
		||||
 | 
			
		||||
  // 4-byte aligned types
 | 
			
		||||
  uint32_t last_traffic_;
 | 
			
		||||
  uint32_t next_ping_retry_{0};
 | 
			
		||||
  int state_subs_at_ = -1;
 | 
			
		||||
 | 
			
		||||
  // Strings (12 bytes each on 32-bit)
 | 
			
		||||
  std::string client_info_;
 | 
			
		||||
  std::string client_peername_;
 | 
			
		||||
 | 
			
		||||
  // 2-byte aligned types
 | 
			
		||||
  uint16_t client_api_version_major_{0};
 | 
			
		||||
  uint16_t client_api_version_minor_{0};
 | 
			
		||||
 | 
			
		||||
  // Group all 1-byte types together to minimize padding
 | 
			
		||||
  enum class ConnectionState : uint8_t {
 | 
			
		||||
    WAITING_FOR_HELLO,
 | 
			
		||||
    CONNECTED,
 | 
			
		||||
    AUTHENTICATED,
 | 
			
		||||
  } connection_state_{ConnectionState::WAITING_FOR_HELLO};
 | 
			
		||||
 | 
			
		||||
  uint8_t log_subscription_{ESPHOME_LOG_LEVEL_NONE};
 | 
			
		||||
  bool remove_{false};
 | 
			
		||||
  bool state_subscription_{false};
 | 
			
		||||
  bool sent_ping_{false};
 | 
			
		||||
  bool service_call_subscription_{false};
 | 
			
		||||
  bool next_close_ = false;
 | 
			
		||||
  uint8_t ping_retries_{0};
 | 
			
		||||
  // 8 bytes used, no padding needed
 | 
			
		||||
 | 
			
		||||
  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};
 | 
			
		||||
  // Larger objects at the end
 | 
			
		||||
  InitialStateIterator initial_state_iterator_;
 | 
			
		||||
  ListEntitiesIterator list_entities_iterator_;
 | 
			
		||||
#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_;
 | 
			
		||||
  InitialStateIterator initial_state_iterator_;
 | 
			
		||||
  ListEntitiesIterator list_entities_iterator_;
 | 
			
		||||
  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
 | 
			
		||||
  // Optimized MessageCreator class using tagged pointer
 | 
			
		||||
  class MessageCreator {
 | 
			
		||||
    // Ensure pointer alignment allows LSB tagging
 | 
			
		||||
    static_assert(alignof(std::string *) > 1, "String pointer alignment must be > 1 for LSB tagging");
 | 
			
		||||
 | 
			
		||||
   public:
 | 
			
		||||
    // Constructor for function pointer (message_type = 0)
 | 
			
		||||
    MessageCreator(MessageCreatorPtr ptr) : message_type_(0) { data_.ptr = ptr; }
 | 
			
		||||
    // Constructor for function pointer
 | 
			
		||||
    MessageCreator(MessageCreatorPtr ptr) {
 | 
			
		||||
      // Function pointers are always aligned, so LSB is 0
 | 
			
		||||
      data_.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) {
 | 
			
		||||
      // Allocate string and tag the pointer
 | 
			
		||||
      auto *str = new std::string(str_value);
 | 
			
		||||
      // Set LSB to 1 to indicate string pointer
 | 
			
		||||
      data_.tagged = reinterpret_cast<uintptr_t>(str) | 1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Destructor
 | 
			
		||||
    ~MessageCreator() {
 | 
			
		||||
      // Clean up string data for string-based message types
 | 
			
		||||
      if (uses_string_data_()) {
 | 
			
		||||
        delete data_.string_ptr;
 | 
			
		||||
      if (has_tagged_string_ptr_()) {
 | 
			
		||||
        delete get_string_ptr_();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 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);
 | 
			
		||||
    MessageCreator(const MessageCreator &other) {
 | 
			
		||||
      if (other.has_tagged_string_ptr_()) {
 | 
			
		||||
        auto *str = new std::string(*other.get_string_ptr_());
 | 
			
		||||
        data_.tagged = reinterpret_cast<uintptr_t>(str) | 1;
 | 
			
		||||
      } else {
 | 
			
		||||
        data_ = other.data_;  // For POD types
 | 
			
		||||
        data_ = other.data_;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // 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;
 | 
			
		||||
    }
 | 
			
		||||
    MessageCreator(MessageCreator &&other) noexcept : data_(other.data_) { 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;
 | 
			
		||||
        if (has_tagged_string_ptr_()) {
 | 
			
		||||
          delete get_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);
 | 
			
		||||
        if (other.has_tagged_string_ptr_()) {
 | 
			
		||||
          auto *str = new std::string(*other.get_string_ptr_());
 | 
			
		||||
          data_.tagged = reinterpret_cast<uintptr_t>(str) | 1;
 | 
			
		||||
        } else {
 | 
			
		||||
          data_ = other.data_;
 | 
			
		||||
        }
 | 
			
		||||
@@ -526,30 +544,35 @@ class APIConnection : public APIServerConnection {
 | 
			
		||||
    MessageCreator &operator=(MessageCreator &&other) noexcept {
 | 
			
		||||
      if (this != &other) {
 | 
			
		||||
        // Clean up current string data if needed
 | 
			
		||||
        if (uses_string_data_()) {
 | 
			
		||||
          delete data_.string_ptr;
 | 
			
		||||
        if (has_tagged_string_ptr_()) {
 | 
			
		||||
          delete get_string_ptr_();
 | 
			
		||||
        }
 | 
			
		||||
        // Move data
 | 
			
		||||
        message_type_ = other.message_type_;
 | 
			
		||||
        data_ = other.data_;
 | 
			
		||||
        // Reset other to safe state
 | 
			
		||||
        other.message_type_ = 0;
 | 
			
		||||
        other.data_.ptr = nullptr;
 | 
			
		||||
      }
 | 
			
		||||
      return *this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Call operator
 | 
			
		||||
    uint16_t operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) const;
 | 
			
		||||
    // Call operator - now accepts message_type as parameter
 | 
			
		||||
    uint16_t operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single,
 | 
			
		||||
                        uint16_t message_type) const;
 | 
			
		||||
 | 
			
		||||
   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)
 | 
			
		||||
    // Check if this contains a string pointer
 | 
			
		||||
    bool has_tagged_string_ptr_() const { return (data_.tagged & 1) != 0; }
 | 
			
		||||
 | 
			
		||||
    // Get the actual string pointer (clears the tag bit)
 | 
			
		||||
    std::string *get_string_ptr_() const {
 | 
			
		||||
      // NOLINTNEXTLINE(performance-no-int-to-ptr)
 | 
			
		||||
      return reinterpret_cast<std::string *>(data_.tagged & ~uintptr_t(1));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    union {
 | 
			
		||||
      MessageCreatorPtr ptr;
 | 
			
		||||
      uintptr_t tagged;
 | 
			
		||||
    } data_;  // 4 bytes on 32-bit
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  // Generic batching mechanism for both state updates and entity info
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
@@ -274,17 +285,21 @@ APIError APINoiseFrameHelper::init() {
 | 
			
		||||
}
 | 
			
		||||
/// Run through handshake messages (if in that phase)
 | 
			
		||||
APIError APINoiseFrameHelper::loop() {
 | 
			
		||||
  // 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 (!this->tx_buf_.empty()) {
 | 
			
		||||
    err = try_send_tx_buf_();
 | 
			
		||||
    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
 | 
			
		||||
@@ -330,17 +345,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) {
 | 
			
		||||
@@ -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) {
 | 
			
		||||
@@ -822,18 +831,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
 | 
			
		||||
 
 | 
			
		||||
@@ -38,7 +38,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 +74,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(); }
 | 
			
		||||
@@ -125,38 +125,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,15 +137,41 @@ 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_();
 | 
			
		||||
@@ -213,19 +207,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
 | 
			
		||||
 | 
			
		||||
@@ -252,6 +255,12 @@ class APIPlaintextFrameHelper : public APIFrameHelper {
 | 
			
		||||
 | 
			
		||||
 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 +272,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
											
										
									
								
							@@ -620,544 +620,300 @@ void APIServerConnection::on_ping_request(const PingRequest &msg) {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void APIServerConnection::on_device_info_request(const DeviceInfoRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
    this->on_no_setup_connection();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->check_connection_setup_()) {
 | 
			
		||||
    DeviceInfoResponse ret = this->device_info(msg);
 | 
			
		||||
    if (!this->send_message(ret)) {
 | 
			
		||||
      this->on_fatal_error();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void APIServerConnection::on_list_entities_request(const ListEntitiesRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
    this->on_no_setup_connection();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->is_authenticated()) {
 | 
			
		||||
    this->on_unauthenticated_access();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->list_entities(msg);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void APIServerConnection::on_subscribe_states_request(const SubscribeStatesRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
    this->on_no_setup_connection();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->is_authenticated()) {
 | 
			
		||||
    this->on_unauthenticated_access();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->subscribe_states(msg);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void APIServerConnection::on_subscribe_logs_request(const SubscribeLogsRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
    this->on_no_setup_connection();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->is_authenticated()) {
 | 
			
		||||
    this->on_unauthenticated_access();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->subscribe_logs(msg);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void APIServerConnection::on_subscribe_homeassistant_services_request(
 | 
			
		||||
    const SubscribeHomeassistantServicesRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
    this->on_no_setup_connection();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->is_authenticated()) {
 | 
			
		||||
    this->on_unauthenticated_access();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->subscribe_homeassistant_services(msg);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void APIServerConnection::on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
    this->on_no_setup_connection();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->is_authenticated()) {
 | 
			
		||||
    this->on_unauthenticated_access();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->subscribe_home_assistant_states(msg);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void APIServerConnection::on_get_time_request(const GetTimeRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
    this->on_no_setup_connection();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->check_connection_setup_()) {
 | 
			
		||||
    GetTimeResponse ret = this->get_time(msg);
 | 
			
		||||
    if (!this->send_message(ret)) {
 | 
			
		||||
      this->on_fatal_error();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void APIServerConnection::on_execute_service_request(const ExecuteServiceRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
    this->on_no_setup_connection();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->is_authenticated()) {
 | 
			
		||||
    this->on_unauthenticated_access();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->execute_service(msg);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
#ifdef USE_API_NOISE
 | 
			
		||||
void APIServerConnection::on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
    this->on_no_setup_connection();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->is_authenticated()) {
 | 
			
		||||
    this->on_unauthenticated_access();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    NoiseEncryptionSetKeyResponse ret = this->noise_encryption_set_key(msg);
 | 
			
		||||
    if (!this->send_message(ret)) {
 | 
			
		||||
      this->on_fatal_error();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_BUTTON
 | 
			
		||||
void APIServerConnection::on_button_command_request(const ButtonCommandRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
    this->on_no_setup_connection();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->is_authenticated()) {
 | 
			
		||||
    this->on_unauthenticated_access();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->button_command(msg);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_ESP32_CAMERA
 | 
			
		||||
void APIServerConnection::on_camera_image_request(const CameraImageRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
    this->on_no_setup_connection();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->is_authenticated()) {
 | 
			
		||||
    this->on_unauthenticated_access();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->camera_image(msg);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_CLIMATE
 | 
			
		||||
void APIServerConnection::on_climate_command_request(const ClimateCommandRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
    this->on_no_setup_connection();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->is_authenticated()) {
 | 
			
		||||
    this->on_unauthenticated_access();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->climate_command(msg);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_COVER
 | 
			
		||||
void APIServerConnection::on_cover_command_request(const CoverCommandRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
    this->on_no_setup_connection();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->is_authenticated()) {
 | 
			
		||||
    this->on_unauthenticated_access();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->cover_command(msg);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_DATETIME_DATE
 | 
			
		||||
void APIServerConnection::on_date_command_request(const DateCommandRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
    this->on_no_setup_connection();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->is_authenticated()) {
 | 
			
		||||
    this->on_unauthenticated_access();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->date_command(msg);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_DATETIME_DATETIME
 | 
			
		||||
void APIServerConnection::on_date_time_command_request(const DateTimeCommandRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
    this->on_no_setup_connection();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->is_authenticated()) {
 | 
			
		||||
    this->on_unauthenticated_access();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->datetime_command(msg);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_FAN
 | 
			
		||||
void APIServerConnection::on_fan_command_request(const FanCommandRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
    this->on_no_setup_connection();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->is_authenticated()) {
 | 
			
		||||
    this->on_unauthenticated_access();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->fan_command(msg);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_LIGHT
 | 
			
		||||
void APIServerConnection::on_light_command_request(const LightCommandRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
    this->on_no_setup_connection();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->is_authenticated()) {
 | 
			
		||||
    this->on_unauthenticated_access();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->light_command(msg);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_LOCK
 | 
			
		||||
void APIServerConnection::on_lock_command_request(const LockCommandRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
    this->on_no_setup_connection();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->is_authenticated()) {
 | 
			
		||||
    this->on_unauthenticated_access();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->lock_command(msg);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_MEDIA_PLAYER
 | 
			
		||||
void APIServerConnection::on_media_player_command_request(const MediaPlayerCommandRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
    this->on_no_setup_connection();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->is_authenticated()) {
 | 
			
		||||
    this->on_unauthenticated_access();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->media_player_command(msg);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_NUMBER
 | 
			
		||||
void APIServerConnection::on_number_command_request(const NumberCommandRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
    this->on_no_setup_connection();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->is_authenticated()) {
 | 
			
		||||
    this->on_unauthenticated_access();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->number_command(msg);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SELECT
 | 
			
		||||
void APIServerConnection::on_select_command_request(const SelectCommandRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
    this->on_no_setup_connection();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->is_authenticated()) {
 | 
			
		||||
    this->on_unauthenticated_access();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->select_command(msg);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SIREN
 | 
			
		||||
void APIServerConnection::on_siren_command_request(const SirenCommandRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
    this->on_no_setup_connection();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->is_authenticated()) {
 | 
			
		||||
    this->on_unauthenticated_access();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->siren_command(msg);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SWITCH
 | 
			
		||||
void APIServerConnection::on_switch_command_request(const SwitchCommandRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
    this->on_no_setup_connection();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->is_authenticated()) {
 | 
			
		||||
    this->on_unauthenticated_access();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->switch_command(msg);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_TEXT
 | 
			
		||||
void APIServerConnection::on_text_command_request(const TextCommandRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
    this->on_no_setup_connection();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->is_authenticated()) {
 | 
			
		||||
    this->on_unauthenticated_access();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->text_command(msg);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_DATETIME_TIME
 | 
			
		||||
void APIServerConnection::on_time_command_request(const TimeCommandRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
    this->on_no_setup_connection();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->is_authenticated()) {
 | 
			
		||||
    this->on_unauthenticated_access();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->time_command(msg);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_UPDATE
 | 
			
		||||
void APIServerConnection::on_update_command_request(const UpdateCommandRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
    this->on_no_setup_connection();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->is_authenticated()) {
 | 
			
		||||
    this->on_unauthenticated_access();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->update_command(msg);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_VALVE
 | 
			
		||||
void APIServerConnection::on_valve_command_request(const ValveCommandRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
    this->on_no_setup_connection();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->is_authenticated()) {
 | 
			
		||||
    this->on_unauthenticated_access();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->valve_command(msg);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_BLUETOOTH_PROXY
 | 
			
		||||
void APIServerConnection::on_subscribe_bluetooth_le_advertisements_request(
 | 
			
		||||
    const SubscribeBluetoothLEAdvertisementsRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
    this->on_no_setup_connection();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->is_authenticated()) {
 | 
			
		||||
    this->on_unauthenticated_access();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->subscribe_bluetooth_le_advertisements(msg);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_BLUETOOTH_PROXY
 | 
			
		||||
void APIServerConnection::on_bluetooth_device_request(const BluetoothDeviceRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
    this->on_no_setup_connection();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->is_authenticated()) {
 | 
			
		||||
    this->on_unauthenticated_access();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->bluetooth_device_request(msg);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_BLUETOOTH_PROXY
 | 
			
		||||
void APIServerConnection::on_bluetooth_gatt_get_services_request(const BluetoothGATTGetServicesRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
    this->on_no_setup_connection();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->is_authenticated()) {
 | 
			
		||||
    this->on_unauthenticated_access();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->bluetooth_gatt_get_services(msg);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_BLUETOOTH_PROXY
 | 
			
		||||
void APIServerConnection::on_bluetooth_gatt_read_request(const BluetoothGATTReadRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
    this->on_no_setup_connection();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->is_authenticated()) {
 | 
			
		||||
    this->on_unauthenticated_access();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->bluetooth_gatt_read(msg);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_BLUETOOTH_PROXY
 | 
			
		||||
void APIServerConnection::on_bluetooth_gatt_write_request(const BluetoothGATTWriteRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
    this->on_no_setup_connection();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->is_authenticated()) {
 | 
			
		||||
    this->on_unauthenticated_access();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->bluetooth_gatt_write(msg);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_BLUETOOTH_PROXY
 | 
			
		||||
void APIServerConnection::on_bluetooth_gatt_read_descriptor_request(const BluetoothGATTReadDescriptorRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
    this->on_no_setup_connection();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->is_authenticated()) {
 | 
			
		||||
    this->on_unauthenticated_access();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->bluetooth_gatt_read_descriptor(msg);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_BLUETOOTH_PROXY
 | 
			
		||||
void APIServerConnection::on_bluetooth_gatt_write_descriptor_request(const BluetoothGATTWriteDescriptorRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
    this->on_no_setup_connection();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->is_authenticated()) {
 | 
			
		||||
    this->on_unauthenticated_access();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->bluetooth_gatt_write_descriptor(msg);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_BLUETOOTH_PROXY
 | 
			
		||||
void APIServerConnection::on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
    this->on_no_setup_connection();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->is_authenticated()) {
 | 
			
		||||
    this->on_unauthenticated_access();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->bluetooth_gatt_notify(msg);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_BLUETOOTH_PROXY
 | 
			
		||||
void APIServerConnection::on_subscribe_bluetooth_connections_free_request(
 | 
			
		||||
    const SubscribeBluetoothConnectionsFreeRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
    this->on_no_setup_connection();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->is_authenticated()) {
 | 
			
		||||
    this->on_unauthenticated_access();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    BluetoothConnectionsFreeResponse ret = this->subscribe_bluetooth_connections_free(msg);
 | 
			
		||||
    if (!this->send_message(ret)) {
 | 
			
		||||
      this->on_fatal_error();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_BLUETOOTH_PROXY
 | 
			
		||||
void APIServerConnection::on_unsubscribe_bluetooth_le_advertisements_request(
 | 
			
		||||
    const UnsubscribeBluetoothLEAdvertisementsRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
    this->on_no_setup_connection();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->is_authenticated()) {
 | 
			
		||||
    this->on_unauthenticated_access();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->unsubscribe_bluetooth_le_advertisements(msg);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_BLUETOOTH_PROXY
 | 
			
		||||
void APIServerConnection::on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
    this->on_no_setup_connection();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->is_authenticated()) {
 | 
			
		||||
    this->on_unauthenticated_access();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->bluetooth_scanner_set_mode(msg);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_VOICE_ASSISTANT
 | 
			
		||||
void APIServerConnection::on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
    this->on_no_setup_connection();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->is_authenticated()) {
 | 
			
		||||
    this->on_unauthenticated_access();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->subscribe_voice_assistant(msg);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_VOICE_ASSISTANT
 | 
			
		||||
void APIServerConnection::on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
    this->on_no_setup_connection();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->is_authenticated()) {
 | 
			
		||||
    this->on_unauthenticated_access();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    VoiceAssistantConfigurationResponse ret = this->voice_assistant_get_configuration(msg);
 | 
			
		||||
    if (!this->send_message(ret)) {
 | 
			
		||||
      this->on_fatal_error();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_VOICE_ASSISTANT
 | 
			
		||||
void APIServerConnection::on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
    this->on_no_setup_connection();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->is_authenticated()) {
 | 
			
		||||
    this->on_unauthenticated_access();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->voice_assistant_set_configuration(msg);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_ALARM_CONTROL_PANEL
 | 
			
		||||
void APIServerConnection::on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) {
 | 
			
		||||
  if (!this->is_connection_setup()) {
 | 
			
		||||
    this->on_no_setup_connection();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->is_authenticated()) {
 | 
			
		||||
    this->on_unauthenticated_access();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->check_authenticated_()) {
 | 
			
		||||
    this->alarm_control_panel_command(msg);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -19,7 +19,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);
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -47,6 +47,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");
 | 
			
		||||
@@ -106,8 +111,6 @@ void APIServer::setup() {
 | 
			
		||||
  }
 | 
			
		||||
#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(
 | 
			
		||||
@@ -121,6 +124,16 @@ void APIServer::setup() {
 | 
			
		||||
#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() {
 | 
			
		||||
  // Accept new clients only if the socket exists and has incoming connections
 | 
			
		||||
  if (this->socket_ && this->socket_->ready()) {
 | 
			
		||||
@@ -130,51 +143,61 @@ 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()) {
 | 
			
		||||
  // 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
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  size_t client_index = 0;
 | 
			
		||||
  while (client_index < this->clients_.size()) {
 | 
			
		||||
    auto &client = this->clients_[client_index];
 | 
			
		||||
 | 
			
		||||
      if (client->remove_) {
 | 
			
		||||
        // Handle disconnection
 | 
			
		||||
    if (!client->remove_) {
 | 
			
		||||
      // Common case: process active client
 | 
			
		||||
      client->loop();
 | 
			
		||||
      client_index++;
 | 
			
		||||
      continue;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Rare case: 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());
 | 
			
		||||
    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();
 | 
			
		||||
        // 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
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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();
 | 
			
		||||
    // 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
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -227,7 +250,7 @@ bool APIServer::check_password(const std::string &password) const {
 | 
			
		||||
void APIServer::handle_disconnect(APIConnection *conn) {}
 | 
			
		||||
 | 
			
		||||
#ifdef USE_BINARY_SENSOR
 | 
			
		||||
void APIServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state) {
 | 
			
		||||
void APIServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj) {
 | 
			
		||||
  if (obj->is_internal())
 | 
			
		||||
    return;
 | 
			
		||||
  for (auto &c : this->clients_)
 | 
			
		||||
 
 | 
			
		||||
@@ -54,7 +54,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;
 | 
			
		||||
@@ -142,19 +142,27 @@ class APIServer : public Component, public Controller {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 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};
 | 
			
		||||
  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>();
 | 
			
		||||
 | 
			
		||||
  // 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_;
 | 
			
		||||
  std::string password_;
 | 
			
		||||
  std::vector<uint8_t> shared_write_buffer_;  // Shared proto write buffer for all connections
 | 
			
		||||
  std::vector<HomeAssistantStateSubscription> state_subs_;
 | 
			
		||||
  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>();
 | 
			
		||||
 | 
			
		||||
  // Group smaller types together
 | 
			
		||||
  uint16_t port_{6053};
 | 
			
		||||
  bool shutting_down_ = false;
 | 
			
		||||
  // 3 bytes used, 1 byte padding
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API_NOISE
 | 
			
		||||
  std::shared_ptr<APINoiseContext> noise_ctx_ = std::make_shared<APINoiseContext>();
 | 
			
		||||
 
 | 
			
		||||
@@ -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:
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -21,8 +21,8 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
@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")
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
 
 | 
			
		||||
@@ -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() {
 | 
			
		||||
 
 | 
			
		||||
@@ -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");
 | 
			
		||||
 
 | 
			
		||||
@@ -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!");
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
    });
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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 {
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import re
 | 
			
		||||
 | 
			
		||||
from esphome import automation
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
 
 | 
			
		||||
@@ -41,6 +41,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:
 | 
			
		||||
 
 | 
			
		||||
@@ -37,7 +37,12 @@ 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_) {
 | 
			
		||||
@@ -50,6 +55,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 +75,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;
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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:
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
 
 | 
			
		||||
@@ -3,4 +3,5 @@
 | 
			
		||||
CODEOWNERS = ["@esphome/core"]
 | 
			
		||||
 | 
			
		||||
CONF_DRAW_ROUNDING = "draw_rounding"
 | 
			
		||||
CONF_ON_STATE_CHANGE = "on_state_change"
 | 
			
		||||
CONF_REQUEST_HEADERS = "request_headers"
 | 
			
		||||
 
 | 
			
		||||
@@ -33,8 +33,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
 | 
			
		||||
 | 
			
		||||
IS_PLATFORM_COMPONENT = True
 | 
			
		||||
 | 
			
		||||
@@ -126,6 +126,9 @@ _COVER_SCHEMA = (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
_COVER_SCHEMA.add_extra(entity_duplicate_validator("cover"))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def cover_schema(
 | 
			
		||||
    class_: MockObjClass,
 | 
			
		||||
    *,
 | 
			
		||||
@@ -154,7 +157,7 @@ COVER_SCHEMA.add_extra(cv.deprecated_schema_constant("cover"))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def setup_cover_core_(var, config):
 | 
			
		||||
    await setup_entity(var, config)
 | 
			
		||||
    await setup_entity(var, config, "cover")
 | 
			
		||||
 | 
			
		||||
    if (device_class := config.get(CONF_DEVICE_CLASS)) is not None:
 | 
			
		||||
        cg.add(var.set_device_class(device_class))
 | 
			
		||||
 
 | 
			
		||||
@@ -22,8 +22,8 @@ from esphome.const import (
 | 
			
		||||
    CONF_YEAR,
 | 
			
		||||
)
 | 
			
		||||
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 = ["@rfdarter", "@jesserockz"]
 | 
			
		||||
 | 
			
		||||
@@ -84,6 +84,8 @@ _DATETIME_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(
 | 
			
		||||
    .extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
 | 
			
		||||
).add_extra(_validate_time_present)
 | 
			
		||||
 | 
			
		||||
_DATETIME_SCHEMA.add_extra(entity_duplicate_validator("datetime"))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def date_schema(class_: MockObjClass) -> cv.Schema:
 | 
			
		||||
    schema = cv.Schema(
 | 
			
		||||
@@ -133,7 +135,7 @@ def datetime_schema(class_: MockObjClass) -> cv.Schema:
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def setup_datetime_core_(var, config):
 | 
			
		||||
    await setup_entity(var, config)
 | 
			
		||||
    await setup_entity(var, config, "datetime")
 | 
			
		||||
 | 
			
		||||
    if (mqtt_id := config.get(CONF_MQTT_ID)) is not None:
 | 
			
		||||
        mqtt_ = cg.new_Pvariable(mqtt_id, var)
 | 
			
		||||
 
 | 
			
		||||
@@ -455,7 +455,7 @@ CONFIG_SCHEMA = cv.Schema(
 | 
			
		||||
                    CONF_NAME: "Demo Plain Sensor",
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    CONF_NAME: "Demo Temperature Sensor",
 | 
			
		||||
                    CONF_NAME: "Demo Temperature Sensor 1",
 | 
			
		||||
                    CONF_UNIT_OF_MEASUREMENT: UNIT_CELSIUS,
 | 
			
		||||
                    CONF_ICON: ICON_THERMOMETER,
 | 
			
		||||
                    CONF_ACCURACY_DECIMALS: 1,
 | 
			
		||||
@@ -463,7 +463,7 @@ CONFIG_SCHEMA = cv.Schema(
 | 
			
		||||
                    CONF_STATE_CLASS: STATE_CLASS_MEASUREMENT,
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    CONF_NAME: "Demo Temperature Sensor",
 | 
			
		||||
                    CONF_NAME: "Demo Temperature Sensor 2",
 | 
			
		||||
                    CONF_UNIT_OF_MEASUREMENT: UNIT_CELSIUS,
 | 
			
		||||
                    CONF_ICON: ICON_THERMOMETER,
 | 
			
		||||
                    CONF_ACCURACY_DECIMALS: 1,
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@ namespace display {
 | 
			
		||||
static const char *const TAG = "display";
 | 
			
		||||
 | 
			
		||||
void DisplayBuffer::init_internal_(uint32_t buffer_length) {
 | 
			
		||||
  ExternalRAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
 | 
			
		||||
  RAMAllocator<uint8_t> allocator;
 | 
			
		||||
  this->buffer_ = allocator.allocate(buffer_length);
 | 
			
		||||
  if (this->buffer_ == nullptr) {
 | 
			
		||||
    ESP_LOGE(TAG, "Could not allocate buffer for display!");
 | 
			
		||||
 
 | 
			
		||||
@@ -132,6 +132,8 @@ def set_core_data(config):
 | 
			
		||||
        choices = CPU_FREQUENCIES[variant]
 | 
			
		||||
        if "160MHZ" in choices:
 | 
			
		||||
            cpu_frequency = "160MHZ"
 | 
			
		||||
        elif "360MHZ" in choices:
 | 
			
		||||
            cpu_frequency = "360MHZ"
 | 
			
		||||
        else:
 | 
			
		||||
            cpu_frequency = choices[-1]
 | 
			
		||||
        config[CONF_CPU_FREQUENCY] = cpu_frequency
 | 
			
		||||
@@ -289,11 +291,8 @@ def add_extra_build_file(filename: str, path: str) -> bool:
 | 
			
		||||
 | 
			
		||||
def _format_framework_arduino_version(ver: cv.Version) -> str:
 | 
			
		||||
    # format the given arduino (https://github.com/espressif/arduino-esp32/releases) version to
 | 
			
		||||
    # a PIO platformio/framework-arduinoespressif32 value
 | 
			
		||||
    # List of package versions: https://api.registry.platformio.org/v3/packages/platformio/tool/framework-arduinoespressif32
 | 
			
		||||
    if ver <= cv.Version(1, 0, 3):
 | 
			
		||||
        return f"~2.{ver.major}{ver.minor:02d}{ver.patch:02d}.0"
 | 
			
		||||
    return f"~3.{ver.major}{ver.minor:02d}{ver.patch:02d}.0"
 | 
			
		||||
    # a PIO pioarduino/framework-arduinoespressif32 value
 | 
			
		||||
    return f"pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/{str(ver)}/esp32-{str(ver)}.zip"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _format_framework_espidf_version(
 | 
			
		||||
@@ -317,12 +316,10 @@ def _format_framework_espidf_version(
 | 
			
		||||
 | 
			
		||||
# The default/recommended arduino framework version
 | 
			
		||||
#  - https://github.com/espressif/arduino-esp32/releases
 | 
			
		||||
#  - https://api.registry.platformio.org/v3/packages/platformio/tool/framework-arduinoespressif32
 | 
			
		||||
RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(2, 0, 5)
 | 
			
		||||
# The platformio/espressif32 version to use for arduino frameworks
 | 
			
		||||
#  - https://github.com/platformio/platform-espressif32/releases
 | 
			
		||||
#  - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif32
 | 
			
		||||
ARDUINO_PLATFORM_VERSION = cv.Version(5, 4, 0)
 | 
			
		||||
RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(3, 1, 3)
 | 
			
		||||
# The platform-espressif32 version to use for arduino frameworks
 | 
			
		||||
#  - https://github.com/pioarduino/platform-espressif32/releases
 | 
			
		||||
ARDUINO_PLATFORM_VERSION = cv.Version(53, 3, 13)
 | 
			
		||||
 | 
			
		||||
# The default/recommended esp-idf framework version
 | 
			
		||||
#  - https://github.com/espressif/esp-idf/releases
 | 
			
		||||
@@ -365,8 +362,8 @@ SUPPORTED_PIOARDUINO_ESP_IDF_5X = [
 | 
			
		||||
def _arduino_check_versions(value):
 | 
			
		||||
    value = value.copy()
 | 
			
		||||
    lookups = {
 | 
			
		||||
        "dev": (cv.Version(2, 1, 0), "https://github.com/espressif/arduino-esp32.git"),
 | 
			
		||||
        "latest": (cv.Version(2, 0, 9), None),
 | 
			
		||||
        "dev": (cv.Version(3, 1, 3), "https://github.com/espressif/arduino-esp32.git"),
 | 
			
		||||
        "latest": (cv.Version(3, 1, 3), None),
 | 
			
		||||
        "recommended": (RECOMMENDED_ARDUINO_FRAMEWORK_VERSION, None),
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -388,6 +385,10 @@ def _arduino_check_versions(value):
 | 
			
		||||
        CONF_PLATFORM_VERSION, _parse_platform_version(str(ARDUINO_PLATFORM_VERSION))
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    if value[CONF_SOURCE].startswith("http"):
 | 
			
		||||
        # prefix is necessary or platformio will complain with a cryptic error
 | 
			
		||||
        value[CONF_SOURCE] = f"framework-arduinoespressif32@{value[CONF_SOURCE]}"
 | 
			
		||||
 | 
			
		||||
    if version != RECOMMENDED_ARDUINO_FRAMEWORK_VERSION:
 | 
			
		||||
        _LOGGER.warning(
 | 
			
		||||
            "The selected Arduino framework version is not the recommended one. "
 | 
			
		||||
@@ -695,6 +696,7 @@ FINAL_VALIDATE_SCHEMA = cv.Schema(final_validate)
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    cg.add_platformio_option("board", config[CONF_BOARD])
 | 
			
		||||
    cg.add_platformio_option("board_upload.flash_size", config[CONF_FLASH_SIZE])
 | 
			
		||||
    cg.set_cpp_standard("gnu++17")
 | 
			
		||||
    cg.add_build_flag("-DUSE_ESP32")
 | 
			
		||||
    cg.add_define("ESPHOME_BOARD", config[CONF_BOARD])
 | 
			
		||||
    cg.add_build_flag(f"-DUSE_ESP32_VARIANT_{config[CONF_VARIANT]}")
 | 
			
		||||
@@ -828,10 +830,7 @@ async def to_code(config):
 | 
			
		||||
        cg.add_platformio_option("framework", "arduino")
 | 
			
		||||
        cg.add_build_flag("-DUSE_ARDUINO")
 | 
			
		||||
        cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ARDUINO")
 | 
			
		||||
        cg.add_platformio_option(
 | 
			
		||||
            "platform_packages",
 | 
			
		||||
            [f"platformio/framework-arduinoespressif32@{conf[CONF_SOURCE]}"],
 | 
			
		||||
        )
 | 
			
		||||
        cg.add_platformio_option("platform_packages", [conf[CONF_SOURCE]])
 | 
			
		||||
 | 
			
		||||
        if CONF_PARTITIONS in config:
 | 
			
		||||
            cg.add_platformio_option("board_build.partitions", config[CONF_PARTITIONS])
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,10 @@
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
 | 
			
		||||
#include "ble.h"
 | 
			
		||||
#include "ble_event_pool.h"
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/application.h"
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
#include <esp_bt.h>
 | 
			
		||||
@@ -23,9 +25,6 @@ namespace esp32_ble {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "esp32_ble";
 | 
			
		||||
 | 
			
		||||
static RAMAllocator<BLEEvent> EVENT_ALLOCATOR(  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
 | 
			
		||||
    RAMAllocator<BLEEvent>::ALLOW_FAILURE | RAMAllocator<BLEEvent>::ALLOC_INTERNAL);
 | 
			
		||||
 | 
			
		||||
void ESP32BLE::setup() {
 | 
			
		||||
  global_ble = this;
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Running setup");
 | 
			
		||||
@@ -326,14 +325,18 @@ void ESP32BLE::loop() {
 | 
			
		||||
      }
 | 
			
		||||
      case BLEEvent::GAP: {
 | 
			
		||||
        esp_gap_ble_cb_event_t gap_event = ble_event->event_.gap.gap_event;
 | 
			
		||||
        if (gap_event == ESP_GAP_BLE_SCAN_RESULT_EVT) {
 | 
			
		||||
        switch (gap_event) {
 | 
			
		||||
          case ESP_GAP_BLE_SCAN_RESULT_EVT:
 | 
			
		||||
            // Use the new scan event handler - no memcpy!
 | 
			
		||||
            for (auto *scan_handler : this->gap_scan_event_handlers_) {
 | 
			
		||||
              scan_handler->gap_scan_event_handler(ble_event->scan_result());
 | 
			
		||||
            }
 | 
			
		||||
        } else if (gap_event == ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT ||
 | 
			
		||||
                   gap_event == ESP_GAP_BLE_SCAN_START_COMPLETE_EVT ||
 | 
			
		||||
                   gap_event == ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT) {
 | 
			
		||||
            break;
 | 
			
		||||
 | 
			
		||||
          // Scan complete events
 | 
			
		||||
          case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT:
 | 
			
		||||
          case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT:
 | 
			
		||||
          case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT:
 | 
			
		||||
            // All three scan complete events have the same structure with just status
 | 
			
		||||
            // The scan_complete struct matches ESP-IDF's layout exactly, so this reinterpret_cast is safe
 | 
			
		||||
            // This is verified at compile-time by static_assert checks in ble_event.h
 | 
			
		||||
@@ -343,15 +346,56 @@ void ESP32BLE::loop() {
 | 
			
		||||
              gap_handler->gap_event_handler(
 | 
			
		||||
                  gap_event, reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.scan_complete));
 | 
			
		||||
            }
 | 
			
		||||
            break;
 | 
			
		||||
 | 
			
		||||
          // Advertising complete events
 | 
			
		||||
          case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT:
 | 
			
		||||
          case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT:
 | 
			
		||||
          case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT:
 | 
			
		||||
          case ESP_GAP_BLE_ADV_START_COMPLETE_EVT:
 | 
			
		||||
          case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT:
 | 
			
		||||
            // All advertising complete events have the same structure with just status
 | 
			
		||||
            ESP_LOGV(TAG, "gap_event_handler - %d", gap_event);
 | 
			
		||||
            for (auto *gap_handler : this->gap_event_handlers_) {
 | 
			
		||||
              gap_handler->gap_event_handler(
 | 
			
		||||
                  gap_event, reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.adv_complete));
 | 
			
		||||
            }
 | 
			
		||||
            break;
 | 
			
		||||
 | 
			
		||||
          // RSSI complete event
 | 
			
		||||
          case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT:
 | 
			
		||||
            ESP_LOGV(TAG, "gap_event_handler - %d", gap_event);
 | 
			
		||||
            for (auto *gap_handler : this->gap_event_handlers_) {
 | 
			
		||||
              gap_handler->gap_event_handler(
 | 
			
		||||
                  gap_event, reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.read_rssi_complete));
 | 
			
		||||
            }
 | 
			
		||||
            break;
 | 
			
		||||
 | 
			
		||||
          // Security events
 | 
			
		||||
          case ESP_GAP_BLE_AUTH_CMPL_EVT:
 | 
			
		||||
          case ESP_GAP_BLE_SEC_REQ_EVT:
 | 
			
		||||
          case ESP_GAP_BLE_PASSKEY_NOTIF_EVT:
 | 
			
		||||
          case ESP_GAP_BLE_PASSKEY_REQ_EVT:
 | 
			
		||||
          case ESP_GAP_BLE_NC_REQ_EVT:
 | 
			
		||||
            ESP_LOGV(TAG, "gap_event_handler - %d", gap_event);
 | 
			
		||||
            for (auto *gap_handler : this->gap_event_handlers_) {
 | 
			
		||||
              gap_handler->gap_event_handler(
 | 
			
		||||
                  gap_event, reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.security));
 | 
			
		||||
            }
 | 
			
		||||
            break;
 | 
			
		||||
 | 
			
		||||
          default:
 | 
			
		||||
            // Unknown/unhandled event
 | 
			
		||||
            ESP_LOGW(TAG, "Unhandled GAP event type in loop: %d", gap_event);
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
      default:
 | 
			
		||||
        break;
 | 
			
		||||
    }
 | 
			
		||||
    // Destructor will clean up external allocations for GATTC/GATTS
 | 
			
		||||
    ble_event->~BLEEvent();
 | 
			
		||||
    EVENT_ALLOCATOR.deallocate(ble_event, 1);
 | 
			
		||||
    // Return the event to the pool
 | 
			
		||||
    this->ble_event_pool_.release(ble_event);
 | 
			
		||||
    ble_event = this->ble_events_.pop();
 | 
			
		||||
  }
 | 
			
		||||
  if (this->advertising_ != nullptr) {
 | 
			
		||||
@@ -359,37 +403,41 @@ void ESP32BLE::loop() {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Log dropped events periodically
 | 
			
		||||
  size_t dropped = this->ble_events_.get_and_reset_dropped_count();
 | 
			
		||||
  uint16_t dropped = this->ble_events_.get_and_reset_dropped_count();
 | 
			
		||||
  if (dropped > 0) {
 | 
			
		||||
    ESP_LOGW(TAG, "Dropped %zu BLE events due to buffer overflow", dropped);
 | 
			
		||||
    ESP_LOGW(TAG, "Dropped %u BLE events due to buffer overflow", dropped);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Helper function to load new event data based on type
 | 
			
		||||
void load_ble_event(BLEEvent *event, esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) {
 | 
			
		||||
  event->load_gap_event(e, p);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void load_ble_event(BLEEvent *event, esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) {
 | 
			
		||||
  event->load_gattc_event(e, i, p);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void load_ble_event(BLEEvent *event, esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) {
 | 
			
		||||
  event->load_gatts_event(e, i, p);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
template<typename... Args> void enqueue_ble_event(Args... args) {
 | 
			
		||||
  // Check if queue is full before allocating
 | 
			
		||||
  if (global_ble->ble_events_.full()) {
 | 
			
		||||
    // Queue is full, drop the event
 | 
			
		||||
  // Allocate an event from the pool
 | 
			
		||||
  BLEEvent *event = global_ble->ble_event_pool_.allocate();
 | 
			
		||||
  if (event == nullptr) {
 | 
			
		||||
    // No events available - queue is full or we're out of memory
 | 
			
		||||
    global_ble->ble_events_.increment_dropped_count();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  BLEEvent *new_event = EVENT_ALLOCATOR.allocate(1);
 | 
			
		||||
  if (new_event == nullptr) {
 | 
			
		||||
    // Memory too fragmented to allocate new event. Can only drop it until memory comes back
 | 
			
		||||
    global_ble->ble_events_.increment_dropped_count();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  new (new_event) BLEEvent(args...);
 | 
			
		||||
  // Load new event data (replaces previous event)
 | 
			
		||||
  load_ble_event(event, args...);
 | 
			
		||||
 | 
			
		||||
  // Push the event - since we're the only producer and we checked full() above,
 | 
			
		||||
  // this should always succeed unless we have a bug
 | 
			
		||||
  if (!global_ble->ble_events_.push(new_event)) {
 | 
			
		||||
    // This should not happen in SPSC queue with single producer
 | 
			
		||||
    ESP_LOGE(TAG, "BLE queue push failed unexpectedly");
 | 
			
		||||
    new_event->~BLEEvent();
 | 
			
		||||
    EVENT_ALLOCATOR.deallocate(new_event, 1);
 | 
			
		||||
  }
 | 
			
		||||
}  // NOLINT(clang-analyzer-unix.Malloc)
 | 
			
		||||
  // Push the event to the queue
 | 
			
		||||
  global_ble->ble_events_.push(event);
 | 
			
		||||
  // Push always succeeds because we're the only producer and the pool ensures we never exceed queue size
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Explicit template instantiations for the friend function
 | 
			
		||||
template void enqueue_ble_event(esp_gap_ble_cb_event_t, esp_ble_gap_cb_param_t *);
 | 
			
		||||
@@ -398,11 +446,26 @@ template void enqueue_ble_event(esp_gattc_cb_event_t, esp_gatt_if_t, esp_ble_gat
 | 
			
		||||
 | 
			
		||||
void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
 | 
			
		||||
  switch (event) {
 | 
			
		||||
    // Only queue the 4 GAP events we actually handle
 | 
			
		||||
    // Queue GAP events that components need to handle
 | 
			
		||||
    // Scanning events - used by esp32_ble_tracker
 | 
			
		||||
    case ESP_GAP_BLE_SCAN_RESULT_EVT:
 | 
			
		||||
    case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT:
 | 
			
		||||
    case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT:
 | 
			
		||||
    case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT:
 | 
			
		||||
    // Advertising events - used by esp32_ble_beacon and esp32_ble server
 | 
			
		||||
    case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT:
 | 
			
		||||
    case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT:
 | 
			
		||||
    case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT:
 | 
			
		||||
    case ESP_GAP_BLE_ADV_START_COMPLETE_EVT:
 | 
			
		||||
    case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT:
 | 
			
		||||
    // Connection events - used by ble_client
 | 
			
		||||
    case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT:
 | 
			
		||||
    // Security events - used by ble_client and bluetooth_proxy
 | 
			
		||||
    case ESP_GAP_BLE_AUTH_CMPL_EVT:
 | 
			
		||||
    case ESP_GAP_BLE_SEC_REQ_EVT:
 | 
			
		||||
    case ESP_GAP_BLE_PASSKEY_NOTIF_EVT:
 | 
			
		||||
    case ESP_GAP_BLE_PASSKEY_REQ_EVT:
 | 
			
		||||
    case ESP_GAP_BLE_NC_REQ_EVT:
 | 
			
		||||
      enqueue_ble_event(event, param);
 | 
			
		||||
      return;
 | 
			
		||||
 | 
			
		||||
@@ -454,13 +517,12 @@ void ESP32BLE::dump_config() {
 | 
			
		||||
        break;
 | 
			
		||||
    }
 | 
			
		||||
    ESP_LOGCONFIG(TAG,
 | 
			
		||||
                  "ESP32 BLE:\n"
 | 
			
		||||
                  "  MAC address: %02X:%02X:%02X:%02X:%02X:%02X\n"
 | 
			
		||||
                  "BLE:\n"
 | 
			
		||||
                  "  MAC address: %s\n"
 | 
			
		||||
                  "  IO Capability: %s",
 | 
			
		||||
                  mac_address[0], mac_address[1], mac_address[2], mac_address[3], mac_address[4], mac_address[5],
 | 
			
		||||
                  io_capability_s);
 | 
			
		||||
                  format_mac_address_pretty(mac_address).c_str(), io_capability_s);
 | 
			
		||||
  } else {
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "ESP32 BLE: bluetooth stack is not enabled");
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "Bluetooth stack is not enabled");
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,7 @@
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
 | 
			
		||||
#include "ble_event.h"
 | 
			
		||||
#include "ble_event_pool.h"
 | 
			
		||||
#include "queue.h"
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
@@ -148,6 +149,7 @@ class ESP32BLE : public Component {
 | 
			
		||||
  BLEComponentState state_{BLE_COMPONENT_STATE_OFF};
 | 
			
		||||
 | 
			
		||||
  LockFreeQueue<BLEEvent, MAX_BLE_QUEUE_SIZE> ble_events_;
 | 
			
		||||
  BLEEventPool<MAX_BLE_QUEUE_SIZE> ble_event_pool_;
 | 
			
		||||
  BLEAdvertising *advertising_{};
 | 
			
		||||
  esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE};
 | 
			
		||||
  uint32_t advertising_cycle_time_{};
 | 
			
		||||
 
 | 
			
		||||
@@ -24,16 +24,45 @@ static_assert(sizeof(esp_ble_gap_cb_param_t::ble_scan_stop_cmpl_evt_param) == si
 | 
			
		||||
              "ESP-IDF scan_stop_cmpl structure has unexpected size");
 | 
			
		||||
 | 
			
		||||
// Verify the status field is at offset 0 (first member)
 | 
			
		||||
static_assert(offsetof(esp_ble_gap_cb_param_t, scan_param_cmpl.status) ==
 | 
			
		||||
                  offsetof(esp_ble_gap_cb_param_t, scan_param_cmpl),
 | 
			
		||||
static_assert(offsetof(esp_ble_gap_cb_param_t, scan_param_cmpl.status) == 0,
 | 
			
		||||
              "status must be first member of scan_param_cmpl");
 | 
			
		||||
static_assert(offsetof(esp_ble_gap_cb_param_t, scan_start_cmpl.status) ==
 | 
			
		||||
                  offsetof(esp_ble_gap_cb_param_t, scan_start_cmpl),
 | 
			
		||||
static_assert(offsetof(esp_ble_gap_cb_param_t, scan_start_cmpl.status) == 0,
 | 
			
		||||
              "status must be first member of scan_start_cmpl");
 | 
			
		||||
static_assert(offsetof(esp_ble_gap_cb_param_t, scan_stop_cmpl.status) ==
 | 
			
		||||
                  offsetof(esp_ble_gap_cb_param_t, scan_stop_cmpl),
 | 
			
		||||
static_assert(offsetof(esp_ble_gap_cb_param_t, scan_stop_cmpl.status) == 0,
 | 
			
		||||
              "status must be first member of scan_stop_cmpl");
 | 
			
		||||
 | 
			
		||||
// Compile-time verification for advertising complete events
 | 
			
		||||
static_assert(sizeof(esp_ble_gap_cb_param_t::ble_adv_data_cmpl_evt_param) == sizeof(esp_bt_status_t),
 | 
			
		||||
              "ESP-IDF adv_data_cmpl structure has unexpected size");
 | 
			
		||||
static_assert(sizeof(esp_ble_gap_cb_param_t::ble_scan_rsp_data_cmpl_evt_param) == sizeof(esp_bt_status_t),
 | 
			
		||||
              "ESP-IDF scan_rsp_data_cmpl structure has unexpected size");
 | 
			
		||||
static_assert(sizeof(esp_ble_gap_cb_param_t::ble_adv_data_raw_cmpl_evt_param) == sizeof(esp_bt_status_t),
 | 
			
		||||
              "ESP-IDF adv_data_raw_cmpl structure has unexpected size");
 | 
			
		||||
static_assert(sizeof(esp_ble_gap_cb_param_t::ble_adv_start_cmpl_evt_param) == sizeof(esp_bt_status_t),
 | 
			
		||||
              "ESP-IDF adv_start_cmpl structure has unexpected size");
 | 
			
		||||
static_assert(sizeof(esp_ble_gap_cb_param_t::ble_adv_stop_cmpl_evt_param) == sizeof(esp_bt_status_t),
 | 
			
		||||
              "ESP-IDF adv_stop_cmpl structure has unexpected size");
 | 
			
		||||
 | 
			
		||||
// Verify the status field is at offset 0 for advertising events
 | 
			
		||||
static_assert(offsetof(esp_ble_gap_cb_param_t, adv_data_cmpl.status) == 0,
 | 
			
		||||
              "status must be first member of adv_data_cmpl");
 | 
			
		||||
static_assert(offsetof(esp_ble_gap_cb_param_t, scan_rsp_data_cmpl.status) == 0,
 | 
			
		||||
              "status must be first member of scan_rsp_data_cmpl");
 | 
			
		||||
static_assert(offsetof(esp_ble_gap_cb_param_t, adv_data_raw_cmpl.status) == 0,
 | 
			
		||||
              "status must be first member of adv_data_raw_cmpl");
 | 
			
		||||
static_assert(offsetof(esp_ble_gap_cb_param_t, adv_start_cmpl.status) == 0,
 | 
			
		||||
              "status must be first member of adv_start_cmpl");
 | 
			
		||||
static_assert(offsetof(esp_ble_gap_cb_param_t, adv_stop_cmpl.status) == 0,
 | 
			
		||||
              "status must be first member of adv_stop_cmpl");
 | 
			
		||||
 | 
			
		||||
// Compile-time verification for RSSI complete event structure
 | 
			
		||||
static_assert(offsetof(esp_ble_gap_cb_param_t, read_rssi_cmpl.status) == 0,
 | 
			
		||||
              "status must be first member of read_rssi_cmpl");
 | 
			
		||||
static_assert(offsetof(esp_ble_gap_cb_param_t, read_rssi_cmpl.rssi) == sizeof(esp_bt_status_t),
 | 
			
		||||
              "rssi must immediately follow status in read_rssi_cmpl");
 | 
			
		||||
static_assert(offsetof(esp_ble_gap_cb_param_t, read_rssi_cmpl.remote_addr) == sizeof(esp_bt_status_t) + sizeof(int8_t),
 | 
			
		||||
              "remote_addr must follow rssi in read_rssi_cmpl");
 | 
			
		||||
 | 
			
		||||
// Received GAP, GATTC and GATTS events are only queued, and get processed in the main loop().
 | 
			
		||||
// This class stores each event with minimal memory usage.
 | 
			
		||||
// GAP events (99% of traffic) don't have the vector overhead.
 | 
			
		||||
@@ -51,6 +80,13 @@ static_assert(offsetof(esp_ble_gap_cb_param_t, scan_stop_cmpl.status) ==
 | 
			
		||||
// - GATTC/GATTS events: We heap-allocate and copy the entire param struct, ensuring
 | 
			
		||||
//   the data remains valid even after the BLE callback returns. The original
 | 
			
		||||
//   param pointer from ESP-IDF is only valid during the callback.
 | 
			
		||||
//
 | 
			
		||||
// CRITICAL DESIGN NOTE:
 | 
			
		||||
// The heap allocations for GATTC/GATTS events are REQUIRED for memory safety.
 | 
			
		||||
// DO NOT attempt to optimize by removing these allocations or storing pointers
 | 
			
		||||
// to the original ESP-IDF data. The ESP-IDF callback data has a different lifetime
 | 
			
		||||
// than our event processing, and accessing it after the callback returns would
 | 
			
		||||
// result in use-after-free bugs and crashes.
 | 
			
		||||
class BLEEvent {
 | 
			
		||||
 public:
 | 
			
		||||
  // NOLINTNEXTLINE(readability-identifier-naming)
 | 
			
		||||
@@ -60,128 +96,88 @@ class BLEEvent {
 | 
			
		||||
    GATTS,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  // Type definitions for cleaner method signatures
 | 
			
		||||
  struct StatusOnlyData {
 | 
			
		||||
    esp_bt_status_t status;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  struct RSSICompleteData {
 | 
			
		||||
    esp_bt_status_t status;
 | 
			
		||||
    int8_t rssi;
 | 
			
		||||
    esp_bd_addr_t remote_addr;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  // Constructor for GAP events - no external allocations needed
 | 
			
		||||
  BLEEvent(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) {
 | 
			
		||||
    this->type_ = GAP;
 | 
			
		||||
    this->event_.gap.gap_event = e;
 | 
			
		||||
 | 
			
		||||
    if (p == nullptr) {
 | 
			
		||||
      return;  // Invalid event, but we can't log in header file
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Only copy the data we actually use for each GAP event type
 | 
			
		||||
    switch (e) {
 | 
			
		||||
      case ESP_GAP_BLE_SCAN_RESULT_EVT:
 | 
			
		||||
        // Copy only the fields we use from scan results
 | 
			
		||||
        memcpy(this->event_.gap.scan_result.bda, p->scan_rst.bda, sizeof(esp_bd_addr_t));
 | 
			
		||||
        this->event_.gap.scan_result.ble_addr_type = p->scan_rst.ble_addr_type;
 | 
			
		||||
        this->event_.gap.scan_result.rssi = p->scan_rst.rssi;
 | 
			
		||||
        this->event_.gap.scan_result.adv_data_len = p->scan_rst.adv_data_len;
 | 
			
		||||
        this->event_.gap.scan_result.scan_rsp_len = p->scan_rst.scan_rsp_len;
 | 
			
		||||
        this->event_.gap.scan_result.search_evt = p->scan_rst.search_evt;
 | 
			
		||||
        memcpy(this->event_.gap.scan_result.ble_adv, p->scan_rst.ble_adv,
 | 
			
		||||
               ESP_BLE_ADV_DATA_LEN_MAX + ESP_BLE_SCAN_RSP_DATA_LEN_MAX);
 | 
			
		||||
        break;
 | 
			
		||||
 | 
			
		||||
      case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT:
 | 
			
		||||
        this->event_.gap.scan_complete.status = p->scan_param_cmpl.status;
 | 
			
		||||
        break;
 | 
			
		||||
 | 
			
		||||
      case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT:
 | 
			
		||||
        this->event_.gap.scan_complete.status = p->scan_start_cmpl.status;
 | 
			
		||||
        break;
 | 
			
		||||
 | 
			
		||||
      case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT:
 | 
			
		||||
        this->event_.gap.scan_complete.status = p->scan_stop_cmpl.status;
 | 
			
		||||
        break;
 | 
			
		||||
 | 
			
		||||
      default:
 | 
			
		||||
        // We only handle 4 GAP event types, others are dropped
 | 
			
		||||
        break;
 | 
			
		||||
    }
 | 
			
		||||
    this->init_gap_data_(e, p);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Constructor for GATTC events - uses heap allocation
 | 
			
		||||
  // Creates a copy of the param struct since the original is only valid during the callback
 | 
			
		||||
  // IMPORTANT: The heap allocation is REQUIRED and must not be removed as an optimization.
 | 
			
		||||
  // The param pointer from ESP-IDF is only valid during the callback execution.
 | 
			
		||||
  // Since BLE events are processed asynchronously in the main loop, we must create
 | 
			
		||||
  // our own copy to ensure the data remains valid until the event is processed.
 | 
			
		||||
  BLEEvent(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) {
 | 
			
		||||
    this->type_ = GATTC;
 | 
			
		||||
    this->event_.gattc.gattc_event = e;
 | 
			
		||||
    this->event_.gattc.gattc_if = i;
 | 
			
		||||
 | 
			
		||||
    if (p == nullptr) {
 | 
			
		||||
      this->event_.gattc.gattc_param = nullptr;
 | 
			
		||||
      this->event_.gattc.data = nullptr;
 | 
			
		||||
      return;  // Invalid event, but we can't log in header file
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Heap-allocate param and data
 | 
			
		||||
    // Heap allocation is used because GATTC/GATTS events are rare (<1% of events)
 | 
			
		||||
    // while GAP events (99%) are stored inline to minimize memory usage
 | 
			
		||||
    this->event_.gattc.gattc_param = new esp_ble_gattc_cb_param_t(*p);
 | 
			
		||||
 | 
			
		||||
    // Copy data for events that need it
 | 
			
		||||
    switch (e) {
 | 
			
		||||
      case ESP_GATTC_NOTIFY_EVT:
 | 
			
		||||
        this->event_.gattc.data = new std::vector<uint8_t>(p->notify.value, p->notify.value + p->notify.value_len);
 | 
			
		||||
        this->event_.gattc.gattc_param->notify.value = this->event_.gattc.data->data();
 | 
			
		||||
        break;
 | 
			
		||||
      case ESP_GATTC_READ_CHAR_EVT:
 | 
			
		||||
      case ESP_GATTC_READ_DESCR_EVT:
 | 
			
		||||
        this->event_.gattc.data = new std::vector<uint8_t>(p->read.value, p->read.value + p->read.value_len);
 | 
			
		||||
        this->event_.gattc.gattc_param->read.value = this->event_.gattc.data->data();
 | 
			
		||||
        break;
 | 
			
		||||
      default:
 | 
			
		||||
        this->event_.gattc.data = nullptr;
 | 
			
		||||
        break;
 | 
			
		||||
    }
 | 
			
		||||
    this->init_gattc_data_(e, i, p);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Constructor for GATTS events - uses heap allocation
 | 
			
		||||
  // Creates a copy of the param struct since the original is only valid during the callback
 | 
			
		||||
  // IMPORTANT: The heap allocation is REQUIRED and must not be removed as an optimization.
 | 
			
		||||
  // The param pointer from ESP-IDF is only valid during the callback execution.
 | 
			
		||||
  // Since BLE events are processed asynchronously in the main loop, we must create
 | 
			
		||||
  // our own copy to ensure the data remains valid until the event is processed.
 | 
			
		||||
  BLEEvent(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) {
 | 
			
		||||
    this->type_ = GATTS;
 | 
			
		||||
    this->event_.gatts.gatts_event = e;
 | 
			
		||||
    this->event_.gatts.gatts_if = i;
 | 
			
		||||
 | 
			
		||||
    if (p == nullptr) {
 | 
			
		||||
      this->event_.gatts.gatts_param = nullptr;
 | 
			
		||||
      this->event_.gatts.data = nullptr;
 | 
			
		||||
      return;  // Invalid event, but we can't log in header file
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Heap-allocate param and data
 | 
			
		||||
    // Heap allocation is used because GATTC/GATTS events are rare (<1% of events)
 | 
			
		||||
    // while GAP events (99%) are stored inline to minimize memory usage
 | 
			
		||||
    this->event_.gatts.gatts_param = new esp_ble_gatts_cb_param_t(*p);
 | 
			
		||||
 | 
			
		||||
    // Copy data for events that need it
 | 
			
		||||
    switch (e) {
 | 
			
		||||
      case ESP_GATTS_WRITE_EVT:
 | 
			
		||||
        this->event_.gatts.data = new std::vector<uint8_t>(p->write.value, p->write.value + p->write.len);
 | 
			
		||||
        this->event_.gatts.gatts_param->write.value = this->event_.gatts.data->data();
 | 
			
		||||
        break;
 | 
			
		||||
      default:
 | 
			
		||||
        this->event_.gatts.data = nullptr;
 | 
			
		||||
        break;
 | 
			
		||||
    }
 | 
			
		||||
    this->init_gatts_data_(e, i, p);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Destructor to clean up heap allocations
 | 
			
		||||
  ~BLEEvent() {
 | 
			
		||||
    switch (this->type_) {
 | 
			
		||||
      case GATTC:
 | 
			
		||||
  ~BLEEvent() { this->cleanup_heap_data(); }
 | 
			
		||||
 | 
			
		||||
  // Default constructor for pre-allocation in pool
 | 
			
		||||
  BLEEvent() : type_(GAP) {}
 | 
			
		||||
 | 
			
		||||
  // Clean up any heap-allocated data
 | 
			
		||||
  void cleanup_heap_data() {
 | 
			
		||||
    if (this->type_ == GAP) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    if (this->type_ == GATTC) {
 | 
			
		||||
      delete this->event_.gattc.gattc_param;
 | 
			
		||||
      delete this->event_.gattc.data;
 | 
			
		||||
        break;
 | 
			
		||||
      case GATTS:
 | 
			
		||||
      this->event_.gattc.gattc_param = nullptr;
 | 
			
		||||
      this->event_.gattc.data = nullptr;
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    if (this->type_ == GATTS) {
 | 
			
		||||
      delete this->event_.gatts.gatts_param;
 | 
			
		||||
      delete this->event_.gatts.data;
 | 
			
		||||
        break;
 | 
			
		||||
      default:
 | 
			
		||||
        break;
 | 
			
		||||
      this->event_.gatts.gatts_param = nullptr;
 | 
			
		||||
      this->event_.gatts.data = nullptr;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Load new event data for reuse (replaces previous event data)
 | 
			
		||||
  void load_gap_event(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) {
 | 
			
		||||
    this->cleanup_heap_data();
 | 
			
		||||
    this->type_ = GAP;
 | 
			
		||||
    this->init_gap_data_(e, p);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void load_gattc_event(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) {
 | 
			
		||||
    this->cleanup_heap_data();
 | 
			
		||||
    this->type_ = GATTC;
 | 
			
		||||
    this->init_gattc_data_(e, i, p);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void load_gatts_event(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) {
 | 
			
		||||
    this->cleanup_heap_data();
 | 
			
		||||
    this->type_ = GATTS;
 | 
			
		||||
    this->init_gatts_data_(e, i, p);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Disable copy to prevent double-delete
 | 
			
		||||
  BLEEvent(const BLEEvent &) = delete;
 | 
			
		||||
  BLEEvent &operator=(const BLEEvent &) = delete;
 | 
			
		||||
@@ -191,12 +187,21 @@ class BLEEvent {
 | 
			
		||||
    struct gap_event {
 | 
			
		||||
      esp_gap_ble_cb_event_t gap_event;
 | 
			
		||||
      union {
 | 
			
		||||
        BLEScanResult scan_result;  // 73 bytes
 | 
			
		||||
        BLEScanResult scan_result;  // 73 bytes - Used by: esp32_ble_tracker
 | 
			
		||||
        // This matches ESP-IDF's scan complete event structures
 | 
			
		||||
        // All three (scan_param_cmpl, scan_start_cmpl, scan_stop_cmpl) have identical layout
 | 
			
		||||
        struct {
 | 
			
		||||
          esp_bt_status_t status;
 | 
			
		||||
        } scan_complete;  // 1 byte
 | 
			
		||||
        // Used by: esp32_ble_tracker
 | 
			
		||||
        StatusOnlyData scan_complete;  // 1 byte
 | 
			
		||||
        // Advertising complete events all have same structure
 | 
			
		||||
        // Used by: esp32_ble_beacon, esp32_ble server components
 | 
			
		||||
        // ADV_DATA_SET, SCAN_RSP_DATA_SET, ADV_DATA_RAW_SET, ADV_START, ADV_STOP
 | 
			
		||||
        StatusOnlyData adv_complete;  // 1 byte
 | 
			
		||||
        // RSSI complete event
 | 
			
		||||
        // Used by: ble_client (ble_rssi_sensor component)
 | 
			
		||||
        RSSICompleteData read_rssi_complete;  // 8 bytes
 | 
			
		||||
        // Security events - we store the full security union
 | 
			
		||||
        // Used by: ble_client (automation), bluetooth_proxy, esp32_ble_client
 | 
			
		||||
        esp_ble_sec_t security;  // Variable size, but fits within scan_result size
 | 
			
		||||
      };
 | 
			
		||||
    } gap;  // 80 bytes total
 | 
			
		||||
 | 
			
		||||
@@ -224,8 +229,170 @@ class BLEEvent {
 | 
			
		||||
  esp_gap_ble_cb_event_t gap_event_type() const { return event_.gap.gap_event; }
 | 
			
		||||
  const BLEScanResult &scan_result() const { return event_.gap.scan_result; }
 | 
			
		||||
  esp_bt_status_t scan_complete_status() const { return event_.gap.scan_complete.status; }
 | 
			
		||||
  esp_bt_status_t adv_complete_status() const { return event_.gap.adv_complete.status; }
 | 
			
		||||
  const RSSICompleteData &read_rssi_complete() const { return event_.gap.read_rssi_complete; }
 | 
			
		||||
  const esp_ble_sec_t &security() const { return event_.gap.security; }
 | 
			
		||||
 | 
			
		||||
 private:
 | 
			
		||||
  // Initialize GAP event data
 | 
			
		||||
  void init_gap_data_(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) {
 | 
			
		||||
    this->event_.gap.gap_event = e;
 | 
			
		||||
 | 
			
		||||
    if (p == nullptr) {
 | 
			
		||||
      return;  // Invalid event, but we can't log in header file
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Copy data based on event type
 | 
			
		||||
    switch (e) {
 | 
			
		||||
      case ESP_GAP_BLE_SCAN_RESULT_EVT:
 | 
			
		||||
        memcpy(this->event_.gap.scan_result.bda, p->scan_rst.bda, sizeof(esp_bd_addr_t));
 | 
			
		||||
        this->event_.gap.scan_result.ble_addr_type = p->scan_rst.ble_addr_type;
 | 
			
		||||
        this->event_.gap.scan_result.rssi = p->scan_rst.rssi;
 | 
			
		||||
        this->event_.gap.scan_result.adv_data_len = p->scan_rst.adv_data_len;
 | 
			
		||||
        this->event_.gap.scan_result.scan_rsp_len = p->scan_rst.scan_rsp_len;
 | 
			
		||||
        this->event_.gap.scan_result.search_evt = p->scan_rst.search_evt;
 | 
			
		||||
        memcpy(this->event_.gap.scan_result.ble_adv, p->scan_rst.ble_adv,
 | 
			
		||||
               ESP_BLE_ADV_DATA_LEN_MAX + ESP_BLE_SCAN_RSP_DATA_LEN_MAX);
 | 
			
		||||
        break;
 | 
			
		||||
 | 
			
		||||
      case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT:
 | 
			
		||||
        this->event_.gap.scan_complete.status = p->scan_param_cmpl.status;
 | 
			
		||||
        break;
 | 
			
		||||
 | 
			
		||||
      case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT:
 | 
			
		||||
        this->event_.gap.scan_complete.status = p->scan_start_cmpl.status;
 | 
			
		||||
        break;
 | 
			
		||||
 | 
			
		||||
      case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT:
 | 
			
		||||
        this->event_.gap.scan_complete.status = p->scan_stop_cmpl.status;
 | 
			
		||||
        break;
 | 
			
		||||
 | 
			
		||||
      // Advertising complete events - all have same structure with just status
 | 
			
		||||
      // Used by: esp32_ble_beacon, esp32_ble server components
 | 
			
		||||
      case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT:
 | 
			
		||||
        this->event_.gap.adv_complete.status = p->adv_data_cmpl.status;
 | 
			
		||||
        break;
 | 
			
		||||
      case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT:
 | 
			
		||||
        this->event_.gap.adv_complete.status = p->scan_rsp_data_cmpl.status;
 | 
			
		||||
        break;
 | 
			
		||||
      case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT:  // Used by: esp32_ble_beacon
 | 
			
		||||
        this->event_.gap.adv_complete.status = p->adv_data_raw_cmpl.status;
 | 
			
		||||
        break;
 | 
			
		||||
      case ESP_GAP_BLE_ADV_START_COMPLETE_EVT:  // Used by: esp32_ble_beacon
 | 
			
		||||
        this->event_.gap.adv_complete.status = p->adv_start_cmpl.status;
 | 
			
		||||
        break;
 | 
			
		||||
      case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT:  // Used by: esp32_ble_beacon
 | 
			
		||||
        this->event_.gap.adv_complete.status = p->adv_stop_cmpl.status;
 | 
			
		||||
        break;
 | 
			
		||||
 | 
			
		||||
      // RSSI complete event
 | 
			
		||||
      // Used by: ble_client (ble_rssi_sensor)
 | 
			
		||||
      case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT:
 | 
			
		||||
        this->event_.gap.read_rssi_complete.status = p->read_rssi_cmpl.status;
 | 
			
		||||
        this->event_.gap.read_rssi_complete.rssi = p->read_rssi_cmpl.rssi;
 | 
			
		||||
        memcpy(this->event_.gap.read_rssi_complete.remote_addr, p->read_rssi_cmpl.remote_addr, sizeof(esp_bd_addr_t));
 | 
			
		||||
        break;
 | 
			
		||||
 | 
			
		||||
      // Security events - copy the entire security union
 | 
			
		||||
      // Used by: ble_client, bluetooth_proxy, esp32_ble_client
 | 
			
		||||
      case ESP_GAP_BLE_AUTH_CMPL_EVT:      // Used by: bluetooth_proxy, esp32_ble_client
 | 
			
		||||
      case ESP_GAP_BLE_SEC_REQ_EVT:        // Used by: esp32_ble_client
 | 
			
		||||
      case ESP_GAP_BLE_PASSKEY_NOTIF_EVT:  // Used by: ble_client automation
 | 
			
		||||
      case ESP_GAP_BLE_PASSKEY_REQ_EVT:    // Used by: ble_client automation
 | 
			
		||||
      case ESP_GAP_BLE_NC_REQ_EVT:         // Used by: ble_client automation
 | 
			
		||||
        memcpy(&this->event_.gap.security, &p->ble_security, sizeof(esp_ble_sec_t));
 | 
			
		||||
        break;
 | 
			
		||||
 | 
			
		||||
      default:
 | 
			
		||||
        // We only store data for GAP events that components currently use
 | 
			
		||||
        // Unknown events still get queued and logged in ble.cpp:375 as
 | 
			
		||||
        // "Unhandled GAP event type in loop" - this helps identify new events
 | 
			
		||||
        // that components might need in the future
 | 
			
		||||
        break;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Initialize GATTC event data
 | 
			
		||||
  void init_gattc_data_(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) {
 | 
			
		||||
    this->event_.gattc.gattc_event = e;
 | 
			
		||||
    this->event_.gattc.gattc_if = i;
 | 
			
		||||
 | 
			
		||||
    if (p == nullptr) {
 | 
			
		||||
      this->event_.gattc.gattc_param = nullptr;
 | 
			
		||||
      this->event_.gattc.data = nullptr;
 | 
			
		||||
      return;  // Invalid event, but we can't log in header file
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Heap-allocate param and data
 | 
			
		||||
    // Heap allocation is used because GATTC/GATTS events are rare (<1% of events)
 | 
			
		||||
    // while GAP events (99%) are stored inline to minimize memory usage
 | 
			
		||||
    // IMPORTANT: This heap allocation provides clear ownership semantics:
 | 
			
		||||
    // - The BLEEvent owns the allocated memory for its lifetime
 | 
			
		||||
    // - The data remains valid from the BLE callback context until processed in the main loop
 | 
			
		||||
    // - Without this copy, we'd have use-after-free bugs as ESP-IDF reuses the callback memory
 | 
			
		||||
    this->event_.gattc.gattc_param = new esp_ble_gattc_cb_param_t(*p);
 | 
			
		||||
 | 
			
		||||
    // Copy data for events that need it
 | 
			
		||||
    // The param struct contains pointers (e.g., notify.value) that point to temporary buffers.
 | 
			
		||||
    // We must copy this data to ensure it remains valid when the event is processed later.
 | 
			
		||||
    switch (e) {
 | 
			
		||||
      case ESP_GATTC_NOTIFY_EVT:
 | 
			
		||||
        this->event_.gattc.data = new std::vector<uint8_t>(p->notify.value, p->notify.value + p->notify.value_len);
 | 
			
		||||
        this->event_.gattc.gattc_param->notify.value = this->event_.gattc.data->data();
 | 
			
		||||
        break;
 | 
			
		||||
      case ESP_GATTC_READ_CHAR_EVT:
 | 
			
		||||
      case ESP_GATTC_READ_DESCR_EVT:
 | 
			
		||||
        this->event_.gattc.data = new std::vector<uint8_t>(p->read.value, p->read.value + p->read.value_len);
 | 
			
		||||
        this->event_.gattc.gattc_param->read.value = this->event_.gattc.data->data();
 | 
			
		||||
        break;
 | 
			
		||||
      default:
 | 
			
		||||
        this->event_.gattc.data = nullptr;
 | 
			
		||||
        break;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Initialize GATTS event data
 | 
			
		||||
  void init_gatts_data_(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) {
 | 
			
		||||
    this->event_.gatts.gatts_event = e;
 | 
			
		||||
    this->event_.gatts.gatts_if = i;
 | 
			
		||||
 | 
			
		||||
    if (p == nullptr) {
 | 
			
		||||
      this->event_.gatts.gatts_param = nullptr;
 | 
			
		||||
      this->event_.gatts.data = nullptr;
 | 
			
		||||
      return;  // Invalid event, but we can't log in header file
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Heap-allocate param and data
 | 
			
		||||
    // Heap allocation is used because GATTC/GATTS events are rare (<1% of events)
 | 
			
		||||
    // while GAP events (99%) are stored inline to minimize memory usage
 | 
			
		||||
    // IMPORTANT: This heap allocation provides clear ownership semantics:
 | 
			
		||||
    // - The BLEEvent owns the allocated memory for its lifetime
 | 
			
		||||
    // - The data remains valid from the BLE callback context until processed in the main loop
 | 
			
		||||
    // - Without this copy, we'd have use-after-free bugs as ESP-IDF reuses the callback memory
 | 
			
		||||
    this->event_.gatts.gatts_param = new esp_ble_gatts_cb_param_t(*p);
 | 
			
		||||
 | 
			
		||||
    // Copy data for events that need it
 | 
			
		||||
    // The param struct contains pointers (e.g., write.value) that point to temporary buffers.
 | 
			
		||||
    // We must copy this data to ensure it remains valid when the event is processed later.
 | 
			
		||||
    switch (e) {
 | 
			
		||||
      case ESP_GATTS_WRITE_EVT:
 | 
			
		||||
        this->event_.gatts.data = new std::vector<uint8_t>(p->write.value, p->write.value + p->write.len);
 | 
			
		||||
        this->event_.gatts.gatts_param->write.value = this->event_.gatts.data->data();
 | 
			
		||||
        break;
 | 
			
		||||
      default:
 | 
			
		||||
        this->event_.gatts.data = nullptr;
 | 
			
		||||
        break;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Verify the gap_event struct hasn't grown beyond expected size
 | 
			
		||||
// The gap member in the union should be 80 bytes (including the gap_event enum)
 | 
			
		||||
static_assert(sizeof(decltype(((BLEEvent *) nullptr)->event_.gap)) <= 80, "gap_event struct has grown beyond 80 bytes");
 | 
			
		||||
 | 
			
		||||
// Verify esp_ble_sec_t fits within our union
 | 
			
		||||
static_assert(sizeof(esp_ble_sec_t) <= 73, "esp_ble_sec_t is larger than BLEScanResult");
 | 
			
		||||
 | 
			
		||||
// BLEEvent total size: 84 bytes (80 byte union + 1 byte type + 3 bytes padding)
 | 
			
		||||
 | 
			
		||||
}  // namespace esp32_ble
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										72
									
								
								esphome/components/esp32_ble/ble_event_pool.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								esphome/components/esp32_ble/ble_event_pool.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,72 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
 | 
			
		||||
#include <atomic>
 | 
			
		||||
#include <cstddef>
 | 
			
		||||
#include "ble_event.h"
 | 
			
		||||
#include "queue.h"
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace esp32_ble {
 | 
			
		||||
 | 
			
		||||
// BLE Event Pool - On-demand pool of BLEEvent objects to avoid heap fragmentation
 | 
			
		||||
// Events are allocated on first use and reused thereafter, growing to peak usage
 | 
			
		||||
template<uint8_t SIZE> class BLEEventPool {
 | 
			
		||||
 public:
 | 
			
		||||
  BLEEventPool() : total_created_(0) {}
 | 
			
		||||
 | 
			
		||||
  ~BLEEventPool() {
 | 
			
		||||
    // Clean up any remaining events in the free list
 | 
			
		||||
    BLEEvent *event;
 | 
			
		||||
    while ((event = this->free_list_.pop()) != nullptr) {
 | 
			
		||||
      delete event;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Allocate an event from the pool
 | 
			
		||||
  // Returns nullptr if pool is full
 | 
			
		||||
  BLEEvent *allocate() {
 | 
			
		||||
    // Try to get from free list first
 | 
			
		||||
    BLEEvent *event = this->free_list_.pop();
 | 
			
		||||
    if (event != nullptr)
 | 
			
		||||
      return event;
 | 
			
		||||
 | 
			
		||||
    // Need to create a new event
 | 
			
		||||
    if (this->total_created_ >= SIZE) {
 | 
			
		||||
      // Pool is at capacity
 | 
			
		||||
      return nullptr;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Use internal RAM for better performance
 | 
			
		||||
    RAMAllocator<BLEEvent> allocator(RAMAllocator<BLEEvent>::ALLOC_INTERNAL);
 | 
			
		||||
    event = allocator.allocate(1);
 | 
			
		||||
 | 
			
		||||
    if (event == nullptr) {
 | 
			
		||||
      // Memory allocation failed
 | 
			
		||||
      return nullptr;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Placement new to construct the object
 | 
			
		||||
    new (event) BLEEvent();
 | 
			
		||||
    this->total_created_++;
 | 
			
		||||
    return event;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Return an event to the pool for reuse
 | 
			
		||||
  void release(BLEEvent *event) {
 | 
			
		||||
    if (event != nullptr) {
 | 
			
		||||
      this->free_list_.push(event);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 private:
 | 
			
		||||
  LockFreeQueue<BLEEvent, SIZE> free_list_;  // Free events ready for reuse
 | 
			
		||||
  uint8_t total_created_;                    // Total events created (high water mark)
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace esp32_ble
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
@@ -18,7 +18,7 @@
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace esp32_ble {
 | 
			
		||||
 | 
			
		||||
template<class T, size_t SIZE> class LockFreeQueue {
 | 
			
		||||
template<class T, uint8_t SIZE> class LockFreeQueue {
 | 
			
		||||
 public:
 | 
			
		||||
  LockFreeQueue() : head_(0), tail_(0), dropped_count_(0) {}
 | 
			
		||||
 | 
			
		||||
@@ -26,8 +26,8 @@ template<class T, size_t SIZE> class LockFreeQueue {
 | 
			
		||||
    if (element == nullptr)
 | 
			
		||||
      return false;
 | 
			
		||||
 | 
			
		||||
    size_t current_tail = tail_.load(std::memory_order_relaxed);
 | 
			
		||||
    size_t next_tail = (current_tail + 1) % SIZE;
 | 
			
		||||
    uint8_t current_tail = tail_.load(std::memory_order_relaxed);
 | 
			
		||||
    uint8_t next_tail = (current_tail + 1) % SIZE;
 | 
			
		||||
 | 
			
		||||
    if (next_tail == head_.load(std::memory_order_acquire)) {
 | 
			
		||||
      // Buffer full
 | 
			
		||||
@@ -41,7 +41,7 @@ template<class T, size_t SIZE> class LockFreeQueue {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  T *pop() {
 | 
			
		||||
    size_t current_head = head_.load(std::memory_order_relaxed);
 | 
			
		||||
    uint8_t current_head = head_.load(std::memory_order_relaxed);
 | 
			
		||||
 | 
			
		||||
    if (current_head == tail_.load(std::memory_order_acquire)) {
 | 
			
		||||
      return nullptr;  // Empty
 | 
			
		||||
@@ -53,27 +53,30 @@ template<class T, size_t SIZE> class LockFreeQueue {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  size_t size() const {
 | 
			
		||||
    size_t tail = tail_.load(std::memory_order_acquire);
 | 
			
		||||
    size_t head = head_.load(std::memory_order_acquire);
 | 
			
		||||
    uint8_t tail = tail_.load(std::memory_order_acquire);
 | 
			
		||||
    uint8_t head = head_.load(std::memory_order_acquire);
 | 
			
		||||
    return (tail - head + SIZE) % SIZE;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  size_t get_and_reset_dropped_count() { return dropped_count_.exchange(0, std::memory_order_relaxed); }
 | 
			
		||||
  uint16_t get_and_reset_dropped_count() { return dropped_count_.exchange(0, std::memory_order_relaxed); }
 | 
			
		||||
 | 
			
		||||
  void increment_dropped_count() { dropped_count_.fetch_add(1, std::memory_order_relaxed); }
 | 
			
		||||
 | 
			
		||||
  bool empty() const { return head_.load(std::memory_order_acquire) == tail_.load(std::memory_order_acquire); }
 | 
			
		||||
 | 
			
		||||
  bool full() const {
 | 
			
		||||
    size_t next_tail = (tail_.load(std::memory_order_relaxed) + 1) % SIZE;
 | 
			
		||||
    uint8_t next_tail = (tail_.load(std::memory_order_relaxed) + 1) % SIZE;
 | 
			
		||||
    return next_tail == head_.load(std::memory_order_acquire);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  T *buffer_[SIZE];
 | 
			
		||||
  std::atomic<size_t> head_;
 | 
			
		||||
  std::atomic<size_t> tail_;
 | 
			
		||||
  std::atomic<size_t> dropped_count_;
 | 
			
		||||
  // Atomic: written by producer (push/increment), read+reset by consumer (get_and_reset)
 | 
			
		||||
  std::atomic<uint16_t> dropped_count_;  // 65535 max - more than enough for drop tracking
 | 
			
		||||
  // Atomic: written by consumer (pop), read by producer (push) to check if full
 | 
			
		||||
  std::atomic<uint8_t> head_;
 | 
			
		||||
  // Atomic: written by producer (push), read by consumer (pop) to check if empty
 | 
			
		||||
  std::atomic<uint8_t> tail_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace esp32_ble
 | 
			
		||||
 
 | 
			
		||||
@@ -22,6 +22,16 @@ void BLEClientBase::setup() {
 | 
			
		||||
  this->connection_index_ = connection_index++;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BLEClientBase::set_state(espbt::ClientState st) {
 | 
			
		||||
  ESP_LOGV(TAG, "[%d] [%s] Set state %d", this->connection_index_, this->address_str_.c_str(), (int) st);
 | 
			
		||||
  ESPBTClient::set_state(st);
 | 
			
		||||
 | 
			
		||||
  if (st == espbt::ClientState::READY_TO_CONNECT) {
 | 
			
		||||
    // Enable loop when we need to connect
 | 
			
		||||
    this->enable_loop();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BLEClientBase::loop() {
 | 
			
		||||
  if (!esp32_ble::global_ble->is_active()) {
 | 
			
		||||
    this->set_state(espbt::ClientState::INIT);
 | 
			
		||||
@@ -37,9 +47,14 @@ void BLEClientBase::loop() {
 | 
			
		||||
  }
 | 
			
		||||
  // READY_TO_CONNECT means we have discovered the device
 | 
			
		||||
  // and the scanner has been stopped by the tracker.
 | 
			
		||||
  if (this->state_ == espbt::ClientState::READY_TO_CONNECT) {
 | 
			
		||||
  else if (this->state_ == espbt::ClientState::READY_TO_CONNECT) {
 | 
			
		||||
    this->connect();
 | 
			
		||||
  }
 | 
			
		||||
  // If its idle, we can disable the loop as set_state
 | 
			
		||||
  // will enable it again when we need to connect.
 | 
			
		||||
  else if (this->state_ == espbt::ClientState::IDLE) {
 | 
			
		||||
    this->disable_loop();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
float BLEClientBase::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; }
 | 
			
		||||
 
 | 
			
		||||
@@ -93,22 +93,37 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
 | 
			
		||||
 | 
			
		||||
  bool check_addr(esp_bd_addr_t &addr) { return memcmp(addr, this->remote_bda_, sizeof(esp_bd_addr_t)) == 0; }
 | 
			
		||||
 | 
			
		||||
  void set_state(espbt::ClientState st) override;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  int gattc_if_;
 | 
			
		||||
  esp_bd_addr_t remote_bda_;
 | 
			
		||||
  esp_ble_addr_type_t remote_addr_type_{BLE_ADDR_TYPE_PUBLIC};
 | 
			
		||||
  uint16_t conn_id_{UNSET_CONN_ID};
 | 
			
		||||
  // Memory optimized layout for 32-bit systems
 | 
			
		||||
  // Group 1: 8-byte types
 | 
			
		||||
  uint64_t address_{0};
 | 
			
		||||
  bool auto_connect_{false};
 | 
			
		||||
 | 
			
		||||
  // Group 2: Container types (grouped for memory optimization)
 | 
			
		||||
  std::string address_str_{};
 | 
			
		||||
  uint8_t connection_index_;
 | 
			
		||||
  int16_t service_count_{0};
 | 
			
		||||
  uint16_t mtu_{23};
 | 
			
		||||
  bool paired_{false};
 | 
			
		||||
  espbt::ConnectionType connection_type_{espbt::ConnectionType::V1};
 | 
			
		||||
  std::vector<BLEService *> services_;
 | 
			
		||||
 | 
			
		||||
  // Group 3: 4-byte types
 | 
			
		||||
  int gattc_if_;
 | 
			
		||||
  esp_gatt_status_t status_{ESP_GATT_OK};
 | 
			
		||||
 | 
			
		||||
  // Group 4: Arrays (6 bytes)
 | 
			
		||||
  esp_bd_addr_t remote_bda_;
 | 
			
		||||
 | 
			
		||||
  // Group 5: 2-byte types
 | 
			
		||||
  uint16_t conn_id_{UNSET_CONN_ID};
 | 
			
		||||
  uint16_t mtu_{23};
 | 
			
		||||
 | 
			
		||||
  // Group 6: 1-byte types and small enums
 | 
			
		||||
  esp_ble_addr_type_t remote_addr_type_{BLE_ADDR_TYPE_PUBLIC};
 | 
			
		||||
  espbt::ConnectionType connection_type_{espbt::ConnectionType::V1};
 | 
			
		||||
  uint8_t connection_index_;
 | 
			
		||||
  uint8_t service_count_{0};  // ESP32 has max handles < 255, typical devices have < 50 services
 | 
			
		||||
  bool auto_connect_{false};
 | 
			
		||||
  bool paired_{false};
 | 
			
		||||
  // 6 bytes used, 2 bytes padding
 | 
			
		||||
 | 
			
		||||
  void log_event_(const char *name);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -122,10 +122,10 @@ void ESP32BLETracker::loop() {
 | 
			
		||||
  // Consumer side: This runs in the main loop thread
 | 
			
		||||
  if (this->scanner_state_ == ScannerState::RUNNING) {
 | 
			
		||||
    // Load our own index with relaxed ordering (we're the only writer)
 | 
			
		||||
    size_t read_idx = this->ring_read_index_.load(std::memory_order_relaxed);
 | 
			
		||||
    uint8_t read_idx = this->ring_read_index_.load(std::memory_order_relaxed);
 | 
			
		||||
 | 
			
		||||
    // Load producer's index with acquire to see their latest writes
 | 
			
		||||
    size_t write_idx = this->ring_write_index_.load(std::memory_order_acquire);
 | 
			
		||||
    uint8_t write_idx = this->ring_write_index_.load(std::memory_order_acquire);
 | 
			
		||||
 | 
			
		||||
    while (read_idx != write_idx) {
 | 
			
		||||
      // Process one result at a time directly from ring buffer
 | 
			
		||||
@@ -409,11 +409,11 @@ void ESP32BLETracker::gap_scan_event_handler(const BLEScanResult &scan_result) {
 | 
			
		||||
    // IMPORTANT: Only this thread writes to ring_write_index_
 | 
			
		||||
 | 
			
		||||
    // Load our own index with relaxed ordering (we're the only writer)
 | 
			
		||||
    size_t write_idx = this->ring_write_index_.load(std::memory_order_relaxed);
 | 
			
		||||
    size_t next_write_idx = (write_idx + 1) % SCAN_RESULT_BUFFER_SIZE;
 | 
			
		||||
    uint8_t write_idx = this->ring_write_index_.load(std::memory_order_relaxed);
 | 
			
		||||
    uint8_t next_write_idx = (write_idx + 1) % SCAN_RESULT_BUFFER_SIZE;
 | 
			
		||||
 | 
			
		||||
    // Load consumer's index with acquire to see their latest updates
 | 
			
		||||
    size_t read_idx = this->ring_read_index_.load(std::memory_order_acquire);
 | 
			
		||||
    uint8_t read_idx = this->ring_read_index_.load(std::memory_order_acquire);
 | 
			
		||||
 | 
			
		||||
    // Check if buffer is full
 | 
			
		||||
    if (next_write_idx != read_idx) {
 | 
			
		||||
@@ -522,6 +522,7 @@ optional<ESPBLEiBeacon> ESPBLEiBeacon::from_manufacturer_data(const ServiceData
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ESPBTDevice::parse_scan_rst(const BLEScanResult &scan_result) {
 | 
			
		||||
  this->scan_result_ = &scan_result;
 | 
			
		||||
  for (uint8_t i = 0; i < ESP_BD_ADDR_LEN; i++)
 | 
			
		||||
    this->address_[i] = scan_result.bda[i];
 | 
			
		||||
  this->address_type_ = static_cast<esp_ble_addr_type_t>(scan_result.ble_addr_type);
 | 
			
		||||
 
 | 
			
		||||
@@ -85,6 +85,9 @@ class ESPBTDevice {
 | 
			
		||||
 | 
			
		||||
  const std::vector<ServiceData> &get_service_datas() const { return service_datas_; }
 | 
			
		||||
 | 
			
		||||
  // Exposed through a function for use in lambdas
 | 
			
		||||
  const BLEScanResult &get_scan_result() const { return *scan_result_; }
 | 
			
		||||
 | 
			
		||||
  bool resolve_irk(const uint8_t *irk) const;
 | 
			
		||||
 | 
			
		||||
  optional<ESPBLEiBeacon> get_ibeacon() const {
 | 
			
		||||
@@ -111,6 +114,7 @@ class ESPBTDevice {
 | 
			
		||||
  std::vector<ESPBTUUID> service_uuids_{};
 | 
			
		||||
  std::vector<ServiceData> manufacturer_datas_{};
 | 
			
		||||
  std::vector<ServiceData> service_datas_{};
 | 
			
		||||
  const BLEScanResult *scan_result_{nullptr};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class ESP32BLETracker;
 | 
			
		||||
@@ -129,7 +133,7 @@ class ESPBTDeviceListener {
 | 
			
		||||
  ESP32BLETracker *parent_{nullptr};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
enum class ClientState {
 | 
			
		||||
enum class ClientState : uint8_t {
 | 
			
		||||
  // Connection is allocated
 | 
			
		||||
  INIT,
 | 
			
		||||
  // Client is disconnecting
 | 
			
		||||
@@ -165,7 +169,7 @@ enum class ScannerState {
 | 
			
		||||
  STOPPED,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
enum class ConnectionType {
 | 
			
		||||
enum class ConnectionType : uint8_t {
 | 
			
		||||
  // The default connection type, we hold all the services in ram
 | 
			
		||||
  // for the duration of the connection.
 | 
			
		||||
  V1,
 | 
			
		||||
@@ -193,15 +197,19 @@ class ESPBTClient : public ESPBTDeviceListener {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  ClientState state() const { return state_; }
 | 
			
		||||
  int app_id;
 | 
			
		||||
 | 
			
		||||
  // Memory optimized layout
 | 
			
		||||
  uint8_t app_id;  // App IDs are small integers assigned sequentially
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  // Group 1: 1-byte types
 | 
			
		||||
  ClientState state_{ClientState::INIT};
 | 
			
		||||
  // want_disconnect_ is set to true when a disconnect is requested
 | 
			
		||||
  // while the client is connecting. This is used to disconnect the
 | 
			
		||||
  // client as soon as we get the connection id (conn_id_) from the
 | 
			
		||||
  // ESP_GATTC_OPEN_EVT event.
 | 
			
		||||
  bool want_disconnect_{false};
 | 
			
		||||
  // 2 bytes used, 2 bytes padding
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class ESP32BLETracker : public Component,
 | 
			
		||||
@@ -262,7 +270,7 @@ class ESP32BLETracker : public Component,
 | 
			
		||||
  /// Called to set the scanner state. Will also call callbacks to let listeners know when state is changed.
 | 
			
		||||
  void set_scanner_state_(ScannerState state);
 | 
			
		||||
 | 
			
		||||
  int app_id_{0};
 | 
			
		||||
  uint8_t app_id_{0};
 | 
			
		||||
 | 
			
		||||
  /// Vector of addresses that have already been printed in print_bt_device_info
 | 
			
		||||
  std::vector<uint64_t> already_discovered_;
 | 
			
		||||
@@ -289,9 +297,9 @@ class ESP32BLETracker : public Component,
 | 
			
		||||
  // Consumer: ESPHome main loop (loop() method)
 | 
			
		||||
  // This design ensures zero blocking in the BT callback and prevents scan result loss
 | 
			
		||||
  BLEScanResult *scan_ring_buffer_;
 | 
			
		||||
  std::atomic<size_t> ring_write_index_{0};      // Written only by BT callback (producer)
 | 
			
		||||
  std::atomic<size_t> ring_read_index_{0};       // Written only by main loop (consumer)
 | 
			
		||||
  std::atomic<size_t> scan_results_dropped_{0};  // Tracks buffer overflow events
 | 
			
		||||
  std::atomic<uint8_t> ring_write_index_{0};       // Written only by BT callback (producer)
 | 
			
		||||
  std::atomic<uint8_t> ring_read_index_{0};        // Written only by main loop (consumer)
 | 
			
		||||
  std::atomic<uint16_t> scan_results_dropped_{0};  // Tracks buffer overflow events
 | 
			
		||||
 | 
			
		||||
  esp_bt_status_t scan_start_failed_{ESP_BT_STATUS_SUCCESS};
 | 
			
		||||
  esp_bt_status_t scan_set_param_failed_{ESP_BT_STATUS_SUCCESS};
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
from esphome import automation, pins
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import i2c
 | 
			
		||||
from esphome.components.esp32 import add_idf_component
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
@@ -7,6 +8,7 @@ from esphome.const import (
 | 
			
		||||
    CONF_CONTRAST,
 | 
			
		||||
    CONF_DATA_PINS,
 | 
			
		||||
    CONF_FREQUENCY,
 | 
			
		||||
    CONF_I2C_ID,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_PIN,
 | 
			
		||||
    CONF_RESET_PIN,
 | 
			
		||||
@@ -17,7 +19,7 @@ from esphome.const import (
 | 
			
		||||
    CONF_VSYNC_PIN,
 | 
			
		||||
)
 | 
			
		||||
from esphome.core import CORE
 | 
			
		||||
from esphome.cpp_helpers import setup_entity
 | 
			
		||||
from esphome.core.entity_helpers import setup_entity
 | 
			
		||||
 | 
			
		||||
DEPENDENCIES = ["esp32"]
 | 
			
		||||
 | 
			
		||||
@@ -149,7 +151,8 @@ CONF_ON_IMAGE = "on_image"
 | 
			
		||||
 | 
			
		||||
camera_range_param = cv.int_range(min=-2, max=2)
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(
 | 
			
		||||
CONFIG_SCHEMA = cv.All(
 | 
			
		||||
    cv.ENTITY_BASE_SCHEMA.extend(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(ESP32Camera),
 | 
			
		||||
            # pin assignment
 | 
			
		||||
@@ -167,12 +170,16 @@ CONFIG_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(
 | 
			
		||||
                    ),
 | 
			
		||||
                }
 | 
			
		||||
            ),
 | 
			
		||||
        cv.Required(CONF_I2C_PINS): cv.Schema(
 | 
			
		||||
            cv.Optional(CONF_I2C_PINS): cv.Schema(
 | 
			
		||||
                {
 | 
			
		||||
                    cv.Required(CONF_SDA): pins.internal_gpio_output_pin_number,
 | 
			
		||||
                    cv.Required(CONF_SCL): pins.internal_gpio_output_pin_number,
 | 
			
		||||
                }
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_I2C_ID): cv.Any(
 | 
			
		||||
                cv.use_id(i2c.InternalI2CBus),
 | 
			
		||||
                msg="I2C bus must be an internal ESP32 I2C bus",
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_RESET_PIN): pins.internal_gpio_output_pin_number,
 | 
			
		||||
            cv.Optional(CONF_POWER_DOWN_PIN): pins.internal_gpio_output_pin_number,
 | 
			
		||||
            # image
 | 
			
		||||
@@ -204,7 +211,9 @@ CONFIG_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(
 | 
			
		||||
                ENUM_GAIN_CEILING, upper=True
 | 
			
		||||
            ),
 | 
			
		||||
            # white balance
 | 
			
		||||
        cv.Optional(CONF_WB_MODE, default="AUTO"): cv.enum(ENUM_WB_MODE, upper=True),
 | 
			
		||||
            cv.Optional(CONF_WB_MODE, default="AUTO"): cv.enum(
 | 
			
		||||
                ENUM_WB_MODE, upper=True
 | 
			
		||||
            ),
 | 
			
		||||
            # test pattern
 | 
			
		||||
            cv.Optional(CONF_TEST_PATTERN, default=False): cv.boolean,
 | 
			
		||||
            # framerates
 | 
			
		||||
@@ -231,11 +240,15 @@ CONFIG_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_ON_IMAGE): automation.validate_automation(
 | 
			
		||||
                {
 | 
			
		||||
                cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ESP32CameraImageTrigger),
 | 
			
		||||
                    cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
 | 
			
		||||
                        ESP32CameraImageTrigger
 | 
			
		||||
                    ),
 | 
			
		||||
                }
 | 
			
		||||
            ),
 | 
			
		||||
        }
 | 
			
		||||
).extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
    ).extend(cv.COMPONENT_SCHEMA),
 | 
			
		||||
    cv.has_exactly_one_key(CONF_I2C_PINS, CONF_I2C_ID),
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
SETTERS = {
 | 
			
		||||
    # pin assignment
 | 
			
		||||
@@ -271,7 +284,7 @@ SETTERS = {
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await setup_entity(var, config)
 | 
			
		||||
    await setup_entity(var, config, "camera")
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
 | 
			
		||||
    for key, setter in SETTERS.items():
 | 
			
		||||
@@ -280,6 +293,10 @@ async def to_code(config):
 | 
			
		||||
 | 
			
		||||
    extclk = config[CONF_EXTERNAL_CLOCK]
 | 
			
		||||
    cg.add(var.set_external_clock(extclk[CONF_PIN], extclk[CONF_FREQUENCY]))
 | 
			
		||||
    if i2c_id := config.get(CONF_I2C_ID):
 | 
			
		||||
        i2c_hub = await cg.get_variable(i2c_id)
 | 
			
		||||
        cg.add(var.set_i2c_id(i2c_hub))
 | 
			
		||||
    else:
 | 
			
		||||
        i2c_pins = config[CONF_I2C_PINS]
 | 
			
		||||
        cg.add(var.set_i2c_pins(i2c_pins[CONF_SDA], i2c_pins[CONF_SCL]))
 | 
			
		||||
    cg.add(var.set_max_update_interval(1000 / config[CONF_MAX_FRAMERATE]))
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,9 @@
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
 | 
			
		||||
#include "esp32_camera.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include "esphome/core/hal.h"
 | 
			
		||||
#include "esphome/core/application.h"
 | 
			
		||||
#include "esphome/core/hal.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
#include <freertos/task.h>
 | 
			
		||||
 | 
			
		||||
@@ -16,6 +16,12 @@ static const char *const TAG = "esp32_camera";
 | 
			
		||||
void ESP32Camera::setup() {
 | 
			
		||||
  global_esp32_camera = this;
 | 
			
		||||
 | 
			
		||||
#ifdef USE_I2C
 | 
			
		||||
  if (this->i2c_bus_ != nullptr) {
 | 
			
		||||
    this->config_.sccb_i2c_port = this->i2c_bus_->get_port();
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  /* initialize time to now */
 | 
			
		||||
  this->last_update_ = millis();
 | 
			
		||||
 | 
			
		||||
@@ -246,6 +252,13 @@ void ESP32Camera::set_i2c_pins(uint8_t sda, uint8_t scl) {
 | 
			
		||||
  this->config_.pin_sccb_sda = sda;
 | 
			
		||||
  this->config_.pin_sccb_scl = scl;
 | 
			
		||||
}
 | 
			
		||||
#ifdef USE_I2C
 | 
			
		||||
void ESP32Camera::set_i2c_id(i2c::InternalI2CBus *i2c_bus) {
 | 
			
		||||
  this->i2c_bus_ = i2c_bus;
 | 
			
		||||
  this->config_.pin_sccb_sda = -1;
 | 
			
		||||
  this->config_.pin_sccb_scl = -1;
 | 
			
		||||
}
 | 
			
		||||
#endif  // USE_I2C
 | 
			
		||||
void ESP32Camera::set_reset_pin(uint8_t pin) { this->config_.pin_reset = pin; }
 | 
			
		||||
void ESP32Camera::set_power_down_pin(uint8_t pin) { this->config_.pin_pwdn = pin; }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -2,13 +2,17 @@
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
 | 
			
		||||
#include <esp_camera.h>
 | 
			
		||||
#include <freertos/FreeRTOS.h>
 | 
			
		||||
#include <freertos/queue.h>
 | 
			
		||||
#include "esphome/core/automation.h"
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/core/entity_base.h"
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
#include <esp_camera.h>
 | 
			
		||||
#include <freertos/FreeRTOS.h>
 | 
			
		||||
#include <freertos/queue.h>
 | 
			
		||||
 | 
			
		||||
#ifdef USE_I2C
 | 
			
		||||
#include "esphome/components/i2c/i2c_bus.h"
 | 
			
		||||
#endif  // USE_I2C
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace esp32_camera {
 | 
			
		||||
@@ -118,6 +122,9 @@ class ESP32Camera : public EntityBase, public Component {
 | 
			
		||||
  void set_pixel_clock_pin(uint8_t pin);
 | 
			
		||||
  void set_external_clock(uint8_t pin, uint32_t frequency);
 | 
			
		||||
  void set_i2c_pins(uint8_t sda, uint8_t scl);
 | 
			
		||||
#ifdef USE_I2C
 | 
			
		||||
  void set_i2c_id(i2c::InternalI2CBus *i2c_bus);
 | 
			
		||||
#endif  // USE_I2C
 | 
			
		||||
  void set_reset_pin(uint8_t pin);
 | 
			
		||||
  void set_power_down_pin(uint8_t pin);
 | 
			
		||||
  /* -- image */
 | 
			
		||||
@@ -210,6 +217,9 @@ class ESP32Camera : public EntityBase, public Component {
 | 
			
		||||
 | 
			
		||||
  uint32_t last_idle_request_{0};
 | 
			
		||||
  uint32_t last_update_{0};
 | 
			
		||||
#ifdef USE_I2C
 | 
			
		||||
  i2c::InternalI2CBus *i2c_bus_{nullptr};
 | 
			
		||||
#endif  // USE_I2C
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,7 @@ import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import CONF_ID, CONF_MODE, CONF_PORT
 | 
			
		||||
 | 
			
		||||
CODEOWNERS = ["@ayufan"]
 | 
			
		||||
DEPENDENCIES = ["esp32_camera"]
 | 
			
		||||
DEPENDENCIES = ["esp32_camera", "network"]
 | 
			
		||||
MULTI_CONF = True
 | 
			
		||||
 | 
			
		||||
esp32_camera_web_server_ns = cg.esphome_ns.namespace("esp32_camera_web_server")
 | 
			
		||||
 
 | 
			
		||||
@@ -1,25 +0,0 @@
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
#include "esp32_hall.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include "esphome/core/hal.h"
 | 
			
		||||
#include <driver/adc.h>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace esp32_hall {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "esp32_hall";
 | 
			
		||||
 | 
			
		||||
void ESP32HallSensor::update() {
 | 
			
		||||
  adc1_config_width(ADC_WIDTH_BIT_12);
 | 
			
		||||
  int value_int = hall_sensor_read();
 | 
			
		||||
  float value = (value_int / 4095.0f) * 10000.0f;
 | 
			
		||||
  ESP_LOGD(TAG, "'%s': Got reading %.0f µT", this->name_.c_str(), value);
 | 
			
		||||
  this->publish_state(value);
 | 
			
		||||
}
 | 
			
		||||
std::string ESP32HallSensor::unique_id() { return get_mac_address() + "-hall"; }
 | 
			
		||||
void ESP32HallSensor::dump_config() { LOG_SENSOR("", "ESP32 Hall Sensor", this); }
 | 
			
		||||
 | 
			
		||||
}  // namespace esp32_hall
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
@@ -1,23 +0,0 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/components/sensor/sensor.h"
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace esp32_hall {
 | 
			
		||||
 | 
			
		||||
class ESP32HallSensor : public sensor::Sensor, public PollingComponent {
 | 
			
		||||
 public:
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
 | 
			
		||||
  void update() override;
 | 
			
		||||
 | 
			
		||||
  std::string unique_id() override;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace esp32_hall
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
@@ -1,24 +1,5 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import sensor
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import ICON_MAGNET, STATE_CLASS_MEASUREMENT, UNIT_MICROTESLA
 | 
			
		||||
 | 
			
		||||
DEPENDENCIES = ["esp32"]
 | 
			
		||||
 | 
			
		||||
esp32_hall_ns = cg.esphome_ns.namespace("esp32_hall")
 | 
			
		||||
ESP32HallSensor = esp32_hall_ns.class_(
 | 
			
		||||
    "ESP32HallSensor", sensor.Sensor, cg.PollingComponent
 | 
			
		||||
CONFIG_SCHEMA = cv.invalid(
 | 
			
		||||
    "The esp32_hall component has been removed as of ESPHome 2025.7.0. See https://github.com/esphome/esphome/pull/9117 for details."
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = sensor.sensor_schema(
 | 
			
		||||
    ESP32HallSensor,
 | 
			
		||||
    unit_of_measurement=UNIT_MICROTESLA,
 | 
			
		||||
    icon=ICON_MAGNET,
 | 
			
		||||
    accuracy_decimals=1,
 | 
			
		||||
    state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
).extend(cv.polling_component_schema("60s"))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = await sensor.new_sensor(config)
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
 
 | 
			
		||||
@@ -168,6 +168,8 @@ void ESP32ImprovComponent::loop() {
 | 
			
		||||
    case improv::STATE_PROVISIONED: {
 | 
			
		||||
      this->incoming_data_.clear();
 | 
			
		||||
      this->set_status_indicator_state_(false);
 | 
			
		||||
      // Provisioning complete, no further loop execution needed
 | 
			
		||||
      this->disable_loop();
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@@ -254,6 +256,7 @@ void ESP32ImprovComponent::start() {
 | 
			
		||||
 | 
			
		||||
  ESP_LOGD(TAG, "Setting Improv to start");
 | 
			
		||||
  this->should_start_ = true;
 | 
			
		||||
  this->enable_loop();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ESP32ImprovComponent::stop() {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,48 +1,8 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import esp32
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import KEY_CORE, KEY_FRAMEWORK_VERSION
 | 
			
		||||
from esphome.core import CORE
 | 
			
		||||
 | 
			
		||||
CODEOWNERS = ["@jesserockz"]
 | 
			
		||||
 | 
			
		||||
RMT_TX_CHANNELS = {
 | 
			
		||||
    esp32.const.VARIANT_ESP32: [0, 1, 2, 3, 4, 5, 6, 7],
 | 
			
		||||
    esp32.const.VARIANT_ESP32S2: [0, 1, 2, 3],
 | 
			
		||||
    esp32.const.VARIANT_ESP32S3: [0, 1, 2, 3],
 | 
			
		||||
    esp32.const.VARIANT_ESP32C3: [0, 1],
 | 
			
		||||
    esp32.const.VARIANT_ESP32C6: [0, 1],
 | 
			
		||||
    esp32.const.VARIANT_ESP32H2: [0, 1],
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
RMT_RX_CHANNELS = {
 | 
			
		||||
    esp32.const.VARIANT_ESP32: [0, 1, 2, 3, 4, 5, 6, 7],
 | 
			
		||||
    esp32.const.VARIANT_ESP32S2: [0, 1, 2, 3],
 | 
			
		||||
    esp32.const.VARIANT_ESP32S3: [4, 5, 6, 7],
 | 
			
		||||
    esp32.const.VARIANT_ESP32C3: [2, 3],
 | 
			
		||||
    esp32.const.VARIANT_ESP32C6: [2, 3],
 | 
			
		||||
    esp32.const.VARIANT_ESP32H2: [2, 3],
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
rmt_channel_t = cg.global_ns.enum("rmt_channel_t")
 | 
			
		||||
RMT_CHANNEL_ENUMS = {
 | 
			
		||||
    0: rmt_channel_t.RMT_CHANNEL_0,
 | 
			
		||||
    1: rmt_channel_t.RMT_CHANNEL_1,
 | 
			
		||||
    2: rmt_channel_t.RMT_CHANNEL_2,
 | 
			
		||||
    3: rmt_channel_t.RMT_CHANNEL_3,
 | 
			
		||||
    4: rmt_channel_t.RMT_CHANNEL_4,
 | 
			
		||||
    5: rmt_channel_t.RMT_CHANNEL_5,
 | 
			
		||||
    6: rmt_channel_t.RMT_CHANNEL_6,
 | 
			
		||||
    7: rmt_channel_t.RMT_CHANNEL_7,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def use_new_rmt_driver():
 | 
			
		||||
    framework_version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
 | 
			
		||||
    if CORE.using_esp_idf and framework_version >= cv.Version(5, 0, 0):
 | 
			
		||||
        return True
 | 
			
		||||
    return False
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def validate_clock_resolution():
 | 
			
		||||
    def _validator(value):
 | 
			
		||||
@@ -60,21 +20,3 @@ def validate_clock_resolution():
 | 
			
		||||
        return value
 | 
			
		||||
 | 
			
		||||
    return _validator
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def validate_rmt_channel(*, tx: bool):
 | 
			
		||||
    rmt_channels = RMT_TX_CHANNELS if tx else RMT_RX_CHANNELS
 | 
			
		||||
 | 
			
		||||
    def _validator(value):
 | 
			
		||||
        cv.only_on_esp32(value)
 | 
			
		||||
        value = cv.int_(value)
 | 
			
		||||
        variant = esp32.get_esp32_variant()
 | 
			
		||||
        if variant not in rmt_channels:
 | 
			
		||||
            raise cv.Invalid(f"ESP32 variant {variant} does not support RMT.")
 | 
			
		||||
        if value not in rmt_channels[variant]:
 | 
			
		||||
            raise cv.Invalid(
 | 
			
		||||
                f"RMT channel {value} does not support {'transmitting' if tx else 'receiving'} for ESP32 variant {variant}."
 | 
			
		||||
            )
 | 
			
		||||
        return cv.enum(RMT_CHANNEL_ENUMS)(value)
 | 
			
		||||
 | 
			
		||||
    return _validator
 | 
			
		||||
 
 | 
			
		||||
@@ -42,7 +42,6 @@ void ESP32RMTLEDStripLightOutput::setup() {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
#if ESP_IDF_VERSION_MAJOR >= 5
 | 
			
		||||
  RAMAllocator<rmt_symbol_word_t> rmt_allocator(this->use_psram_ ? 0 : RAMAllocator<rmt_symbol_word_t>::ALLOC_INTERNAL);
 | 
			
		||||
 | 
			
		||||
  // 8 bits per byte, 1 rmt_symbol_word_t per bit + 1 rmt_symbol_word_t for reset
 | 
			
		||||
@@ -79,36 +78,6 @@ void ESP32RMTLEDStripLightOutput::setup() {
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
#else
 | 
			
		||||
  RAMAllocator<rmt_item32_t> rmt_allocator(this->use_psram_ ? 0 : RAMAllocator<rmt_item32_t>::ALLOC_INTERNAL);
 | 
			
		||||
 | 
			
		||||
  // 8 bits per byte, 1 rmt_item32_t per bit + 1 rmt_item32_t for reset
 | 
			
		||||
  this->rmt_buf_ = rmt_allocator.allocate(buffer_size * 8 + 1);
 | 
			
		||||
 | 
			
		||||
  rmt_config_t config;
 | 
			
		||||
  memset(&config, 0, sizeof(config));
 | 
			
		||||
  config.channel = this->channel_;
 | 
			
		||||
  config.rmt_mode = RMT_MODE_TX;
 | 
			
		||||
  config.gpio_num = gpio_num_t(this->pin_);
 | 
			
		||||
  config.mem_block_num = 1;
 | 
			
		||||
  config.clk_div = RMT_CLK_DIV;
 | 
			
		||||
  config.tx_config.loop_en = false;
 | 
			
		||||
  config.tx_config.carrier_level = RMT_CARRIER_LEVEL_LOW;
 | 
			
		||||
  config.tx_config.carrier_en = false;
 | 
			
		||||
  config.tx_config.idle_level = RMT_IDLE_LEVEL_LOW;
 | 
			
		||||
  config.tx_config.idle_output_en = true;
 | 
			
		||||
 | 
			
		||||
  if (rmt_config(&config) != ESP_OK) {
 | 
			
		||||
    ESP_LOGE(TAG, "Cannot initialize RMT!");
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (rmt_driver_install(config.channel, 0, 0) != ESP_OK) {
 | 
			
		||||
    ESP_LOGE(TAG, "Cannot install RMT driver!");
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ESP32RMTLEDStripLightOutput::set_led_params(uint32_t bit0_high, uint32_t bit0_low, uint32_t bit1_high,
 | 
			
		||||
@@ -145,11 +114,7 @@ void ESP32RMTLEDStripLightOutput::write_state(light::LightState *state) {
 | 
			
		||||
 | 
			
		||||
  ESP_LOGVV(TAG, "Writing RGB values to bus");
 | 
			
		||||
 | 
			
		||||
#if ESP_IDF_VERSION_MAJOR >= 5
 | 
			
		||||
  esp_err_t error = rmt_tx_wait_all_done(this->channel_, 1000);
 | 
			
		||||
#else
 | 
			
		||||
  esp_err_t error = rmt_wait_tx_done(this->channel_, pdMS_TO_TICKS(1000));
 | 
			
		||||
#endif
 | 
			
		||||
  if (error != ESP_OK) {
 | 
			
		||||
    ESP_LOGE(TAG, "RMT TX timeout");
 | 
			
		||||
    this->status_set_warning();
 | 
			
		||||
@@ -162,11 +127,7 @@ void ESP32RMTLEDStripLightOutput::write_state(light::LightState *state) {
 | 
			
		||||
  size_t size = 0;
 | 
			
		||||
  size_t len = 0;
 | 
			
		||||
  uint8_t *psrc = this->buf_;
 | 
			
		||||
#if ESP_IDF_VERSION_MAJOR >= 5
 | 
			
		||||
  rmt_symbol_word_t *pdest = this->rmt_buf_;
 | 
			
		||||
#else
 | 
			
		||||
  rmt_item32_t *pdest = this->rmt_buf_;
 | 
			
		||||
#endif
 | 
			
		||||
  while (size < buffer_size) {
 | 
			
		||||
    uint8_t b = *psrc;
 | 
			
		||||
    for (int i = 0; i < 8; i++) {
 | 
			
		||||
@@ -184,15 +145,11 @@ void ESP32RMTLEDStripLightOutput::write_state(light::LightState *state) {
 | 
			
		||||
    len++;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
#if ESP_IDF_VERSION_MAJOR >= 5
 | 
			
		||||
  rmt_transmit_config_t config;
 | 
			
		||||
  memset(&config, 0, sizeof(config));
 | 
			
		||||
  config.loop_count = 0;
 | 
			
		||||
  config.flags.eot_level = 0;
 | 
			
		||||
  error = rmt_transmit(this->channel_, this->encoder_, this->rmt_buf_, len * sizeof(rmt_symbol_word_t), &config);
 | 
			
		||||
#else
 | 
			
		||||
  error = rmt_write_items(this->channel_, this->rmt_buf_, len, false);
 | 
			
		||||
#endif
 | 
			
		||||
  if (error != ESP_OK) {
 | 
			
		||||
    ESP_LOGE(TAG, "RMT TX error");
 | 
			
		||||
    this->status_set_warning();
 | 
			
		||||
@@ -251,11 +208,7 @@ void ESP32RMTLEDStripLightOutput::dump_config() {
 | 
			
		||||
                "ESP32 RMT LED Strip:\n"
 | 
			
		||||
                "  Pin: %u",
 | 
			
		||||
                this->pin_);
 | 
			
		||||
#if ESP_IDF_VERSION_MAJOR >= 5
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  RMT Symbols: %" PRIu32, this->rmt_symbols_);
 | 
			
		||||
#else
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Channel: %u", this->channel_);
 | 
			
		||||
#endif
 | 
			
		||||
  const char *rgb_order;
 | 
			
		||||
  switch (this->rgb_order_) {
 | 
			
		||||
    case ORDER_RGB:
 | 
			
		||||
 
 | 
			
		||||
@@ -11,12 +11,7 @@
 | 
			
		||||
#include <driver/gpio.h>
 | 
			
		||||
#include <esp_err.h>
 | 
			
		||||
#include <esp_idf_version.h>
 | 
			
		||||
 | 
			
		||||
#if ESP_IDF_VERSION_MAJOR >= 5
 | 
			
		||||
#include <driver/rmt_tx.h>
 | 
			
		||||
#else
 | 
			
		||||
#include <driver/rmt.h>
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace esp32_rmt_led_strip {
 | 
			
		||||
@@ -61,11 +56,7 @@ class ESP32RMTLEDStripLightOutput : public light::AddressableLight {
 | 
			
		||||
                      uint32_t reset_time_high, uint32_t reset_time_low);
 | 
			
		||||
 | 
			
		||||
  void set_rgb_order(RGBOrder rgb_order) { this->rgb_order_ = rgb_order; }
 | 
			
		||||
#if ESP_IDF_VERSION_MAJOR >= 5
 | 
			
		||||
  void set_rmt_symbols(uint32_t rmt_symbols) { this->rmt_symbols_ = rmt_symbols; }
 | 
			
		||||
#else
 | 
			
		||||
  void set_rmt_channel(rmt_channel_t channel) { this->channel_ = channel; }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  void clear_effect_data() override {
 | 
			
		||||
    for (int i = 0; i < this->size(); i++)
 | 
			
		||||
@@ -81,17 +72,11 @@ class ESP32RMTLEDStripLightOutput : public light::AddressableLight {
 | 
			
		||||
 | 
			
		||||
  uint8_t *buf_{nullptr};
 | 
			
		||||
  uint8_t *effect_data_{nullptr};
 | 
			
		||||
#if ESP_IDF_VERSION_MAJOR >= 5
 | 
			
		||||
  rmt_channel_handle_t channel_{nullptr};
 | 
			
		||||
  rmt_encoder_handle_t encoder_{nullptr};
 | 
			
		||||
  rmt_symbol_word_t *rmt_buf_{nullptr};
 | 
			
		||||
  rmt_symbol_word_t bit0_, bit1_, reset_;
 | 
			
		||||
  uint32_t rmt_symbols_{48};
 | 
			
		||||
#else
 | 
			
		||||
  rmt_item32_t *rmt_buf_{nullptr};
 | 
			
		||||
  rmt_item32_t bit0_, bit1_, reset_;
 | 
			
		||||
  rmt_channel_t channel_{RMT_CHANNEL_0};
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  uint8_t pin_;
 | 
			
		||||
  uint16_t num_leds_;
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,7 @@ import logging
 | 
			
		||||
 | 
			
		||||
from esphome import pins
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import esp32, esp32_rmt, light
 | 
			
		||||
from esphome.components import esp32, light
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_CHIPSET,
 | 
			
		||||
@@ -13,11 +13,9 @@ from esphome.const import (
 | 
			
		||||
    CONF_OUTPUT_ID,
 | 
			
		||||
    CONF_PIN,
 | 
			
		||||
    CONF_RGB_ORDER,
 | 
			
		||||
    CONF_RMT_CHANNEL,
 | 
			
		||||
    CONF_RMT_SYMBOLS,
 | 
			
		||||
    CONF_USE_DMA,
 | 
			
		||||
)
 | 
			
		||||
from esphome.core import CORE
 | 
			
		||||
 | 
			
		||||
_LOGGER = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
@@ -69,53 +67,6 @@ CONF_RESET_HIGH = "reset_high"
 | 
			
		||||
CONF_RESET_LOW = "reset_low"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class OptionalForIDF5(cv.SplitDefault):
 | 
			
		||||
    @property
 | 
			
		||||
    def default(self):
 | 
			
		||||
        if not esp32_rmt.use_new_rmt_driver():
 | 
			
		||||
            return cv.UNDEFINED
 | 
			
		||||
        return super().default
 | 
			
		||||
 | 
			
		||||
    @default.setter
 | 
			
		||||
    def default(self, value):
 | 
			
		||||
        # Ignore default set from vol.Optional
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def only_with_new_rmt_driver(obj):
 | 
			
		||||
    if not esp32_rmt.use_new_rmt_driver():
 | 
			
		||||
        raise cv.Invalid(
 | 
			
		||||
            "This feature is only available for the IDF framework version 5."
 | 
			
		||||
        )
 | 
			
		||||
    return obj
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def not_with_new_rmt_driver(obj):
 | 
			
		||||
    if esp32_rmt.use_new_rmt_driver():
 | 
			
		||||
        raise cv.Invalid(
 | 
			
		||||
            "This feature is not available for the IDF framework version 5."
 | 
			
		||||
        )
 | 
			
		||||
    return obj
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def final_validation(config):
 | 
			
		||||
    if not esp32_rmt.use_new_rmt_driver():
 | 
			
		||||
        if CONF_RMT_CHANNEL not in config:
 | 
			
		||||
            if CORE.using_esp_idf:
 | 
			
		||||
                raise cv.Invalid(
 | 
			
		||||
                    "rmt_channel is a required option for IDF version < 5."
 | 
			
		||||
                )
 | 
			
		||||
            raise cv.Invalid(
 | 
			
		||||
                "rmt_channel is a required option for the Arduino framework."
 | 
			
		||||
            )
 | 
			
		||||
        _LOGGER.warning(
 | 
			
		||||
            "RMT_LED_STRIP support for IDF version < 5 is deprecated and will be removed soon."
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
FINAL_VALIDATE_SCHEMA = final_validation
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.All(
 | 
			
		||||
    light.ADDRESSABLE_LIGHT_SCHEMA.extend(
 | 
			
		||||
        {
 | 
			
		||||
@@ -123,20 +74,17 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
            cv.Required(CONF_PIN): pins.internal_gpio_output_pin_number,
 | 
			
		||||
            cv.Required(CONF_NUM_LEDS): cv.positive_not_null_int,
 | 
			
		||||
            cv.Required(CONF_RGB_ORDER): cv.enum(RGB_ORDERS, upper=True),
 | 
			
		||||
            cv.Optional(CONF_RMT_CHANNEL): cv.All(
 | 
			
		||||
                not_with_new_rmt_driver, esp32_rmt.validate_rmt_channel(tx=True)
 | 
			
		||||
            ),
 | 
			
		||||
            OptionalForIDF5(
 | 
			
		||||
            cv.SplitDefault(
 | 
			
		||||
                CONF_RMT_SYMBOLS,
 | 
			
		||||
                esp32_idf=192,
 | 
			
		||||
                esp32_s2_idf=192,
 | 
			
		||||
                esp32_s3_idf=192,
 | 
			
		||||
                esp32_p4_idf=192,
 | 
			
		||||
                esp32_c3_idf=96,
 | 
			
		||||
                esp32_c5_idf=96,
 | 
			
		||||
                esp32_c6_idf=96,
 | 
			
		||||
                esp32_h2_idf=96,
 | 
			
		||||
            ): cv.All(only_with_new_rmt_driver, cv.int_range(min=2)),
 | 
			
		||||
                esp32=192,
 | 
			
		||||
                esp32_s2=192,
 | 
			
		||||
                esp32_s3=192,
 | 
			
		||||
                esp32_p4=192,
 | 
			
		||||
                esp32_c3=96,
 | 
			
		||||
                esp32_c5=96,
 | 
			
		||||
                esp32_c6=96,
 | 
			
		||||
                esp32_h2=96,
 | 
			
		||||
            ): cv.int_range(min=2),
 | 
			
		||||
            cv.Optional(CONF_MAX_REFRESH_RATE): cv.positive_time_period_microseconds,
 | 
			
		||||
            cv.Optional(CONF_CHIPSET): cv.one_of(*CHIPSETS, upper=True),
 | 
			
		||||
            cv.Optional(CONF_IS_RGBW, default=False): cv.boolean,
 | 
			
		||||
@@ -145,7 +93,6 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
                esp32.only_on_variant(
 | 
			
		||||
                    supported=[esp32.const.VARIANT_ESP32S3, esp32.const.VARIANT_ESP32P4]
 | 
			
		||||
                ),
 | 
			
		||||
                cv.only_with_esp_idf,
 | 
			
		||||
                cv.boolean,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_USE_PSRAM, default=True): cv.boolean,
 | 
			
		||||
@@ -218,15 +165,6 @@ async def to_code(config):
 | 
			
		||||
    cg.add(var.set_is_rgbw(config[CONF_IS_RGBW]))
 | 
			
		||||
    cg.add(var.set_is_wrgb(config[CONF_IS_WRGB]))
 | 
			
		||||
    cg.add(var.set_use_psram(config[CONF_USE_PSRAM]))
 | 
			
		||||
 | 
			
		||||
    if esp32_rmt.use_new_rmt_driver():
 | 
			
		||||
    cg.add(var.set_rmt_symbols(config[CONF_RMT_SYMBOLS]))
 | 
			
		||||
    if CONF_USE_DMA in config:
 | 
			
		||||
        cg.add(var.set_use_dma(config[CONF_USE_DMA]))
 | 
			
		||||
    else:
 | 
			
		||||
        rmt_channel_t = cg.global_ns.enum("rmt_channel_t")
 | 
			
		||||
        cg.add(
 | 
			
		||||
            var.set_rmt_channel(
 | 
			
		||||
                getattr(rmt_channel_t, f"RMT_CHANNEL_{config[CONF_RMT_CHANNEL]}")
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
 
 | 
			
		||||
@@ -183,6 +183,7 @@ async def to_code(config):
 | 
			
		||||
 | 
			
		||||
    cg.add_platformio_option("board", config[CONF_BOARD])
 | 
			
		||||
    cg.add_build_flag("-DUSE_ESP8266")
 | 
			
		||||
    cg.set_cpp_standard("gnu++17")
 | 
			
		||||
    cg.add_define("ESPHOME_BOARD", config[CONF_BOARD])
 | 
			
		||||
    cg.add_define("ESPHOME_VARIANT", "ESP8266")
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -26,19 +26,19 @@ void ESPHomeOTAComponent::setup() {
 | 
			
		||||
  ota::register_ota_platform(this);
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  server_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0);  // monitored for incoming connections
 | 
			
		||||
  if (server_ == nullptr) {
 | 
			
		||||
  this->server_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0);  // monitored for incoming connections
 | 
			
		||||
  if (this->server_ == nullptr) {
 | 
			
		||||
    ESP_LOGW(TAG, "Could not create socket");
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  int enable = 1;
 | 
			
		||||
  int err = server_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int));
 | 
			
		||||
  int err = this->server_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int));
 | 
			
		||||
  if (err != 0) {
 | 
			
		||||
    ESP_LOGW(TAG, "Socket unable to set reuseaddr: errno %d", err);
 | 
			
		||||
    // we can still continue
 | 
			
		||||
  }
 | 
			
		||||
  err = server_->setblocking(false);
 | 
			
		||||
  err = this->server_->setblocking(false);
 | 
			
		||||
  if (err != 0) {
 | 
			
		||||
    ESP_LOGW(TAG, "Socket unable to set nonblocking mode: errno %d", err);
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
@@ -54,14 +54,14 @@ void ESPHomeOTAComponent::setup() {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  err = server_->bind((struct sockaddr *) &server, sizeof(server));
 | 
			
		||||
  err = this->server_->bind((struct sockaddr *) &server, sizeof(server));
 | 
			
		||||
  if (err != 0) {
 | 
			
		||||
    ESP_LOGW(TAG, "Socket unable to bind: errno %d", errno);
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  err = server_->listen(4);
 | 
			
		||||
  err = this->server_->listen(4);
 | 
			
		||||
  if (err != 0) {
 | 
			
		||||
    ESP_LOGW(TAG, "Socket unable to listen: errno %d", errno);
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
@@ -82,7 +82,14 @@ void ESPHomeOTAComponent::dump_config() {
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ESPHomeOTAComponent::loop() { this->handle_(); }
 | 
			
		||||
void ESPHomeOTAComponent::loop() {
 | 
			
		||||
  // Skip handle_() call if no client connected and no incoming connections
 | 
			
		||||
  // This optimization reduces idle loop overhead when OTA is not active
 | 
			
		||||
  // Note: No need to check server_ for null as the component is marked failed in setup() if server_ creation fails
 | 
			
		||||
  if (this->client_ != nullptr || this->server_->ready()) {
 | 
			
		||||
    this->handle_();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static const uint8_t FEATURE_SUPPORTS_COMPRESSION = 0x01;
 | 
			
		||||
 | 
			
		||||
@@ -101,23 +108,21 @@ void ESPHomeOTAComponent::handle_() {
 | 
			
		||||
  size_t size_acknowledged = 0;
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  if (client_ == nullptr) {
 | 
			
		||||
    // Check if the server socket is ready before accepting
 | 
			
		||||
    if (this->server_->ready()) {
 | 
			
		||||
  if (this->client_ == nullptr) {
 | 
			
		||||
    // We already checked server_->ready() in loop(), so we can accept directly
 | 
			
		||||
    struct sockaddr_storage source_addr;
 | 
			
		||||
    socklen_t addr_len = sizeof(source_addr);
 | 
			
		||||
      client_ = server_->accept((struct sockaddr *) &source_addr, &addr_len);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  if (client_ == nullptr)
 | 
			
		||||
    this->client_ = this->server_->accept((struct sockaddr *) &source_addr, &addr_len);
 | 
			
		||||
    if (this->client_ == nullptr)
 | 
			
		||||
      return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  int enable = 1;
 | 
			
		||||
  int err = client_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int));
 | 
			
		||||
  int err = this->client_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int));
 | 
			
		||||
  if (err != 0) {
 | 
			
		||||
    ESP_LOGW(TAG, "Socket could not enable TCP nodelay, errno %d", errno);
 | 
			
		||||
    client_->close();
 | 
			
		||||
    client_ = nullptr;
 | 
			
		||||
    this->client_->close();
 | 
			
		||||
    this->client_ = nullptr;
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -106,7 +106,7 @@ void EthernetComponent::setup() {
 | 
			
		||||
      .post_cb = nullptr,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
#if USE_ESP_IDF && (ESP_IDF_VERSION_MAJOR >= 5)
 | 
			
		||||
#if ESP_IDF_VERSION_MAJOR >= 5
 | 
			
		||||
  eth_w5500_config_t w5500_config = ETH_W5500_DEFAULT_CONFIG(host, &devcfg);
 | 
			
		||||
#else
 | 
			
		||||
  spi_device_handle_t spi_handle = nullptr;
 | 
			
		||||
@@ -274,6 +274,9 @@ void EthernetComponent::loop() {
 | 
			
		||||
        ESP_LOGW(TAG, "Connection lost; reconnecting");
 | 
			
		||||
        this->state_ = EthernetComponentState::CONNECTING;
 | 
			
		||||
        this->start_connect_();
 | 
			
		||||
      } else {
 | 
			
		||||
        // When connected and stable, disable the loop to save CPU cycles
 | 
			
		||||
        this->disable_loop();
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
@@ -397,11 +400,13 @@ void EthernetComponent::eth_event_handler(void *arg, esp_event_base_t event_base
 | 
			
		||||
    case ETHERNET_EVENT_START:
 | 
			
		||||
      event_name = "ETH started";
 | 
			
		||||
      global_eth_component->started_ = true;
 | 
			
		||||
      global_eth_component->enable_loop_soon_any_context();
 | 
			
		||||
      break;
 | 
			
		||||
    case ETHERNET_EVENT_STOP:
 | 
			
		||||
      event_name = "ETH stopped";
 | 
			
		||||
      global_eth_component->started_ = false;
 | 
			
		||||
      global_eth_component->connected_ = false;
 | 
			
		||||
      global_eth_component->enable_loop_soon_any_context();  // Enable loop when connection state changes
 | 
			
		||||
      break;
 | 
			
		||||
    case ETHERNET_EVENT_CONNECTED:
 | 
			
		||||
      event_name = "ETH connected";
 | 
			
		||||
@@ -409,6 +414,7 @@ void EthernetComponent::eth_event_handler(void *arg, esp_event_base_t event_base
 | 
			
		||||
    case ETHERNET_EVENT_DISCONNECTED:
 | 
			
		||||
      event_name = "ETH disconnected";
 | 
			
		||||
      global_eth_component->connected_ = false;
 | 
			
		||||
      global_eth_component->enable_loop_soon_any_context();  // Enable loop when connection state changes
 | 
			
		||||
      break;
 | 
			
		||||
    default:
 | 
			
		||||
      return;
 | 
			
		||||
@@ -425,8 +431,10 @@ void EthernetComponent::got_ip_event_handler(void *arg, esp_event_base_t event_b
 | 
			
		||||
  global_eth_component->got_ipv4_address_ = true;
 | 
			
		||||
#if USE_NETWORK_IPV6 && (USE_NETWORK_MIN_IPV6_ADDR_COUNT > 0)
 | 
			
		||||
  global_eth_component->connected_ = global_eth_component->ipv6_count_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT;
 | 
			
		||||
  global_eth_component->enable_loop_soon_any_context();  // Enable loop when connection state changes
 | 
			
		||||
#else
 | 
			
		||||
  global_eth_component->connected_ = true;
 | 
			
		||||
  global_eth_component->enable_loop_soon_any_context();  // Enable loop when connection state changes
 | 
			
		||||
#endif /* USE_NETWORK_IPV6 */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -439,8 +447,10 @@ void EthernetComponent::got_ip6_event_handler(void *arg, esp_event_base_t event_
 | 
			
		||||
#if (USE_NETWORK_MIN_IPV6_ADDR_COUNT > 0)
 | 
			
		||||
  global_eth_component->connected_ =
 | 
			
		||||
      global_eth_component->got_ipv4_address_ && (global_eth_component->ipv6_count_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT);
 | 
			
		||||
  global_eth_component->enable_loop_soon_any_context();  // Enable loop when connection state changes
 | 
			
		||||
#else
 | 
			
		||||
  global_eth_component->connected_ = global_eth_component->got_ipv4_address_;
 | 
			
		||||
  global_eth_component->enable_loop_soon_any_context();  // Enable loop when connection state changes
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
#endif /* USE_NETWORK_IPV6 */
 | 
			
		||||
@@ -620,6 +630,7 @@ bool EthernetComponent::powerdown() {
 | 
			
		||||
  }
 | 
			
		||||
  this->connected_ = false;
 | 
			
		||||
  this->started_ = false;
 | 
			
		||||
  // No need to enable_loop() here as this is only called during shutdown/reboot
 | 
			
		||||
  if (this->phy_->pwrctl(this->phy_, false) != ESP_OK) {
 | 
			
		||||
    ESP_LOGE(TAG, "Error powering down ethernet PHY");
 | 
			
		||||
    return false;
 | 
			
		||||
 
 | 
			
		||||
@@ -18,8 +18,8 @@ from esphome.const import (
 | 
			
		||||
    DEVICE_CLASS_MOTION,
 | 
			
		||||
)
 | 
			
		||||
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 = ["@nohat"]
 | 
			
		||||
IS_PLATFORM_COMPONENT = True
 | 
			
		||||
@@ -59,6 +59,9 @@ _EVENT_SCHEMA = (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
_EVENT_SCHEMA.add_extra(entity_duplicate_validator("event"))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def event_schema(
 | 
			
		||||
    class_: MockObjClass = cv.UNDEFINED,
 | 
			
		||||
    *,
 | 
			
		||||
@@ -88,7 +91,7 @@ EVENT_SCHEMA.add_extra(cv.deprecated_schema_constant("event"))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def setup_event_core_(var, config, *, event_types: list[str]):
 | 
			
		||||
    await setup_entity(var, config)
 | 
			
		||||
    await setup_entity(var, config, "event")
 | 
			
		||||
 | 
			
		||||
    for conf in config.get(CONF_ON_EVENT, []):
 | 
			
		||||
        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
 | 
			
		||||
 
 | 
			
		||||
@@ -32,7 +32,7 @@ from esphome.const import (
 | 
			
		||||
    CONF_WEB_SERVER,
 | 
			
		||||
)
 | 
			
		||||
from esphome.core import CORE, coroutine_with_priority
 | 
			
		||||
from esphome.cpp_helpers import setup_entity
 | 
			
		||||
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
 | 
			
		||||
 | 
			
		||||
IS_PLATFORM_COMPONENT = True
 | 
			
		||||
 | 
			
		||||
@@ -161,6 +161,9 @@ _FAN_SCHEMA = (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
_FAN_SCHEMA.add_extra(entity_duplicate_validator("fan"))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def fan_schema(
 | 
			
		||||
    class_: cg.Pvariable,
 | 
			
		||||
    *,
 | 
			
		||||
@@ -225,7 +228,7 @@ def validate_preset_modes(value):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def setup_fan_core_(var, config):
 | 
			
		||||
    await setup_entity(var, config)
 | 
			
		||||
    await setup_entity(var, config, "fan")
 | 
			
		||||
 | 
			
		||||
    cg.add(var.set_restore_mode(config[CONF_RESTORE_MODE]))
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
from collections.abc import MutableMapping
 | 
			
		||||
import functools
 | 
			
		||||
import hashlib
 | 
			
		||||
from itertools import accumulate
 | 
			
		||||
import logging
 | 
			
		||||
import os
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
@@ -468,8 +469,9 @@ class EFont:
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GlyphInfo:
 | 
			
		||||
    def __init__(self, data_len, advance, offset_x, offset_y, width, height):
 | 
			
		||||
        self.data_len = data_len
 | 
			
		||||
    def __init__(self, glyph, data, advance, offset_x, offset_y, width, height):
 | 
			
		||||
        self.glyph = glyph
 | 
			
		||||
        self.bitmap_data = data
 | 
			
		||||
        self.advance = advance
 | 
			
		||||
        self.offset_x = offset_x
 | 
			
		||||
        self.offset_y = offset_y
 | 
			
		||||
@@ -477,6 +479,62 @@ class GlyphInfo:
 | 
			
		||||
        self.height = height
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def glyph_to_glyphinfo(glyph, font, size, bpp):
 | 
			
		||||
    scale = 256 // (1 << bpp)
 | 
			
		||||
    if not font.is_scalable:
 | 
			
		||||
        sizes = [pt_to_px(x.size) for x in font.available_sizes]
 | 
			
		||||
        if size in sizes:
 | 
			
		||||
            font.select_size(sizes.index(size))
 | 
			
		||||
    else:
 | 
			
		||||
        font.set_pixel_sizes(size, 0)
 | 
			
		||||
    flags = FT_LOAD_RENDER
 | 
			
		||||
    if bpp != 1:
 | 
			
		||||
        flags |= FT_LOAD_NO_BITMAP
 | 
			
		||||
    else:
 | 
			
		||||
        flags |= FT_LOAD_TARGET_MONO
 | 
			
		||||
    font.load_char(glyph, flags)
 | 
			
		||||
    width = font.glyph.bitmap.width
 | 
			
		||||
    height = font.glyph.bitmap.rows
 | 
			
		||||
    buffer = font.glyph.bitmap.buffer
 | 
			
		||||
    pitch = font.glyph.bitmap.pitch
 | 
			
		||||
    glyph_data = [0] * ((height * width * bpp + 7) // 8)
 | 
			
		||||
    src_mode = font.glyph.bitmap.pixel_mode
 | 
			
		||||
    pos = 0
 | 
			
		||||
    for y in range(height):
 | 
			
		||||
        for x in range(width):
 | 
			
		||||
            if src_mode == ft_pixel_mode_mono:
 | 
			
		||||
                pixel = (
 | 
			
		||||
                    (1 << bpp) - 1
 | 
			
		||||
                    if buffer[y * pitch + x // 8] & (1 << (7 - x % 8))
 | 
			
		||||
                    else 0
 | 
			
		||||
                )
 | 
			
		||||
            else:
 | 
			
		||||
                pixel = buffer[y * pitch + x] // scale
 | 
			
		||||
            for bit_num in range(bpp):
 | 
			
		||||
                if pixel & (1 << (bpp - bit_num - 1)):
 | 
			
		||||
                    glyph_data[pos // 8] |= 0x80 >> (pos % 8)
 | 
			
		||||
                pos += 1
 | 
			
		||||
    ascender = pt_to_px(font.size.ascender)
 | 
			
		||||
    if ascender == 0:
 | 
			
		||||
        if not font.is_scalable:
 | 
			
		||||
            ascender = size
 | 
			
		||||
        else:
 | 
			
		||||
            _LOGGER.error(
 | 
			
		||||
                "Unable to determine ascender of font %s %s",
 | 
			
		||||
                font.family_name,
 | 
			
		||||
                font.style_name,
 | 
			
		||||
            )
 | 
			
		||||
    return GlyphInfo(
 | 
			
		||||
        glyph,
 | 
			
		||||
        glyph_data,
 | 
			
		||||
        pt_to_px(font.glyph.metrics.horiAdvance),
 | 
			
		||||
        font.glyph.bitmap_left,
 | 
			
		||||
        ascender - font.glyph.bitmap_top,
 | 
			
		||||
        width,
 | 
			
		||||
        height,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    """
 | 
			
		||||
    Collect all glyph codepoints, construct a map from a codepoint to a font file.
 | 
			
		||||
@@ -506,98 +564,47 @@ async def to_code(config):
 | 
			
		||||
 | 
			
		||||
    codepoints = list(point_set)
 | 
			
		||||
    codepoints.sort(key=functools.cmp_to_key(glyph_comparator))
 | 
			
		||||
    glyph_args = {}
 | 
			
		||||
    data = []
 | 
			
		||||
    bpp = config[CONF_BPP]
 | 
			
		||||
    scale = 256 // (1 << bpp)
 | 
			
		||||
    size = config[CONF_SIZE]
 | 
			
		||||
    # create the data array for all glyphs
 | 
			
		||||
    for codepoint in codepoints:
 | 
			
		||||
        font = point_font_map[codepoint]
 | 
			
		||||
        if not font.is_scalable:
 | 
			
		||||
            sizes = [pt_to_px(x.size) for x in font.available_sizes]
 | 
			
		||||
            if size in sizes:
 | 
			
		||||
                font.select_size(sizes.index(size))
 | 
			
		||||
        else:
 | 
			
		||||
            font.set_pixel_sizes(size, 0)
 | 
			
		||||
        flags = FT_LOAD_RENDER
 | 
			
		||||
        if bpp != 1:
 | 
			
		||||
            flags |= FT_LOAD_NO_BITMAP
 | 
			
		||||
        else:
 | 
			
		||||
            flags |= FT_LOAD_TARGET_MONO
 | 
			
		||||
        font.load_char(codepoint, flags)
 | 
			
		||||
        width = font.glyph.bitmap.width
 | 
			
		||||
        height = font.glyph.bitmap.rows
 | 
			
		||||
        buffer = font.glyph.bitmap.buffer
 | 
			
		||||
        pitch = font.glyph.bitmap.pitch
 | 
			
		||||
        glyph_data = [0] * ((height * width * bpp + 7) // 8)
 | 
			
		||||
        src_mode = font.glyph.bitmap.pixel_mode
 | 
			
		||||
        pos = 0
 | 
			
		||||
        for y in range(height):
 | 
			
		||||
            for x in range(width):
 | 
			
		||||
                if src_mode == ft_pixel_mode_mono:
 | 
			
		||||
                    pixel = (
 | 
			
		||||
                        (1 << bpp) - 1
 | 
			
		||||
                        if buffer[y * pitch + x // 8] & (1 << (7 - x % 8))
 | 
			
		||||
                        else 0
 | 
			
		||||
                    )
 | 
			
		||||
                else:
 | 
			
		||||
                    pixel = buffer[y * pitch + x] // scale
 | 
			
		||||
                for bit_num in range(bpp):
 | 
			
		||||
                    if pixel & (1 << (bpp - bit_num - 1)):
 | 
			
		||||
                        glyph_data[pos // 8] |= 0x80 >> (pos % 8)
 | 
			
		||||
                    pos += 1
 | 
			
		||||
        ascender = pt_to_px(font.size.ascender)
 | 
			
		||||
        if ascender == 0:
 | 
			
		||||
            if not font.is_scalable:
 | 
			
		||||
                ascender = size
 | 
			
		||||
            else:
 | 
			
		||||
                _LOGGER.error(
 | 
			
		||||
                    "Unable to determine ascender of font %s", config[CONF_FILE]
 | 
			
		||||
                )
 | 
			
		||||
        glyph_args[codepoint] = GlyphInfo(
 | 
			
		||||
            len(data),
 | 
			
		||||
            pt_to_px(font.glyph.metrics.horiAdvance),
 | 
			
		||||
            font.glyph.bitmap_left,
 | 
			
		||||
            ascender - font.glyph.bitmap_top,
 | 
			
		||||
            width,
 | 
			
		||||
            height,
 | 
			
		||||
        )
 | 
			
		||||
        data += glyph_data
 | 
			
		||||
 | 
			
		||||
    rhs = [HexInt(x) for x in data]
 | 
			
		||||
    glyph_args = [
 | 
			
		||||
        glyph_to_glyphinfo(x, point_font_map[x], size, bpp) for x in codepoints
 | 
			
		||||
    ]
 | 
			
		||||
    rhs = [HexInt(x) for x in flatten([x.bitmap_data for x in glyph_args])]
 | 
			
		||||
    prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
 | 
			
		||||
 | 
			
		||||
    # Create the glyph table that points to data in the above array.
 | 
			
		||||
    glyph_initializer = []
 | 
			
		||||
    for codepoint in codepoints:
 | 
			
		||||
        glyph_initializer.append(
 | 
			
		||||
    glyph_initializer = [
 | 
			
		||||
        cg.StructInitializer(
 | 
			
		||||
            GlyphData,
 | 
			
		||||
            (
 | 
			
		||||
                "a_char",
 | 
			
		||||
                    cg.RawExpression(
 | 
			
		||||
                        f"(const uint8_t *){cpp_string_escape(codepoint)}"
 | 
			
		||||
                    ),
 | 
			
		||||
                cg.RawExpression(f"(const uint8_t *){cpp_string_escape(x.glyph)}"),
 | 
			
		||||
            ),
 | 
			
		||||
            (
 | 
			
		||||
                "data",
 | 
			
		||||
                    cg.RawExpression(
 | 
			
		||||
                        f"{str(prog_arr)} + {str(glyph_args[codepoint].data_len)}"
 | 
			
		||||
                cg.RawExpression(f"{str(prog_arr)} + {str(y - len(x.bitmap_data))}"),
 | 
			
		||||
            ),
 | 
			
		||||
                ),
 | 
			
		||||
                ("advance", glyph_args[codepoint].advance),
 | 
			
		||||
                ("offset_x", glyph_args[codepoint].offset_x),
 | 
			
		||||
                ("offset_y", glyph_args[codepoint].offset_y),
 | 
			
		||||
                ("width", glyph_args[codepoint].width),
 | 
			
		||||
                ("height", glyph_args[codepoint].height),
 | 
			
		||||
            ("advance", x.advance),
 | 
			
		||||
            ("offset_x", x.offset_x),
 | 
			
		||||
            ("offset_y", x.offset_y),
 | 
			
		||||
            ("width", x.width),
 | 
			
		||||
            ("height", x.height),
 | 
			
		||||
        )
 | 
			
		||||
        for (x, y) in zip(
 | 
			
		||||
            glyph_args, list(accumulate([len(x.bitmap_data) for x in glyph_args]))
 | 
			
		||||
        )
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    glyphs = cg.static_const_array(config[CONF_RAW_GLYPH_ID], glyph_initializer)
 | 
			
		||||
 | 
			
		||||
    font_height = pt_to_px(base_font.size.height)
 | 
			
		||||
    ascender = pt_to_px(base_font.size.ascender)
 | 
			
		||||
    descender = abs(pt_to_px(base_font.size.descender))
 | 
			
		||||
    g = glyph_to_glyphinfo("x", base_font, size, bpp)
 | 
			
		||||
    xheight = g.height if len(g.bitmap_data) > 1 else 0
 | 
			
		||||
    g = glyph_to_glyphinfo("X", base_font, size, bpp)
 | 
			
		||||
    capheight = g.height if len(g.bitmap_data) > 1 else 0
 | 
			
		||||
    if font_height == 0:
 | 
			
		||||
        if not base_font.is_scalable:
 | 
			
		||||
            font_height = size
 | 
			
		||||
@@ -610,5 +617,8 @@ async def to_code(config):
 | 
			
		||||
        len(glyph_initializer),
 | 
			
		||||
        ascender,
 | 
			
		||||
        font_height,
 | 
			
		||||
        descender,
 | 
			
		||||
        xheight,
 | 
			
		||||
        capheight,
 | 
			
		||||
        bpp,
 | 
			
		||||
    )
 | 
			
		||||
 
 | 
			
		||||
@@ -45,8 +45,15 @@ void Glyph::scan_area(int *x1, int *y1, int *width, int *height) const {
 | 
			
		||||
  *height = this->glyph_data_->height;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Font::Font(const GlyphData *data, int data_nr, int baseline, int height, uint8_t bpp)
 | 
			
		||||
    : baseline_(baseline), height_(height), bpp_(bpp) {
 | 
			
		||||
Font::Font(const GlyphData *data, int data_nr, int baseline, int height, int descender, int xheight, int capheight,
 | 
			
		||||
           uint8_t bpp)
 | 
			
		||||
    : baseline_(baseline),
 | 
			
		||||
      height_(height),
 | 
			
		||||
      descender_(descender),
 | 
			
		||||
      linegap_(height - baseline - descender),
 | 
			
		||||
      xheight_(xheight),
 | 
			
		||||
      capheight_(capheight),
 | 
			
		||||
      bpp_(bpp) {
 | 
			
		||||
  glyphs_.reserve(data_nr);
 | 
			
		||||
  for (int i = 0; i < data_nr; ++i)
 | 
			
		||||
    glyphs_.emplace_back(&data[i]);
 | 
			
		||||
 
 | 
			
		||||
@@ -50,11 +50,17 @@ class Font
 | 
			
		||||
 public:
 | 
			
		||||
  /** Construct the font with the given glyphs.
 | 
			
		||||
   *
 | 
			
		||||
   * @param glyphs A vector of glyphs, must be sorted lexicographically.
 | 
			
		||||
   * @param data A vector of glyphs, must be sorted lexicographically.
 | 
			
		||||
   * @param data_nr The number of glyphs in data.
 | 
			
		||||
   * @param baseline The y-offset from the top of the text to the baseline.
 | 
			
		||||
   * @param bottom The y-offset from the top of the text to the bottom (i.e. height).
 | 
			
		||||
   * @param height The y-offset from the top of the text to the bottom.
 | 
			
		||||
   * @param descender The y-offset from the baseline to the lowest stroke in the font (e.g. from letters like g or p).
 | 
			
		||||
   * @param xheight The height of lowercase letters, usually measured at the "x" glyph.
 | 
			
		||||
   * @param capheight The height of capital letters, usually measured at the "X" glyph.
 | 
			
		||||
   * @param bpp The bits per pixel used for this font. Used to read data out of the glyph bitmaps.
 | 
			
		||||
   */
 | 
			
		||||
  Font(const GlyphData *data, int data_nr, int baseline, int height, uint8_t bpp = 1);
 | 
			
		||||
  Font(const GlyphData *data, int data_nr, int baseline, int height, int descender, int xheight, int capheight,
 | 
			
		||||
       uint8_t bpp = 1);
 | 
			
		||||
 | 
			
		||||
  int match_next_glyph(const uint8_t *str, int *match_length);
 | 
			
		||||
 | 
			
		||||
@@ -65,14 +71,23 @@ class Font
 | 
			
		||||
#endif
 | 
			
		||||
  inline int get_baseline() { return this->baseline_; }
 | 
			
		||||
  inline int get_height() { return this->height_; }
 | 
			
		||||
  inline int get_ascender() { return this->baseline_; }
 | 
			
		||||
  inline int get_descender() { return this->descender_; }
 | 
			
		||||
  inline int get_linegap() { return this->linegap_; }
 | 
			
		||||
  inline int get_xheight() { return this->xheight_; }
 | 
			
		||||
  inline int get_capheight() { return this->capheight_; }
 | 
			
		||||
  inline int get_bpp() { return this->bpp_; }
 | 
			
		||||
 | 
			
		||||
  const std::vector<Glyph, ExternalRAMAllocator<Glyph>> &get_glyphs() const { return glyphs_; }
 | 
			
		||||
  const std::vector<Glyph, RAMAllocator<Glyph>> &get_glyphs() const { return glyphs_; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  std::vector<Glyph, ExternalRAMAllocator<Glyph>> glyphs_;
 | 
			
		||||
  std::vector<Glyph, RAMAllocator<Glyph>> glyphs_;
 | 
			
		||||
  int baseline_;
 | 
			
		||||
  int height_;
 | 
			
		||||
  int descender_;
 | 
			
		||||
  int linegap_;
 | 
			
		||||
  int xheight_;
 | 
			
		||||
  int capheight_;
 | 
			
		||||
  uint8_t bpp_;  // bits per pixel
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -125,6 +125,6 @@ async def to_code(config):
 | 
			
		||||
    cg.add(var.set_max_temperature(config[CONF_MAX_TEMPERATURE]))
 | 
			
		||||
    cg.add(var.set_min_temperature(config[CONF_MIN_TEMPERATURE]))
 | 
			
		||||
 | 
			
		||||
    cg.add_library("tonia/HeatpumpIR", "1.0.32")
 | 
			
		||||
    cg.add_library("tonia/HeatpumpIR", "1.0.35")
 | 
			
		||||
    if CORE.is_libretiny:
 | 
			
		||||
        CORE.add_platformio_option("lib_ignore", "IRremoteESP8266")
 | 
			
		||||
 
 | 
			
		||||
@@ -41,6 +41,6 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    cg.add_build_flag("-DUSE_HOST")
 | 
			
		||||
    cg.add_define("USE_ESPHOME_HOST_MAC_ADDRESS", config[CONF_MAC_ADDRESS].parts)
 | 
			
		||||
    cg.add_build_flag("-std=c++17")
 | 
			
		||||
    cg.add_build_flag("-std=gnu++17")
 | 
			
		||||
    cg.add_define("ESPHOME_BOARD", "host")
 | 
			
		||||
    cg.add_platformio_option("platform", "platformio/native")
 | 
			
		||||
 
 | 
			
		||||
@@ -175,7 +175,7 @@ async def to_code(config):
 | 
			
		||||
                not config.get(CONF_VERIFY_SSL),
 | 
			
		||||
            )
 | 
			
		||||
        else:
 | 
			
		||||
            cg.add_library("WiFiClientSecure", None)
 | 
			
		||||
            cg.add_library("NetworkClientSecure", None)
 | 
			
		||||
            cg.add_library("HTTPClient", None)
 | 
			
		||||
    if CORE.is_esp8266:
 | 
			
		||||
        cg.add_library("ESP8266HTTPClient", None)
 | 
			
		||||
 
 | 
			
		||||
@@ -239,7 +239,7 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
 | 
			
		||||
 | 
			
		||||
    std::string response_body;
 | 
			
		||||
    if (this->capture_response_.value(x...)) {
 | 
			
		||||
      ExternalRAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
 | 
			
		||||
      RAMAllocator<uint8_t> allocator;
 | 
			
		||||
      uint8_t *buf = allocator.allocate(max_length);
 | 
			
		||||
      if (buf != nullptr) {
 | 
			
		||||
        size_t read_index = 0;
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@
 | 
			
		||||
 | 
			
		||||
#if defined(USE_ESP32) || defined(USE_RP2040)
 | 
			
		||||
#include <HTTPClient.h>
 | 
			
		||||
#include <WiFiClient.h>
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_ESP8266
 | 
			
		||||
#include <ESP8266HTTPClient.h>
 | 
			
		||||
 
 | 
			
		||||
@@ -54,7 +54,7 @@ void HttpRequestUpdate::update_task(void *params) {
 | 
			
		||||
    UPDATE_RETURN;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ExternalRAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
 | 
			
		||||
  RAMAllocator<uint8_t> allocator;
 | 
			
		||||
  uint8_t *data = allocator.allocate(container->content_length);
 | 
			
		||||
  if (data == nullptr) {
 | 
			
		||||
    std::string msg = str_sprintf("Failed to allocate %d bytes for manifest", container->content_length);
 | 
			
		||||
 
 | 
			
		||||
@@ -22,8 +22,9 @@ import esphome.final_validate as fv
 | 
			
		||||
CODEOWNERS = ["@esphome/core"]
 | 
			
		||||
i2c_ns = cg.esphome_ns.namespace("i2c")
 | 
			
		||||
I2CBus = i2c_ns.class_("I2CBus")
 | 
			
		||||
ArduinoI2CBus = i2c_ns.class_("ArduinoI2CBus", I2CBus, cg.Component)
 | 
			
		||||
IDFI2CBus = i2c_ns.class_("IDFI2CBus", I2CBus, cg.Component)
 | 
			
		||||
InternalI2CBus = i2c_ns.class_("InternalI2CBus", I2CBus)
 | 
			
		||||
ArduinoI2CBus = i2c_ns.class_("ArduinoI2CBus", InternalI2CBus, cg.Component)
 | 
			
		||||
IDFI2CBus = i2c_ns.class_("IDFI2CBus", InternalI2CBus, cg.Component)
 | 
			
		||||
I2CDevice = i2c_ns.class_("I2CDevice")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -71,6 +72,7 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
@coroutine_with_priority(1.0)
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    cg.add_global(i2c_ns.using)
 | 
			
		||||
    cg.add_define("USE_I2C")
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
#include <cstddef>
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
#include <utility>
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
@@ -108,5 +108,12 @@ class I2CBus {
 | 
			
		||||
  bool scan_{false};                                    ///< Should we scan ? Can be set in the yaml
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class InternalI2CBus : public I2CBus {
 | 
			
		||||
 public:
 | 
			
		||||
  /// @brief Returns the I2C port number.
 | 
			
		||||
  /// @return the port number of the internal I2C bus
 | 
			
		||||
  virtual int get_port() const = 0;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace i2c
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,11 @@
 | 
			
		||||
#ifdef USE_ARDUINO
 | 
			
		||||
 | 
			
		||||
#include "i2c_bus_arduino.h"
 | 
			
		||||
#include <Arduino.h>
 | 
			
		||||
#include <cstring>
 | 
			
		||||
#include "esphome/core/application.h"
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include <Arduino.h>
 | 
			
		||||
#include <cstring>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace i2c {
 | 
			
		||||
@@ -23,6 +23,7 @@ void ArduinoI2CBus::setup() {
 | 
			
		||||
  } else {
 | 
			
		||||
    wire_ = new TwoWire(next_bus_num);  // NOLINT(cppcoreguidelines-owning-memory)
 | 
			
		||||
  }
 | 
			
		||||
  this->port_ = next_bus_num;
 | 
			
		||||
  next_bus_num++;
 | 
			
		||||
#elif defined(USE_ESP8266)
 | 
			
		||||
  wire_ = new TwoWire();  // NOLINT(cppcoreguidelines-owning-memory)
 | 
			
		||||
@@ -125,7 +126,7 @@ ErrorCode ArduinoI2CBus::readv(uint8_t address, ReadBuffer *buffers, size_t cnt)
 | 
			
		||||
  size_t to_request = 0;
 | 
			
		||||
  for (size_t i = 0; i < cnt; i++)
 | 
			
		||||
    to_request += buffers[i].len;
 | 
			
		||||
  size_t ret = wire_->requestFrom((int) address, (int) to_request, 1);
 | 
			
		||||
  size_t ret = wire_->requestFrom(address, to_request, true);
 | 
			
		||||
  if (ret != to_request) {
 | 
			
		||||
    ESP_LOGVV(TAG, "RX %u from %02X failed with error %u", to_request, address, ret);
 | 
			
		||||
    return ERROR_TIMEOUT;
 | 
			
		||||
 
 | 
			
		||||
@@ -2,9 +2,9 @@
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ARDUINO
 | 
			
		||||
 | 
			
		||||
#include "i2c_bus.h"
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include <Wire.h>
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "i2c_bus.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace i2c {
 | 
			
		||||
@@ -15,7 +15,7 @@ enum RecoveryCode {
 | 
			
		||||
  RECOVERY_COMPLETED,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class ArduinoI2CBus : public I2CBus, public Component {
 | 
			
		||||
class ArduinoI2CBus : public InternalI2CBus, public Component {
 | 
			
		||||
 public:
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
@@ -29,12 +29,15 @@ class ArduinoI2CBus : public I2CBus, public Component {
 | 
			
		||||
  void set_frequency(uint32_t frequency) { frequency_ = frequency; }
 | 
			
		||||
  void set_timeout(uint32_t timeout) { timeout_ = timeout; }
 | 
			
		||||
 | 
			
		||||
  int get_port() const override { return this->port_; }
 | 
			
		||||
 | 
			
		||||
 private:
 | 
			
		||||
  void recover_();
 | 
			
		||||
  void set_pins_and_clock_();
 | 
			
		||||
  RecoveryCode recovery_result_;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  int8_t port_{-1};
 | 
			
		||||
  TwoWire *wire_;
 | 
			
		||||
  uint8_t sda_pin_;
 | 
			
		||||
  uint8_t scl_pin_;
 | 
			
		||||
 
 | 
			
		||||
@@ -2,9 +2,9 @@
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP_IDF
 | 
			
		||||
 | 
			
		||||
#include "i2c_bus.h"
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include <driver/i2c.h>
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "i2c_bus.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace i2c {
 | 
			
		||||
@@ -15,7 +15,7 @@ enum RecoveryCode {
 | 
			
		||||
  RECOVERY_COMPLETED,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class IDFI2CBus : public I2CBus, public Component {
 | 
			
		||||
class IDFI2CBus : public InternalI2CBus, public Component {
 | 
			
		||||
 public:
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
@@ -31,6 +31,8 @@ class IDFI2CBus : public I2CBus, public Component {
 | 
			
		||||
  void set_frequency(uint32_t frequency) { frequency_ = frequency; }
 | 
			
		||||
  void set_timeout(uint32_t timeout) { timeout_ = timeout; }
 | 
			
		||||
 | 
			
		||||
  int get_port() const override { return static_cast<int>(this->port_); }
 | 
			
		||||
 | 
			
		||||
 private:
 | 
			
		||||
  void recover_();
 | 
			
		||||
  RecoveryCode recovery_result_;
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@ namespace i2s_audio {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "i2s_audio";
 | 
			
		||||
 | 
			
		||||
#if defined(USE_ESP_IDF) && (ESP_IDF_VERSION_MAJOR >= 5)
 | 
			
		||||
#if ESP_IDF_VERSION_MAJOR >= 5
 | 
			
		||||
static const uint8_t I2S_NUM_MAX = SOC_I2S_NUM;  // because IDF 5+ took this away :(
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -114,7 +114,7 @@ async def to_code(config):
 | 
			
		||||
        cg.add(var.set_external_dac_channels(2 if config[CONF_MODE] == "stereo" else 1))
 | 
			
		||||
        cg.add(var.set_i2s_comm_fmt_lsb(config[CONF_I2S_COMM_FMT] == "lsb"))
 | 
			
		||||
 | 
			
		||||
    cg.add_library("WiFiClientSecure", None)
 | 
			
		||||
    cg.add_library("NetworkClientSecure", None)
 | 
			
		||||
    cg.add_library("HTTPClient", None)
 | 
			
		||||
    cg.add_library("esphome/ESP32-audioI2S", "2.2.0")
 | 
			
		||||
    cg.add_library("esphome/ESP32-audioI2S", "2.3.0")
 | 
			
		||||
    cg.add_build_flag("-DAUDIO_NO_SD_FS")
 | 
			
		||||
 
 | 
			
		||||
@@ -484,7 +484,7 @@ bool I2SAudioSpeaker::send_esp_err_to_event_group_(esp_err_t err) {
 | 
			
		||||
esp_err_t I2SAudioSpeaker::allocate_buffers_(size_t data_buffer_size, size_t ring_buffer_size) {
 | 
			
		||||
  if (this->data_buffer_ == nullptr) {
 | 
			
		||||
    // Allocate data buffer for temporarily storing audio from the ring buffer before writing to the I2S bus
 | 
			
		||||
    ExternalRAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
 | 
			
		||||
    RAMAllocator<uint8_t> allocator;
 | 
			
		||||
    this->data_buffer_ = allocator.allocate(data_buffer_size);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -698,7 +698,7 @@ void I2SAudioSpeaker::delete_task_(size_t buffer_size) {
 | 
			
		||||
  this->audio_ring_buffer_.reset();  // Releases ownership of the shared_ptr
 | 
			
		||||
 | 
			
		||||
  if (this->data_buffer_ != nullptr) {
 | 
			
		||||
    ExternalRAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
 | 
			
		||||
    RAMAllocator<uint8_t> allocator;
 | 
			
		||||
    allocator.deallocate(this->data_buffer_, buffer_size);
 | 
			
		||||
    this->data_buffer_ = nullptr;
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -129,6 +129,13 @@ void INA219Component::setup() {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void INA219Component::on_powerdown() {
 | 
			
		||||
  // Mode = 0 -> power down
 | 
			
		||||
  if (!this->write_byte_16(INA219_REGISTER_CONFIG, 0)) {
 | 
			
		||||
    ESP_LOGE(TAG, "powerdown error");
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void INA219Component::dump_config() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "INA219:");
 | 
			
		||||
  LOG_I2C_DEVICE(this);
 | 
			
		||||
 
 | 
			
		||||
@@ -15,6 +15,7 @@ class INA219Component : public PollingComponent, public i2c::I2CDevice {
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
  float get_setup_priority() const override;
 | 
			
		||||
  void update() override;
 | 
			
		||||
  void on_powerdown() override;
 | 
			
		||||
 | 
			
		||||
  void set_shunt_resistance_ohm(float shunt_resistance_ohm) { shunt_resistance_ohm_ = shunt_resistance_ohm; }
 | 
			
		||||
  void set_max_current_a(float max_current_a) { max_current_a_ = max_current_a; }
 | 
			
		||||
 
 | 
			
		||||
@@ -57,8 +57,8 @@ void Inkplate6::setup() {
 | 
			
		||||
 * Allocate buffers. May be called after setup to re-initialise if e.g. greyscale is changed.
 | 
			
		||||
 */
 | 
			
		||||
void Inkplate6::initialize_() {
 | 
			
		||||
  ExternalRAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE);
 | 
			
		||||
  ExternalRAMAllocator<uint32_t> allocator32(ExternalRAMAllocator<uint32_t>::ALLOW_FAILURE);
 | 
			
		||||
  RAMAllocator<uint8_t> allocator;
 | 
			
		||||
  RAMAllocator<uint32_t> allocator32;
 | 
			
		||||
  uint32_t buffer_size = this->get_buffer_length_();
 | 
			
		||||
  if (buffer_size == 0)
 | 
			
		||||
    return;
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,8 @@
 | 
			
		||||
#include "esphome/components/sensor/sensor.h"
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/application.h"
 | 
			
		||||
 | 
			
		||||
#define highbyte(val) (uint8_t)((val) >> 8)
 | 
			
		||||
#define lowbyte(val) (uint8_t)((val) &0xff)
 | 
			
		||||
 | 
			
		||||
@@ -73,9 +75,9 @@ void LD2410Component::dump_config() {
 | 
			
		||||
#endif
 | 
			
		||||
  this->read_all_info();
 | 
			
		||||
  ESP_LOGCONFIG(TAG,
 | 
			
		||||
                "  Throttle_ : %ums\n"
 | 
			
		||||
                "  MAC Address : %s\n"
 | 
			
		||||
                "  Firmware Version : %s",
 | 
			
		||||
                "  Throttle: %ums\n"
 | 
			
		||||
                "  MAC address: %s\n"
 | 
			
		||||
                "  Firmware version: %s",
 | 
			
		||||
                this->throttle_, const_cast<char *>(this->mac_.c_str()), const_cast<char *>(this->version_.c_str()));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -153,7 +155,7 @@ void LD2410Component::handle_periodic_data_(uint8_t *buffer, int len) {
 | 
			
		||||
  /*
 | 
			
		||||
    Reduce data update rate to prevent home assistant database size grow fast
 | 
			
		||||
  */
 | 
			
		||||
  int32_t current_millis = millis();
 | 
			
		||||
  int32_t current_millis = App.get_loop_component_start_time();
 | 
			
		||||
  if (current_millis - last_periodic_millis_ < this->throttle_)
 | 
			
		||||
    return;
 | 
			
		||||
  last_periodic_millis_ = current_millis;
 | 
			
		||||
@@ -299,21 +301,6 @@ const char MAC_FMT[] = "%02X:%02X:%02X:%02X:%02X:%02X";
 | 
			
		||||
const std::string UNKNOWN_MAC("unknown");
 | 
			
		||||
const std::string NO_MAC("08:05:04:03:02:01");
 | 
			
		||||
 | 
			
		||||
std::string format_mac(uint8_t *buffer) {
 | 
			
		||||
  std::string::size_type mac_size = 256;
 | 
			
		||||
  std::string mac;
 | 
			
		||||
  do {
 | 
			
		||||
    mac.resize(mac_size + 1);
 | 
			
		||||
    mac_size = std::snprintf(&mac[0], mac.size(), MAC_FMT, buffer[10], buffer[11], buffer[12], buffer[13], buffer[14],
 | 
			
		||||
                             buffer[15]);
 | 
			
		||||
  } while (mac_size + 1 > mac.size());
 | 
			
		||||
  mac.resize(mac_size);
 | 
			
		||||
  if (mac == NO_MAC) {
 | 
			
		||||
    return UNKNOWN_MAC;
 | 
			
		||||
  }
 | 
			
		||||
  return mac;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#ifdef USE_NUMBER
 | 
			
		||||
std::function<void(void)> set_number_value(number::Number *n, float value) {
 | 
			
		||||
  float normalized_value = value * 1.0;
 | 
			
		||||
@@ -328,40 +315,40 @@ std::function<void(void)> set_number_value(number::Number *n, float value) {
 | 
			
		||||
bool LD2410Component::handle_ack_data_(uint8_t *buffer, int len) {
 | 
			
		||||
  ESP_LOGV(TAG, "Handling ACK DATA for COMMAND %02X", buffer[COMMAND]);
 | 
			
		||||
  if (len < 10) {
 | 
			
		||||
    ESP_LOGE(TAG, "Error with last command : incorrect length");
 | 
			
		||||
    ESP_LOGE(TAG, "Invalid length");
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
  if (buffer[0] != 0xFD || buffer[1] != 0xFC || buffer[2] != 0xFB || buffer[3] != 0xFA) {  // check 4 frame start bytes
 | 
			
		||||
    ESP_LOGE(TAG, "Error with last command : incorrect Header");
 | 
			
		||||
    ESP_LOGE(TAG, "Invalid header");
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
  if (buffer[COMMAND_STATUS] != 0x01) {
 | 
			
		||||
    ESP_LOGE(TAG, "Error with last command : status != 0x01");
 | 
			
		||||
    ESP_LOGE(TAG, "Invalid status");
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->two_byte_to_int_(buffer[8], buffer[9]) != 0x00) {
 | 
			
		||||
    ESP_LOGE(TAG, "Error with last command , last buffer was: %u , %u", buffer[8], buffer[9]);
 | 
			
		||||
    ESP_LOGE(TAG, "Invalid command: %u, %u", buffer[8], buffer[9]);
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  switch (buffer[COMMAND]) {
 | 
			
		||||
    case lowbyte(CMD_ENABLE_CONF):
 | 
			
		||||
      ESP_LOGV(TAG, "Handled Enable conf command");
 | 
			
		||||
      ESP_LOGV(TAG, "Enable conf");
 | 
			
		||||
      break;
 | 
			
		||||
    case lowbyte(CMD_DISABLE_CONF):
 | 
			
		||||
      ESP_LOGV(TAG, "Handled Disabled conf command");
 | 
			
		||||
      ESP_LOGV(TAG, "Disabled conf");
 | 
			
		||||
      break;
 | 
			
		||||
    case lowbyte(CMD_SET_BAUD_RATE):
 | 
			
		||||
      ESP_LOGV(TAG, "Handled baud rate change command");
 | 
			
		||||
      ESP_LOGV(TAG, "Baud rate change");
 | 
			
		||||
#ifdef USE_SELECT
 | 
			
		||||
      if (this->baud_rate_select_ != nullptr) {
 | 
			
		||||
        ESP_LOGE(TAG, "Change baud rate component config to %s and reinstall", this->baud_rate_select_->state.c_str());
 | 
			
		||||
        ESP_LOGE(TAG, "Configure baud rate to %s and reinstall", this->baud_rate_select_->state.c_str());
 | 
			
		||||
      }
 | 
			
		||||
#endif
 | 
			
		||||
      break;
 | 
			
		||||
    case lowbyte(CMD_VERSION):
 | 
			
		||||
      this->version_ = format_version(buffer);
 | 
			
		||||
      ESP_LOGV(TAG, "FW Version is: %s", const_cast<char *>(this->version_.c_str()));
 | 
			
		||||
      ESP_LOGV(TAG, "Firmware version: %s", const_cast<char *>(this->version_.c_str()));
 | 
			
		||||
#ifdef USE_TEXT_SENSOR
 | 
			
		||||
      if (this->version_text_sensor_ != nullptr) {
 | 
			
		||||
        this->version_text_sensor_->publish_state(this->version_);
 | 
			
		||||
@@ -371,7 +358,7 @@ bool LD2410Component::handle_ack_data_(uint8_t *buffer, int len) {
 | 
			
		||||
    case lowbyte(CMD_QUERY_DISTANCE_RESOLUTION): {
 | 
			
		||||
      std::string distance_resolution =
 | 
			
		||||
          DISTANCE_RESOLUTION_INT_TO_ENUM.at(this->two_byte_to_int_(buffer[10], buffer[11]));
 | 
			
		||||
      ESP_LOGV(TAG, "Distance resolution is: %s", const_cast<char *>(distance_resolution.c_str()));
 | 
			
		||||
      ESP_LOGV(TAG, "Distance resolution: %s", const_cast<char *>(distance_resolution.c_str()));
 | 
			
		||||
#ifdef USE_SELECT
 | 
			
		||||
      if (this->distance_resolution_select_ != nullptr &&
 | 
			
		||||
          this->distance_resolution_select_->state != distance_resolution) {
 | 
			
		||||
@@ -383,9 +370,9 @@ bool LD2410Component::handle_ack_data_(uint8_t *buffer, int len) {
 | 
			
		||||
      this->light_function_ = LIGHT_FUNCTION_INT_TO_ENUM.at(buffer[10]);
 | 
			
		||||
      this->light_threshold_ = buffer[11] * 1.0;
 | 
			
		||||
      this->out_pin_level_ = OUT_PIN_LEVEL_INT_TO_ENUM.at(buffer[12]);
 | 
			
		||||
      ESP_LOGV(TAG, "Light function is: %s", const_cast<char *>(this->light_function_.c_str()));
 | 
			
		||||
      ESP_LOGV(TAG, "Light threshold is: %f", this->light_threshold_);
 | 
			
		||||
      ESP_LOGV(TAG, "Out pin level is: %s", const_cast<char *>(this->out_pin_level_.c_str()));
 | 
			
		||||
      ESP_LOGV(TAG, "Light function: %s", const_cast<char *>(this->light_function_.c_str()));
 | 
			
		||||
      ESP_LOGV(TAG, "Light threshold: %f", this->light_threshold_);
 | 
			
		||||
      ESP_LOGV(TAG, "Out pin level: %s", const_cast<char *>(this->out_pin_level_.c_str()));
 | 
			
		||||
#ifdef USE_SELECT
 | 
			
		||||
      if (this->light_function_select_ != nullptr && this->light_function_select_->state != this->light_function_) {
 | 
			
		||||
        this->light_function_select_->publish_state(this->light_function_);
 | 
			
		||||
@@ -406,11 +393,11 @@ bool LD2410Component::handle_ack_data_(uint8_t *buffer, int len) {
 | 
			
		||||
      if (len < 20) {
 | 
			
		||||
        return false;
 | 
			
		||||
      }
 | 
			
		||||
      this->mac_ = format_mac(buffer);
 | 
			
		||||
      ESP_LOGV(TAG, "MAC Address is: %s", const_cast<char *>(this->mac_.c_str()));
 | 
			
		||||
      this->mac_ = format_mac_address_pretty(&buffer[10]);
 | 
			
		||||
      ESP_LOGV(TAG, "MAC address: %s", this->mac_.c_str());
 | 
			
		||||
#ifdef USE_TEXT_SENSOR
 | 
			
		||||
      if (this->mac_text_sensor_ != nullptr) {
 | 
			
		||||
        this->mac_text_sensor_->publish_state(this->mac_);
 | 
			
		||||
        this->mac_text_sensor_->publish_state(this->mac_ == NO_MAC ? UNKNOWN_MAC : this->mac_);
 | 
			
		||||
      }
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SWITCH
 | 
			
		||||
@@ -420,19 +407,19 @@ bool LD2410Component::handle_ack_data_(uint8_t *buffer, int len) {
 | 
			
		||||
#endif
 | 
			
		||||
      break;
 | 
			
		||||
    case lowbyte(CMD_GATE_SENS):
 | 
			
		||||
      ESP_LOGV(TAG, "Handled sensitivity command");
 | 
			
		||||
      ESP_LOGV(TAG, "Sensitivity");
 | 
			
		||||
      break;
 | 
			
		||||
    case lowbyte(CMD_BLUETOOTH):
 | 
			
		||||
      ESP_LOGV(TAG, "Handled bluetooth command");
 | 
			
		||||
      ESP_LOGV(TAG, "Bluetooth");
 | 
			
		||||
      break;
 | 
			
		||||
    case lowbyte(CMD_SET_DISTANCE_RESOLUTION):
 | 
			
		||||
      ESP_LOGV(TAG, "Handled set distance resolution command");
 | 
			
		||||
      ESP_LOGV(TAG, "Set distance resolution");
 | 
			
		||||
      break;
 | 
			
		||||
    case lowbyte(CMD_SET_LIGHT_CONTROL):
 | 
			
		||||
      ESP_LOGV(TAG, "Handled set light control command");
 | 
			
		||||
      ESP_LOGV(TAG, "Set light control");
 | 
			
		||||
      break;
 | 
			
		||||
    case lowbyte(CMD_BT_PASSWORD):
 | 
			
		||||
      ESP_LOGV(TAG, "Handled set bluetooth password command");
 | 
			
		||||
      ESP_LOGV(TAG, "Set bluetooth password");
 | 
			
		||||
      break;
 | 
			
		||||
    case lowbyte(CMD_QUERY):  // Query parameters response
 | 
			
		||||
    {
 | 
			
		||||
@@ -532,7 +519,7 @@ void LD2410Component::set_baud_rate(const std::string &state) {
 | 
			
		||||
 | 
			
		||||
void LD2410Component::set_bluetooth_password(const std::string &password) {
 | 
			
		||||
  if (password.length() != 6) {
 | 
			
		||||
    ESP_LOGE(TAG, "set_bluetooth_password(): invalid password length, must be exactly 6 chars '%s'", password.c_str());
 | 
			
		||||
    ESP_LOGE(TAG, "Password must be exactly 6 chars");
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  this->set_config_mode_(true);
 | 
			
		||||
@@ -544,7 +531,7 @@ void LD2410Component::set_bluetooth_password(const std::string &password) {
 | 
			
		||||
 | 
			
		||||
void LD2410Component::set_engineering_mode(bool enable) {
 | 
			
		||||
  this->set_config_mode_(true);
 | 
			
		||||
  last_engineering_mode_change_millis_ = millis();
 | 
			
		||||
  last_engineering_mode_change_millis_ = App.get_loop_component_start_time();
 | 
			
		||||
  uint8_t cmd = enable ? CMD_ENABLE_ENG : CMD_DISABLE_ENG;
 | 
			
		||||
  this->send_command_(cmd, nullptr, 0);
 | 
			
		||||
  this->set_config_mode_(false);
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user