From 40c0c36179aade554ec7d941a10945c7e57ef10c Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 13 Aug 2025 14:46:51 +1200 Subject: [PATCH 001/208] Bump version to 2025.9.0-dev --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index 1f5ac5aa1b..f312ca45e2 100644 --- a/Doxyfile +++ b/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.8.0-dev +PROJECT_NUMBER = 2025.9.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 diff --git a/esphome/const.py b/esphome/const.py index 49997ca766..f27adff9be 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2025.8.0-dev" +__version__ = "2025.9.0-dev" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 0dda3faed543c945402434787b5e2000fc8c3978 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 12 Aug 2025 22:46:56 -0500 Subject: [PATCH 002/208] [CI] Fix CI job failures for PRs with >300 changed files (#10215) --- script/helpers.py | 19 +++++++++++-- tests/script/test_helpers.py | 55 ++++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/script/helpers.py b/script/helpers.py index b346f3a461..2c2f44a513 100644 --- a/script/helpers.py +++ b/script/helpers.py @@ -139,9 +139,24 @@ def _get_changed_files_github_actions() -> list[str] | None: if event_name == "pull_request": pr_number = _get_pr_number_from_github_env() if pr_number: - # Use GitHub CLI to get changed files directly + # Try gh pr diff first (faster for small PRs) cmd = ["gh", "pr", "diff", pr_number, "--name-only"] - return _get_changed_files_from_command(cmd) + try: + return _get_changed_files_from_command(cmd) + except Exception as e: + # If it fails due to the 300 file limit, use the API method + if "maximum" in str(e) and "files" in str(e): + cmd = [ + "gh", + "api", + f"repos/esphome/esphome/pulls/{pr_number}/files", + "--paginate", + "--jq", + ".[].filename", + ] + return _get_changed_files_from_command(cmd) + # Re-raise for other errors + raise # For pushes (including squash-and-merge) elif event_name == "push": diff --git a/tests/script/test_helpers.py b/tests/script/test_helpers.py index 9730efd366..63f1f0e600 100644 --- a/tests/script/test_helpers.py +++ b/tests/script/test_helpers.py @@ -183,6 +183,61 @@ def test_get_changed_files_github_actions_pull_request( assert result == expected_files +def test_get_changed_files_github_actions_pull_request_large_pr( + monkeypatch: MonkeyPatch, +) -> None: + """Test _get_changed_files_github_actions fallback for PRs with >300 files.""" + monkeypatch.setenv("GITHUB_EVENT_NAME", "pull_request") + + expected_files = ["file1.py", "file2.cpp"] + + with ( + patch("helpers._get_pr_number_from_github_env", return_value="10214"), + patch("helpers._get_changed_files_from_command") as mock_get, + ): + # First call fails with too many files error, second succeeds with API method + mock_get.side_effect = [ + Exception("Sorry, the diff exceeded the maximum number of files (300)"), + expected_files, + ] + + result = _get_changed_files_github_actions() + + assert mock_get.call_count == 2 + mock_get.assert_any_call(["gh", "pr", "diff", "10214", "--name-only"]) + mock_get.assert_any_call( + [ + "gh", + "api", + "repos/esphome/esphome/pulls/10214/files", + "--paginate", + "--jq", + ".[].filename", + ] + ) + assert result == expected_files + + +def test_get_changed_files_github_actions_pull_request_other_error( + monkeypatch: MonkeyPatch, +) -> None: + """Test _get_changed_files_github_actions re-raises non-file-limit errors.""" + monkeypatch.setenv("GITHUB_EVENT_NAME", "pull_request") + + with ( + patch("helpers._get_pr_number_from_github_env", return_value="1234"), + patch("helpers._get_changed_files_from_command") as mock_get, + ): + # Error that is not about file limit + mock_get.side_effect = Exception("Command failed: authentication required") + + with pytest.raises(Exception, match="authentication required"): + _get_changed_files_github_actions() + + # Should only be called once (no retry with API) + mock_get.assert_called_once_with(["gh", "pr", "diff", "1234", "--name-only"]) + + def test_get_changed_files_github_actions_pull_request_no_pr_number( monkeypatch: MonkeyPatch, ) -> None: From 6a8722f33e9579fdbbc75f1b7c64330cb315daa6 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 14 Aug 2025 09:42:11 +1200 Subject: [PATCH 003/208] [entity] Allow ``device_id`` to be blank on entities (#10217) --- esphome/config_validation.py | 5 +++- esphome/core/entity_helpers.py | 8 +++--- .../fixtures/areas_and_devices.yaml | 26 +++++++++++++++++++ tests/integration/test_areas_and_devices.py | 13 ++++++++++ tests/unit_tests/core/test_entity_helpers.py | 16 ++++++++++++ 5 files changed, 63 insertions(+), 5 deletions(-) diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 9aaeb9f9e8..f811fbf7c2 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -393,10 +393,13 @@ def icon(value): ) -def sub_device_id(value: str | None) -> core.ID: +def sub_device_id(value: str | None) -> core.ID | None: # Lazy import to avoid circular imports from esphome.core.config import Device + if not value: + return None + return use_id(Device)(value) diff --git a/esphome/core/entity_helpers.py b/esphome/core/entity_helpers.py index 107b9fd739..1ccc3e2683 100644 --- a/esphome/core/entity_helpers.py +++ b/esphome/core/entity_helpers.py @@ -77,8 +77,8 @@ async def setup_entity(var: MockObj, config: ConfigType, platform: str) -> None: """ # Get device info device_name: str | None = None - if CONF_DEVICE_ID in config: - device_id_obj: ID = config[CONF_DEVICE_ID] + device_id_obj: ID | None + if device_id_obj := config.get(CONF_DEVICE_ID): device: MockObj = await get_variable(device_id_obj) add(var.set_device(device)) # Get device name for object ID calculation @@ -199,8 +199,8 @@ def entity_duplicate_validator(platform: str) -> Callable[[ConfigType], ConfigTy # Get device name if entity is on a sub-device device_name = None device_id = "" # Empty string for main device - if CONF_DEVICE_ID in config: - device_id_obj = config[CONF_DEVICE_ID] + device_id_obj: ID | None + if device_id_obj := config.get(CONF_DEVICE_ID): device_name = device_id_obj.id # Use the device ID string directly for uniqueness device_id = device_id_obj.id diff --git a/tests/integration/fixtures/areas_and_devices.yaml b/tests/integration/fixtures/areas_and_devices.yaml index 12ab070e55..08b02e6e1e 100644 --- a/tests/integration/fixtures/areas_and_devices.yaml +++ b/tests/integration/fixtures/areas_and_devices.yaml @@ -55,6 +55,12 @@ sensor: lambda: return 4.0; update_interval: 0.1s + - platform: template + name: Living Room Sensor + device_id: "" + lambda: return 5.0; + update_interval: 0.1s + # Switches with the same name on different devices to test device_id lookup switch: # Switch with no device_id (defaults to 0) @@ -96,3 +102,23 @@ switch: - logger.log: "Turning on Test Switch on Motion Detector" turn_off_action: - logger.log: "Turning off Test Switch on Motion Detector" + + - platform: template + name: Living Room Blank Switch + device_id: "" + id: test_switch_blank_living_room + optimistic: true + turn_on_action: + - logger.log: "Turning on Living Room Blank Switch" + turn_off_action: + - logger.log: "Turning off Living Room Blank Switch" + + - platform: template + name: Living Room None Switch + device_id: + id: test_switch_none_living_room + optimistic: true + turn_on_action: + - logger.log: "Turning on Living Room None Switch" + turn_off_action: + - logger.log: "Turning off Living Room None Switch" diff --git a/tests/integration/test_areas_and_devices.py b/tests/integration/test_areas_and_devices.py index 1af16c87e8..93326de0a9 100644 --- a/tests/integration/test_areas_and_devices.py +++ b/tests/integration/test_areas_and_devices.py @@ -132,6 +132,7 @@ async def test_areas_and_devices( "Temperature Sensor Reading": temp_sensor.device_id, "Motion Detector Status": motion_detector.device_id, "Smart Switch Power": smart_switch.device_id, + "Living Room Sensor": 0, # Main device } for entity in sensor_entities: @@ -160,6 +161,18 @@ async def test_areas_and_devices( "Should have a switch with device_id 0 (main device)" ) + # Verify extra switches with blank and none device_id are correctly available + extra_switches = [ + e for e in switch_entities if e.name.startswith("Living Room") + ] + assert len(extra_switches) == 2, ( + f"Expected 2 extra switches for Living Room, got {len(extra_switches)}" + ) + extra_switch_device_ids = [e.device_id for e in extra_switches] + assert all(d == 0 for d in extra_switch_device_ids), ( + "All extra switches should have device_id 0 (main device)" + ) + # Wait for initial states to be received for all switches await asyncio.wait_for(initial_states_future, timeout=2.0) diff --git a/tests/unit_tests/core/test_entity_helpers.py b/tests/unit_tests/core/test_entity_helpers.py index 2157bc20a9..db99243a1a 100644 --- a/tests/unit_tests/core/test_entity_helpers.py +++ b/tests/unit_tests/core/test_entity_helpers.py @@ -689,3 +689,19 @@ def test_entity_duplicate_validator_internal_entities() -> None: Invalid, match=r"Duplicate sensor entity with name 'Temperature' found" ): validator(config4) + + +def test_empty_or_null_device_id_on_entity() -> None: + """Test that empty or null device IDs are handled correctly.""" + # Create validator for sensor platform + validator = entity_duplicate_validator("sensor") + + # Entity with empty device_id should pass + config1 = {CONF_NAME: "Battery", CONF_DEVICE_ID: ""} + validated1 = validator(config1) + assert validated1 == config1 + + # Entity with None device_id should pass + config2 = {CONF_NAME: "Temperature", CONF_DEVICE_ID: None} + validated2 = validator(config2) + assert validated2 == config2 From 0d1949a61becc1cc99316306f3bffe98595dbcc5 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 14 Aug 2025 10:30:28 +1200 Subject: [PATCH 004/208] [espnow] Set state to enabled before adding initial peers (#10225) --- esphome/components/espnow/espnow_component.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/espnow/espnow_component.cpp b/esphome/components/espnow/espnow_component.cpp index 82f8e3230e..b0d5938dba 100644 --- a/esphome/components/espnow/espnow_component.cpp +++ b/esphome/components/espnow/espnow_component.cpp @@ -208,11 +208,11 @@ void ESPNowComponent::enable_() { esp_wifi_connectionless_module_set_wake_interval(CONFIG_ESPNOW_WAKE_INTERVAL); #endif + this->state_ = ESPNOW_STATE_ENABLED; + for (auto peer : this->peers_) { this->add_peer(peer.address); } - - this->state_ = ESPNOW_STATE_ENABLED; } void ESPNowComponent::disable() { @@ -407,7 +407,7 @@ esp_err_t ESPNowComponent::add_peer(const uint8_t *peer) { } if (memcmp(peer, this->own_address_, ESP_NOW_ETH_ALEN) == 0) { - this->mark_failed(); + this->status_momentary_warning("peer-add-failed"); return ESP_ERR_INVALID_MAC; } From c3f15964987bfb1cfb3464f4bcc18317f62b448b Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Wed, 13 Aug 2025 15:40:12 -0700 Subject: [PATCH 005/208] [psram] allow disabling (#10224) Co-authored-by: Samuel Sieb --- esphome/components/psram/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/esphome/components/psram/__init__.py b/esphome/components/psram/__init__.py index fd7e70a055..b5c87ae5a8 100644 --- a/esphome/components/psram/__init__.py +++ b/esphome/components/psram/__init__.py @@ -16,6 +16,7 @@ from esphome.components.esp32.const import ( import esphome.config_validation as cv from esphome.const import ( CONF_ADVANCED, + CONF_DISABLED, CONF_FRAMEWORK, CONF_ID, CONF_MODE, @@ -102,6 +103,7 @@ def get_config_schema(config): cv.Optional(CONF_MODE, default=modes[0]): cv.one_of(*modes, lower=True), cv.Optional(CONF_ENABLE_ECC, default=False): cv.boolean, cv.Optional(CONF_SPEED, default=speeds[0]): cv.one_of(*speeds, upper=True), + cv.Optional(CONF_DISABLED, default=False): cv.boolean, } )(config) @@ -112,6 +114,8 @@ FINAL_VALIDATE_SCHEMA = validate_psram_mode async def to_code(config): + if config[CONF_DISABLED]: + return if CORE.using_arduino: cg.add_build_flag("-DBOARD_HAS_PSRAM") if config[CONF_MODE] == TYPE_OCTAL: From 7c4a54de909820d208386598116e442f3f925c55 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 13 Aug 2025 17:42:54 -0500 Subject: [PATCH 006/208] Bump aioesphomeapi from 38.2.1 to 39.0.0 (#10222) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 9793336cf3..fc63c62f64 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile esptool==5.0.2 click==8.1.7 esphome-dashboard==20250514.0 -aioesphomeapi==38.2.1 +aioesphomeapi==39.0.0 zeroconf==0.147.0 puremagic==1.30 ruamel.yaml==0.18.14 # dashboard_import From 46d433775b14b08c048aa4f758a35c9e906da709 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 14 Aug 2025 12:40:20 +1200 Subject: [PATCH 007/208] Bump esphome-dashboard from 20250514.0 to 20250814.0 (#10227) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index fc63c62f64..0675115c02 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ pyserial==3.5 platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile esptool==5.0.2 click==8.1.7 -esphome-dashboard==20250514.0 +esphome-dashboard==20250814.0 aioesphomeapi==39.0.0 zeroconf==0.147.0 puremagic==1.30 From 6b5e43ca72bb3a2f72abc6d33672c60ba55b54e4 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 14 Aug 2025 14:19:03 +1200 Subject: [PATCH 008/208] [qm6988] Clean up code (#10216) --- esphome/components/qmp6988/qmp6988.cpp | 205 ++++++++++--------------- esphome/components/qmp6988/qmp6988.h | 68 ++++---- 2 files changed, 111 insertions(+), 162 deletions(-) diff --git a/esphome/components/qmp6988/qmp6988.cpp b/esphome/components/qmp6988/qmp6988.cpp index 6c22150f4f..61fde186d7 100644 --- a/esphome/components/qmp6988/qmp6988.cpp +++ b/esphome/components/qmp6988/qmp6988.cpp @@ -18,14 +18,6 @@ static const uint8_t QMP6988_TEMPERATURE_MSB_REG = 0xFA; /* Temperature MSB Reg static const uint8_t QMP6988_CALIBRATION_DATA_START = 0xA0; /* QMP6988 compensation coefficients */ static const uint8_t QMP6988_CALIBRATION_DATA_LENGTH = 25; -static const uint8_t SHIFT_RIGHT_4_POSITION = 4; -static const uint8_t SHIFT_LEFT_2_POSITION = 2; -static const uint8_t SHIFT_LEFT_4_POSITION = 4; -static const uint8_t SHIFT_LEFT_5_POSITION = 5; -static const uint8_t SHIFT_LEFT_8_POSITION = 8; -static const uint8_t SHIFT_LEFT_12_POSITION = 12; -static const uint8_t SHIFT_LEFT_16_POSITION = 16; - /* power mode */ static const uint8_t QMP6988_SLEEP_MODE = 0x00; static const uint8_t QMP6988_FORCED_MODE = 0x01; @@ -95,64 +87,45 @@ static const char *iir_filter_to_str(QMP6988IIRFilter filter) { } bool QMP6988Component::device_check_() { - uint8_t ret = 0; - - ret = this->read_register(QMP6988_CHIP_ID_REG, &(qmp6988_data_.chip_id), 1); - if (ret != i2c::ERROR_OK) { - ESP_LOGE(TAG, "%s: read chip ID (0xD1) failed", __func__); + if (this->read_register(QMP6988_CHIP_ID_REG, &(qmp6988_data_.chip_id), 1) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Read chip ID (0xD1) failed"); + return false; } - ESP_LOGD(TAG, "qmp6988 read chip id = 0x%x", qmp6988_data_.chip_id); + ESP_LOGV(TAG, "Read chip ID = 0x%x", qmp6988_data_.chip_id); return qmp6988_data_.chip_id == QMP6988_CHIP_ID; } bool QMP6988Component::get_calibration_data_() { - uint8_t status = 0; // BITFIELDS temp_COE; uint8_t a_data_uint8_tr[QMP6988_CALIBRATION_DATA_LENGTH] = {0}; - int len; - for (len = 0; len < QMP6988_CALIBRATION_DATA_LENGTH; len += 1) { - status = this->read_register(QMP6988_CALIBRATION_DATA_START + len, &a_data_uint8_tr[len], 1); - if (status != i2c::ERROR_OK) { - ESP_LOGE(TAG, "qmp6988 read calibration data (0xA0) error!"); + for (uint8_t len = 0; len < QMP6988_CALIBRATION_DATA_LENGTH; len += 1) { + if (this->read_register(QMP6988_CALIBRATION_DATA_START + len, &a_data_uint8_tr[len], 1) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Read calibration data (0xA0) error"); return false; } } qmp6988_data_.qmp6988_cali.COE_a0 = - (QMP6988_S32_t) (((a_data_uint8_tr[18] << SHIFT_LEFT_12_POSITION) | - (a_data_uint8_tr[19] << SHIFT_LEFT_4_POSITION) | (a_data_uint8_tr[24] & 0x0f)) - << 12); + (int32_t) encode_uint32(a_data_uint8_tr[18], a_data_uint8_tr[19], (a_data_uint8_tr[24] & 0x0f) << 4, 0); qmp6988_data_.qmp6988_cali.COE_a0 = qmp6988_data_.qmp6988_cali.COE_a0 >> 12; - qmp6988_data_.qmp6988_cali.COE_a1 = - (QMP6988_S16_t) (((a_data_uint8_tr[20]) << SHIFT_LEFT_8_POSITION) | a_data_uint8_tr[21]); - qmp6988_data_.qmp6988_cali.COE_a2 = - (QMP6988_S16_t) (((a_data_uint8_tr[22]) << SHIFT_LEFT_8_POSITION) | a_data_uint8_tr[23]); + qmp6988_data_.qmp6988_cali.COE_a1 = (int16_t) encode_uint16(a_data_uint8_tr[20], a_data_uint8_tr[21]); + qmp6988_data_.qmp6988_cali.COE_a2 = (int16_t) encode_uint16(a_data_uint8_tr[22], a_data_uint8_tr[23]); qmp6988_data_.qmp6988_cali.COE_b00 = - (QMP6988_S32_t) (((a_data_uint8_tr[0] << SHIFT_LEFT_12_POSITION) | (a_data_uint8_tr[1] << SHIFT_LEFT_4_POSITION) | - ((a_data_uint8_tr[24] & 0xf0) >> SHIFT_RIGHT_4_POSITION)) - << 12); + (int32_t) encode_uint32(a_data_uint8_tr[0], a_data_uint8_tr[1], a_data_uint8_tr[24] & 0xf0, 0); qmp6988_data_.qmp6988_cali.COE_b00 = qmp6988_data_.qmp6988_cali.COE_b00 >> 12; - qmp6988_data_.qmp6988_cali.COE_bt1 = - (QMP6988_S16_t) (((a_data_uint8_tr[2]) << SHIFT_LEFT_8_POSITION) | a_data_uint8_tr[3]); - qmp6988_data_.qmp6988_cali.COE_bt2 = - (QMP6988_S16_t) (((a_data_uint8_tr[4]) << SHIFT_LEFT_8_POSITION) | a_data_uint8_tr[5]); - qmp6988_data_.qmp6988_cali.COE_bp1 = - (QMP6988_S16_t) (((a_data_uint8_tr[6]) << SHIFT_LEFT_8_POSITION) | a_data_uint8_tr[7]); - qmp6988_data_.qmp6988_cali.COE_b11 = - (QMP6988_S16_t) (((a_data_uint8_tr[8]) << SHIFT_LEFT_8_POSITION) | a_data_uint8_tr[9]); - qmp6988_data_.qmp6988_cali.COE_bp2 = - (QMP6988_S16_t) (((a_data_uint8_tr[10]) << SHIFT_LEFT_8_POSITION) | a_data_uint8_tr[11]); - qmp6988_data_.qmp6988_cali.COE_b12 = - (QMP6988_S16_t) (((a_data_uint8_tr[12]) << SHIFT_LEFT_8_POSITION) | a_data_uint8_tr[13]); - qmp6988_data_.qmp6988_cali.COE_b21 = - (QMP6988_S16_t) (((a_data_uint8_tr[14]) << SHIFT_LEFT_8_POSITION) | a_data_uint8_tr[15]); - qmp6988_data_.qmp6988_cali.COE_bp3 = - (QMP6988_S16_t) (((a_data_uint8_tr[16]) << SHIFT_LEFT_8_POSITION) | a_data_uint8_tr[17]); + qmp6988_data_.qmp6988_cali.COE_bt1 = (int16_t) encode_uint16(a_data_uint8_tr[2], a_data_uint8_tr[3]); + qmp6988_data_.qmp6988_cali.COE_bt2 = (int16_t) encode_uint16(a_data_uint8_tr[4], a_data_uint8_tr[5]); + qmp6988_data_.qmp6988_cali.COE_bp1 = (int16_t) encode_uint16(a_data_uint8_tr[6], a_data_uint8_tr[7]); + qmp6988_data_.qmp6988_cali.COE_b11 = (int16_t) encode_uint16(a_data_uint8_tr[8], a_data_uint8_tr[9]); + qmp6988_data_.qmp6988_cali.COE_bp2 = (int16_t) encode_uint16(a_data_uint8_tr[10], a_data_uint8_tr[11]); + qmp6988_data_.qmp6988_cali.COE_b12 = (int16_t) encode_uint16(a_data_uint8_tr[12], a_data_uint8_tr[13]); + qmp6988_data_.qmp6988_cali.COE_b21 = (int16_t) encode_uint16(a_data_uint8_tr[14], a_data_uint8_tr[15]); + qmp6988_data_.qmp6988_cali.COE_bp3 = (int16_t) encode_uint16(a_data_uint8_tr[16], a_data_uint8_tr[17]); ESP_LOGV(TAG, "<-----------calibration data-------------->\r\n"); ESP_LOGV(TAG, "COE_a0[%d] COE_a1[%d] COE_a2[%d] COE_b00[%d]\r\n", qmp6988_data_.qmp6988_cali.COE_a0, @@ -166,17 +139,17 @@ bool QMP6988Component::get_calibration_data_() { qmp6988_data_.ik.a0 = qmp6988_data_.qmp6988_cali.COE_a0; // 20Q4 qmp6988_data_.ik.b00 = qmp6988_data_.qmp6988_cali.COE_b00; // 20Q4 - qmp6988_data_.ik.a1 = 3608L * (QMP6988_S32_t) qmp6988_data_.qmp6988_cali.COE_a1 - 1731677965L; // 31Q23 - qmp6988_data_.ik.a2 = 16889L * (QMP6988_S32_t) qmp6988_data_.qmp6988_cali.COE_a2 - 87619360L; // 30Q47 + qmp6988_data_.ik.a1 = 3608L * (int32_t) qmp6988_data_.qmp6988_cali.COE_a1 - 1731677965L; // 31Q23 + qmp6988_data_.ik.a2 = 16889L * (int32_t) qmp6988_data_.qmp6988_cali.COE_a2 - 87619360L; // 30Q47 - qmp6988_data_.ik.bt1 = 2982L * (QMP6988_S64_t) qmp6988_data_.qmp6988_cali.COE_bt1 + 107370906L; // 28Q15 - qmp6988_data_.ik.bt2 = 329854L * (QMP6988_S64_t) qmp6988_data_.qmp6988_cali.COE_bt2 + 108083093L; // 34Q38 - qmp6988_data_.ik.bp1 = 19923L * (QMP6988_S64_t) qmp6988_data_.qmp6988_cali.COE_bp1 + 1133836764L; // 31Q20 - qmp6988_data_.ik.b11 = 2406L * (QMP6988_S64_t) qmp6988_data_.qmp6988_cali.COE_b11 + 118215883L; // 28Q34 - qmp6988_data_.ik.bp2 = 3079L * (QMP6988_S64_t) qmp6988_data_.qmp6988_cali.COE_bp2 - 181579595L; // 29Q43 - qmp6988_data_.ik.b12 = 6846L * (QMP6988_S64_t) qmp6988_data_.qmp6988_cali.COE_b12 + 85590281L; // 29Q53 - qmp6988_data_.ik.b21 = 13836L * (QMP6988_S64_t) qmp6988_data_.qmp6988_cali.COE_b21 + 79333336L; // 29Q60 - qmp6988_data_.ik.bp3 = 2915L * (QMP6988_S64_t) qmp6988_data_.qmp6988_cali.COE_bp3 + 157155561L; // 28Q65 + qmp6988_data_.ik.bt1 = 2982L * (int64_t) qmp6988_data_.qmp6988_cali.COE_bt1 + 107370906L; // 28Q15 + qmp6988_data_.ik.bt2 = 329854L * (int64_t) qmp6988_data_.qmp6988_cali.COE_bt2 + 108083093L; // 34Q38 + qmp6988_data_.ik.bp1 = 19923L * (int64_t) qmp6988_data_.qmp6988_cali.COE_bp1 + 1133836764L; // 31Q20 + qmp6988_data_.ik.b11 = 2406L * (int64_t) qmp6988_data_.qmp6988_cali.COE_b11 + 118215883L; // 28Q34 + qmp6988_data_.ik.bp2 = 3079L * (int64_t) qmp6988_data_.qmp6988_cali.COE_bp2 - 181579595L; // 29Q43 + qmp6988_data_.ik.b12 = 6846L * (int64_t) qmp6988_data_.qmp6988_cali.COE_b12 + 85590281L; // 29Q53 + qmp6988_data_.ik.b21 = 13836L * (int64_t) qmp6988_data_.qmp6988_cali.COE_b21 + 79333336L; // 29Q60 + qmp6988_data_.ik.bp3 = 2915L * (int64_t) qmp6988_data_.qmp6988_cali.COE_bp3 + 157155561L; // 28Q65 ESP_LOGV(TAG, "<----------- int calibration data -------------->\r\n"); ESP_LOGV(TAG, "a0[%d] a1[%d] a2[%d] b00[%d]\r\n", qmp6988_data_.ik.a0, qmp6988_data_.ik.a1, qmp6988_data_.ik.a2, qmp6988_data_.ik.b00); @@ -188,55 +161,55 @@ bool QMP6988Component::get_calibration_data_() { return true; } -QMP6988_S16_t QMP6988Component::get_compensated_temperature_(qmp6988_ik_data_t *ik, QMP6988_S32_t dt) { - QMP6988_S16_t ret; - QMP6988_S64_t wk1, wk2; +int16_t QMP6988Component::get_compensated_temperature_(qmp6988_ik_data_t *ik, int32_t dt) { + int16_t ret; + int64_t wk1, wk2; // wk1: 60Q4 // bit size - wk1 = ((QMP6988_S64_t) ik->a1 * (QMP6988_S64_t) dt); // 31Q23+24-1=54 (54Q23) - wk2 = ((QMP6988_S64_t) ik->a2 * (QMP6988_S64_t) dt) >> 14; // 30Q47+24-1=53 (39Q33) - wk2 = (wk2 * (QMP6988_S64_t) dt) >> 10; // 39Q33+24-1=62 (52Q23) - wk2 = ((wk1 + wk2) / 32767) >> 19; // 54,52->55Q23 (20Q04) - ret = (QMP6988_S16_t) ((ik->a0 + wk2) >> 4); // 21Q4 -> 17Q0 + wk1 = ((int64_t) ik->a1 * (int64_t) dt); // 31Q23+24-1=54 (54Q23) + wk2 = ((int64_t) ik->a2 * (int64_t) dt) >> 14; // 30Q47+24-1=53 (39Q33) + wk2 = (wk2 * (int64_t) dt) >> 10; // 39Q33+24-1=62 (52Q23) + wk2 = ((wk1 + wk2) / 32767) >> 19; // 54,52->55Q23 (20Q04) + ret = (int16_t) ((ik->a0 + wk2) >> 4); // 21Q4 -> 17Q0 return ret; } -QMP6988_S32_t QMP6988Component::get_compensated_pressure_(qmp6988_ik_data_t *ik, QMP6988_S32_t dp, QMP6988_S16_t tx) { - QMP6988_S32_t ret; - QMP6988_S64_t wk1, wk2, wk3; +int32_t QMP6988Component::get_compensated_pressure_(qmp6988_ik_data_t *ik, int32_t dp, int16_t tx) { + int32_t ret; + int64_t wk1, wk2, wk3; // wk1 = 48Q16 // bit size - wk1 = ((QMP6988_S64_t) ik->bt1 * (QMP6988_S64_t) tx); // 28Q15+16-1=43 (43Q15) - wk2 = ((QMP6988_S64_t) ik->bp1 * (QMP6988_S64_t) dp) >> 5; // 31Q20+24-1=54 (49Q15) - wk1 += wk2; // 43,49->50Q15 - wk2 = ((QMP6988_S64_t) ik->bt2 * (QMP6988_S64_t) tx) >> 1; // 34Q38+16-1=49 (48Q37) - wk2 = (wk2 * (QMP6988_S64_t) tx) >> 8; // 48Q37+16-1=63 (55Q29) - wk3 = wk2; // 55Q29 - wk2 = ((QMP6988_S64_t) ik->b11 * (QMP6988_S64_t) tx) >> 4; // 28Q34+16-1=43 (39Q30) - wk2 = (wk2 * (QMP6988_S64_t) dp) >> 1; // 39Q30+24-1=62 (61Q29) - wk3 += wk2; // 55,61->62Q29 - wk2 = ((QMP6988_S64_t) ik->bp2 * (QMP6988_S64_t) dp) >> 13; // 29Q43+24-1=52 (39Q30) - wk2 = (wk2 * (QMP6988_S64_t) dp) >> 1; // 39Q30+24-1=62 (61Q29) - wk3 += wk2; // 62,61->63Q29 - wk1 += wk3 >> 14; // Q29 >> 14 -> Q15 - wk2 = ((QMP6988_S64_t) ik->b12 * (QMP6988_S64_t) tx); // 29Q53+16-1=45 (45Q53) - wk2 = (wk2 * (QMP6988_S64_t) tx) >> 22; // 45Q53+16-1=61 (39Q31) - wk2 = (wk2 * (QMP6988_S64_t) dp) >> 1; // 39Q31+24-1=62 (61Q30) - wk3 = wk2; // 61Q30 - wk2 = ((QMP6988_S64_t) ik->b21 * (QMP6988_S64_t) tx) >> 6; // 29Q60+16-1=45 (39Q54) - wk2 = (wk2 * (QMP6988_S64_t) dp) >> 23; // 39Q54+24-1=62 (39Q31) - wk2 = (wk2 * (QMP6988_S64_t) dp) >> 1; // 39Q31+24-1=62 (61Q20) - wk3 += wk2; // 61,61->62Q30 - wk2 = ((QMP6988_S64_t) ik->bp3 * (QMP6988_S64_t) dp) >> 12; // 28Q65+24-1=51 (39Q53) - wk2 = (wk2 * (QMP6988_S64_t) dp) >> 23; // 39Q53+24-1=62 (39Q30) - wk2 = (wk2 * (QMP6988_S64_t) dp); // 39Q30+24-1=62 (62Q30) - wk3 += wk2; // 62,62->63Q30 - wk1 += wk3 >> 15; // Q30 >> 15 = Q15 + wk1 = ((int64_t) ik->bt1 * (int64_t) tx); // 28Q15+16-1=43 (43Q15) + wk2 = ((int64_t) ik->bp1 * (int64_t) dp) >> 5; // 31Q20+24-1=54 (49Q15) + wk1 += wk2; // 43,49->50Q15 + wk2 = ((int64_t) ik->bt2 * (int64_t) tx) >> 1; // 34Q38+16-1=49 (48Q37) + wk2 = (wk2 * (int64_t) tx) >> 8; // 48Q37+16-1=63 (55Q29) + wk3 = wk2; // 55Q29 + wk2 = ((int64_t) ik->b11 * (int64_t) tx) >> 4; // 28Q34+16-1=43 (39Q30) + wk2 = (wk2 * (int64_t) dp) >> 1; // 39Q30+24-1=62 (61Q29) + wk3 += wk2; // 55,61->62Q29 + wk2 = ((int64_t) ik->bp2 * (int64_t) dp) >> 13; // 29Q43+24-1=52 (39Q30) + wk2 = (wk2 * (int64_t) dp) >> 1; // 39Q30+24-1=62 (61Q29) + wk3 += wk2; // 62,61->63Q29 + wk1 += wk3 >> 14; // Q29 >> 14 -> Q15 + wk2 = ((int64_t) ik->b12 * (int64_t) tx); // 29Q53+16-1=45 (45Q53) + wk2 = (wk2 * (int64_t) tx) >> 22; // 45Q53+16-1=61 (39Q31) + wk2 = (wk2 * (int64_t) dp) >> 1; // 39Q31+24-1=62 (61Q30) + wk3 = wk2; // 61Q30 + wk2 = ((int64_t) ik->b21 * (int64_t) tx) >> 6; // 29Q60+16-1=45 (39Q54) + wk2 = (wk2 * (int64_t) dp) >> 23; // 39Q54+24-1=62 (39Q31) + wk2 = (wk2 * (int64_t) dp) >> 1; // 39Q31+24-1=62 (61Q20) + wk3 += wk2; // 61,61->62Q30 + wk2 = ((int64_t) ik->bp3 * (int64_t) dp) >> 12; // 28Q65+24-1=51 (39Q53) + wk2 = (wk2 * (int64_t) dp) >> 23; // 39Q53+24-1=62 (39Q30) + wk2 = (wk2 * (int64_t) dp); // 39Q30+24-1=62 (62Q30) + wk3 += wk2; // 62,62->63Q30 + wk1 += wk3 >> 15; // Q30 >> 15 = Q15 wk1 /= 32767L; wk1 >>= 11; // Q15 >> 7 = Q4 wk1 += ik->b00; // Q4 + 20Q4 // wk1 >>= 4; // 28Q4 -> 24Q0 - ret = (QMP6988_S32_t) wk1; + ret = (int32_t) wk1; return ret; } @@ -274,7 +247,7 @@ void QMP6988Component::set_power_mode_(uint8_t power_mode) { delay(10); } -void QMP6988Component::write_filter_(unsigned char filter) { +void QMP6988Component::write_filter_(QMP6988IIRFilter filter) { uint8_t data; data = (filter & 0x03); @@ -282,7 +255,7 @@ void QMP6988Component::write_filter_(unsigned char filter) { delay(10); } -void QMP6988Component::write_oversampling_pressure_(unsigned char oversampling_p) { +void QMP6988Component::write_oversampling_pressure_(QMP6988Oversampling oversampling_p) { uint8_t data; this->read_register(QMP6988_CTRLMEAS_REG, &data, 1); @@ -292,7 +265,7 @@ void QMP6988Component::write_oversampling_pressure_(unsigned char oversampling_p delay(10); } -void QMP6988Component::write_oversampling_temperature_(unsigned char oversampling_t) { +void QMP6988Component::write_oversampling_temperature_(QMP6988Oversampling oversampling_t) { uint8_t data; this->read_register(QMP6988_CTRLMEAS_REG, &data, 1); @@ -302,16 +275,6 @@ void QMP6988Component::write_oversampling_temperature_(unsigned char oversamplin delay(10); } -void QMP6988Component::set_temperature_oversampling(QMP6988Oversampling oversampling_t) { - this->temperature_oversampling_ = oversampling_t; -} - -void QMP6988Component::set_pressure_oversampling(QMP6988Oversampling oversampling_p) { - this->pressure_oversampling_ = oversampling_p; -} - -void QMP6988Component::set_iir_filter(QMP6988IIRFilter iirfilter) { this->iir_filter_ = iirfilter; } - void QMP6988Component::calculate_altitude_(float pressure, float temp) { float altitude; altitude = (pow((101325 / pressure), 1 / 5.257) - 1) * (temp + 273.15) / 0.0065; @@ -320,10 +283,10 @@ void QMP6988Component::calculate_altitude_(float pressure, float temp) { void QMP6988Component::calculate_pressure_() { uint8_t err = 0; - QMP6988_U32_t p_read, t_read; - QMP6988_S32_t p_raw, t_raw; + uint32_t p_read, t_read; + int32_t p_raw, t_raw; uint8_t a_data_uint8_tr[6] = {0}; - QMP6988_S32_t t_int, p_int; + int32_t t_int, p_int; this->qmp6988_data_.temperature = 0; this->qmp6988_data_.pressure = 0; @@ -332,13 +295,11 @@ void QMP6988Component::calculate_pressure_() { ESP_LOGE(TAG, "Error reading raw pressure/temp values"); return; } - p_read = (QMP6988_U32_t) ((((QMP6988_U32_t) (a_data_uint8_tr[0])) << SHIFT_LEFT_16_POSITION) | - (((QMP6988_U16_t) (a_data_uint8_tr[1])) << SHIFT_LEFT_8_POSITION) | (a_data_uint8_tr[2])); - p_raw = (QMP6988_S32_t) (p_read - SUBTRACTOR); + p_read = encode_uint24(a_data_uint8_tr[0], a_data_uint8_tr[1], a_data_uint8_tr[2]); + p_raw = (int32_t) (p_read - SUBTRACTOR); - t_read = (QMP6988_U32_t) ((((QMP6988_U32_t) (a_data_uint8_tr[3])) << SHIFT_LEFT_16_POSITION) | - (((QMP6988_U16_t) (a_data_uint8_tr[4])) << SHIFT_LEFT_8_POSITION) | (a_data_uint8_tr[5])); - t_raw = (QMP6988_S32_t) (t_read - SUBTRACTOR); + t_read = encode_uint24(a_data_uint8_tr[3], a_data_uint8_tr[4], a_data_uint8_tr[5]); + t_raw = (int32_t) (t_read - SUBTRACTOR); t_int = this->get_compensated_temperature_(&(qmp6988_data_.ik), t_raw); p_int = this->get_compensated_pressure_(&(qmp6988_data_.ik), p_raw, t_int); @@ -348,10 +309,9 @@ void QMP6988Component::calculate_pressure_() { } void QMP6988Component::setup() { - bool ret; - ret = this->device_check_(); - if (!ret) { - ESP_LOGCONFIG(TAG, "Setup failed - device not found"); + if (!this->device_check_()) { + this->mark_failed(ESP_LOG_MSG_COMM_FAIL); + return; } this->software_reset_(); @@ -365,9 +325,6 @@ void QMP6988Component::setup() { void QMP6988Component::dump_config() { ESP_LOGCONFIG(TAG, "QMP6988:"); LOG_I2C_DEVICE(this); - if (this->is_failed()) { - ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); - } LOG_UPDATE_INTERVAL(this); LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); @@ -377,8 +334,6 @@ void QMP6988Component::dump_config() { ESP_LOGCONFIG(TAG, " IIR Filter: %s", iir_filter_to_str(this->iir_filter_)); } -float QMP6988Component::get_setup_priority() const { return setup_priority::DATA; } - void QMP6988Component::update() { this->calculate_pressure_(); float pressurehectopascals = this->qmp6988_data_.pressure / 100; diff --git a/esphome/components/qmp6988/qmp6988.h b/esphome/components/qmp6988/qmp6988.h index 61b46a4189..5b0f80c77e 100644 --- a/esphome/components/qmp6988/qmp6988.h +++ b/esphome/components/qmp6988/qmp6988.h @@ -1,24 +1,17 @@ #pragma once +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/sensor/sensor.h" #include "esphome/core/component.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#include "esphome/components/sensor/sensor.h" -#include "esphome/components/i2c/i2c.h" namespace esphome { namespace qmp6988 { -#define QMP6988_U16_t unsigned short -#define QMP6988_S16_t short -#define QMP6988_U32_t unsigned int -#define QMP6988_S32_t int -#define QMP6988_U64_t unsigned long long -#define QMP6988_S64_t long long - /* oversampling */ -enum QMP6988Oversampling { +enum QMP6988Oversampling : uint8_t { QMP6988_OVERSAMPLING_SKIPPED = 0x00, QMP6988_OVERSAMPLING_1X = 0x01, QMP6988_OVERSAMPLING_2X = 0x02, @@ -30,7 +23,7 @@ enum QMP6988Oversampling { }; /* filter */ -enum QMP6988IIRFilter { +enum QMP6988IIRFilter : uint8_t { QMP6988_IIR_FILTER_OFF = 0x00, QMP6988_IIR_FILTER_2X = 0x01, QMP6988_IIR_FILTER_4X = 0x02, @@ -40,18 +33,18 @@ enum QMP6988IIRFilter { }; using qmp6988_cali_data_t = struct Qmp6988CaliData { - QMP6988_S32_t COE_a0; - QMP6988_S16_t COE_a1; - QMP6988_S16_t COE_a2; - QMP6988_S32_t COE_b00; - QMP6988_S16_t COE_bt1; - QMP6988_S16_t COE_bt2; - QMP6988_S16_t COE_bp1; - QMP6988_S16_t COE_b11; - QMP6988_S16_t COE_bp2; - QMP6988_S16_t COE_b12; - QMP6988_S16_t COE_b21; - QMP6988_S16_t COE_bp3; + int32_t COE_a0; + int16_t COE_a1; + int16_t COE_a2; + int32_t COE_b00; + int16_t COE_bt1; + int16_t COE_bt2; + int16_t COE_bp1; + int16_t COE_b11; + int16_t COE_bp2; + int16_t COE_b12; + int16_t COE_b21; + int16_t COE_bp3; }; using qmp6988_fk_data_t = struct Qmp6988FkData { @@ -60,9 +53,9 @@ using qmp6988_fk_data_t = struct Qmp6988FkData { }; using qmp6988_ik_data_t = struct Qmp6988IkData { - QMP6988_S32_t a0, b00; - QMP6988_S32_t a1, a2; - QMP6988_S64_t bt1, bt2, bp1, b11, bp2, b12, b21, bp3; + int32_t a0, b00; + int32_t a1, a2; + int64_t bt1, bt2, bp1, b11, bp2, b12, b21, bp3; }; using qmp6988_data_t = struct Qmp6988Data { @@ -77,17 +70,18 @@ using qmp6988_data_t = struct Qmp6988Data { class QMP6988Component : public PollingComponent, public i2c::I2CDevice { public: - void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } - void set_pressure_sensor(sensor::Sensor *pressure_sensor) { pressure_sensor_ = pressure_sensor; } + void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; } + void set_pressure_sensor(sensor::Sensor *pressure_sensor) { this->pressure_sensor_ = pressure_sensor; } void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; - void set_iir_filter(QMP6988IIRFilter iirfilter); - void set_temperature_oversampling(QMP6988Oversampling oversampling_t); - void set_pressure_oversampling(QMP6988Oversampling oversampling_p); + void set_iir_filter(QMP6988IIRFilter iirfilter) { this->iir_filter_ = iirfilter; } + void set_temperature_oversampling(QMP6988Oversampling oversampling_t) { + this->temperature_oversampling_ = oversampling_t; + } + void set_pressure_oversampling(QMP6988Oversampling oversampling_p) { this->pressure_oversampling_ = oversampling_p; } protected: qmp6988_data_t qmp6988_data_; @@ -102,14 +96,14 @@ class QMP6988Component : public PollingComponent, public i2c::I2CDevice { bool get_calibration_data_(); bool device_check_(); void set_power_mode_(uint8_t power_mode); - void write_oversampling_temperature_(unsigned char oversampling_t); - void write_oversampling_pressure_(unsigned char oversampling_p); - void write_filter_(unsigned char filter); + void write_oversampling_temperature_(QMP6988Oversampling oversampling_t); + void write_oversampling_pressure_(QMP6988Oversampling oversampling_p); + void write_filter_(QMP6988IIRFilter filter); void calculate_pressure_(); void calculate_altitude_(float pressure, float temp); - QMP6988_S32_t get_compensated_pressure_(qmp6988_ik_data_t *ik, QMP6988_S32_t dp, QMP6988_S16_t tx); - QMP6988_S16_t get_compensated_temperature_(qmp6988_ik_data_t *ik, QMP6988_S32_t dt); + int32_t get_compensated_pressure_(qmp6988_ik_data_t *ik, int32_t dp, int16_t tx); + int16_t get_compensated_temperature_(qmp6988_ik_data_t *ik, int32_t dt); }; } // namespace qmp6988 From bd60dbb74608e1eaf89dbfd3fdc81f14cb4d30ad Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 15 Aug 2025 01:48:25 +1200 Subject: [PATCH 009/208] [quality] Remove period from audio related Invalid raises (#10229) --- esphome/components/i2s_audio/__init__.py | 2 +- esphome/components/i2s_audio/media_player/__init__.py | 2 +- esphome/components/i2s_audio/microphone/__init__.py | 2 +- esphome/components/i2s_audio/speaker/__init__.py | 2 +- esphome/components/micro_wake_word/__init__.py | 4 ++-- esphome/components/speaker/media_player/__init__.py | 4 ++-- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/esphome/components/i2s_audio/__init__.py b/esphome/components/i2s_audio/__init__.py index aa0a688fa0..bfa1b726f1 100644 --- a/esphome/components/i2s_audio/__init__.py +++ b/esphome/components/i2s_audio/__init__.py @@ -212,7 +212,7 @@ def validate_use_legacy(value): f"All i2s_audio components must set {CONF_USE_LEGACY} to the same value." ) if (not value[CONF_USE_LEGACY]) and (CORE.using_arduino): - raise cv.Invalid("Arduino supports only the legacy i2s driver.") + raise cv.Invalid("Arduino supports only the legacy i2s driver") _use_legacy_driver = value[CONF_USE_LEGACY] return value diff --git a/esphome/components/i2s_audio/media_player/__init__.py b/esphome/components/i2s_audio/media_player/__init__.py index ad6665a5f5..316ce7c48b 100644 --- a/esphome/components/i2s_audio/media_player/__init__.py +++ b/esphome/components/i2s_audio/media_player/__init__.py @@ -92,7 +92,7 @@ CONFIG_SCHEMA = cv.All( def _final_validate(_): if not use_legacy(): - raise cv.Invalid("I2S media player is only compatible with legacy i2s driver.") + raise cv.Invalid("I2S media player is only compatible with legacy i2s driver") FINAL_VALIDATE_SCHEMA = _final_validate diff --git a/esphome/components/i2s_audio/microphone/__init__.py b/esphome/components/i2s_audio/microphone/__init__.py index 0f02ba6c3a..f919199c60 100644 --- a/esphome/components/i2s_audio/microphone/__init__.py +++ b/esphome/components/i2s_audio/microphone/__init__.py @@ -122,7 +122,7 @@ CONFIG_SCHEMA = cv.All( def _final_validate(config): if not use_legacy() and config[CONF_ADC_TYPE] == "internal": - raise cv.Invalid("Internal ADC is only compatible with legacy i2s driver.") + raise cv.Invalid("Internal ADC is only compatible with legacy i2s driver") FINAL_VALIDATE_SCHEMA = _final_validate diff --git a/esphome/components/i2s_audio/speaker/__init__.py b/esphome/components/i2s_audio/speaker/__init__.py index cb7b876a40..98322d3a18 100644 --- a/esphome/components/i2s_audio/speaker/__init__.py +++ b/esphome/components/i2s_audio/speaker/__init__.py @@ -163,7 +163,7 @@ CONFIG_SCHEMA = cv.All( def _final_validate(config): if not use_legacy(): if config[CONF_DAC_TYPE] == "internal": - raise cv.Invalid("Internal DAC is only compatible with legacy i2s driver.") + raise cv.Invalid("Internal DAC is only compatible with legacy i2s driver") if config[CONF_I2S_COMM_FMT] == "stand_max": raise cv.Invalid( "I2S standard max format only implemented with legacy i2s driver." diff --git a/esphome/components/micro_wake_word/__init__.py b/esphome/components/micro_wake_word/__init__.py index cde8752157..8cd7115368 100644 --- a/esphome/components/micro_wake_word/__init__.py +++ b/esphome/components/micro_wake_word/__init__.py @@ -201,7 +201,7 @@ def _validate_manifest_version(manifest_data): else: raise cv.Invalid("Invalid manifest version") else: - raise cv.Invalid("Invalid manifest file, missing 'version' key.") + raise cv.Invalid("Invalid manifest file, missing 'version' key") def _process_http_source(config): @@ -421,7 +421,7 @@ def _feature_step_size_validate(config): if features_step_size is None: features_step_size = model_step_size elif features_step_size != model_step_size: - raise cv.Invalid("Cannot load models with different features step sizes.") + raise cv.Invalid("Cannot load models with different features step sizes") FINAL_VALIDATE_SCHEMA = cv.All( diff --git a/esphome/components/speaker/media_player/__init__.py b/esphome/components/speaker/media_player/__init__.py index 3ae7b980d3..69ea0a53c6 100644 --- a/esphome/components/speaker/media_player/__init__.py +++ b/esphome/components/speaker/media_player/__init__.py @@ -147,7 +147,7 @@ def _read_audio_file_and_type(file_config): elif file_source == TYPE_WEB: path = _compute_local_file_path(conf_file) else: - raise cv.Invalid("Unsupported file source.") + raise cv.Invalid("Unsupported file source") with open(path, "rb") as f: data = f.read() @@ -219,7 +219,7 @@ def _validate_supported_local_file(config): for file_config in config.get(CONF_FILES, []): _, media_file_type = _read_audio_file_and_type(file_config) if str(media_file_type) == str(audio.AUDIO_FILE_TYPE_ENUM["NONE"]): - raise cv.Invalid("Unsupported local media file.") + raise cv.Invalid("Unsupported local media file") if not config[CONF_CODEC_SUPPORT_ENABLED] and str(media_file_type) != str( audio.AUDIO_FILE_TYPE_ENUM["WAV"] ): From 71efaf097b4f86da7a752e12e2860329d842bfc1 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 15 Aug 2025 01:49:14 +1200 Subject: [PATCH 010/208] [esp32_ble] Add ``USE_ESP32_BLE_UUID`` when advertising is desired (#10230) --- esphome/components/esp32_ble/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index c758b3ef8f..2edd69c6c0 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -294,6 +294,7 @@ async def to_code(config): if config[CONF_ADVERTISING]: cg.add_define("USE_ESP32_BLE_ADVERTISING") + cg.add_define("USE_ESP32_BLE_UUID") @automation.register_condition("ble.enabled", BLEEnabledCondition, cv.Schema({})) From 882237120ec6ff6c900553bc949c1fb314de1d2a Mon Sep 17 00:00:00 2001 From: Katherine Whitlock Date: Thu, 14 Aug 2025 16:14:53 -0400 Subject: [PATCH 011/208] Improve error reporting for add_library (#10226) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/core/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index 9df5da1c78..8a9630735e 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -803,6 +803,10 @@ class EsphomeCore: raise TypeError( f"Library {library} must be instance of Library, not {type(library)}" ) + + if not library.name: + raise ValueError(f"The library for {library.repository} must have a name") + short_name = ( library.name if "/" not in library.name else library.name.split("/")[-1] ) From 4f29b3c7aa4f77b0f10a2147730899f082409f96 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 14 Aug 2025 17:43:45 -0500 Subject: [PATCH 012/208] [wifi] Automatically disable Enterprise WiFi support when EAP is not configured (#10242) --- esphome/components/wifi/__init__.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index ac002eac53..4013e8f400 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -375,11 +375,16 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) cg.add(var.set_use_address(config[CONF_USE_ADDRESS])) + # Track if any network uses Enterprise authentication + has_eap = False + def add_sta(ap, network): ip_config = network.get(CONF_MANUAL_IP, config.get(CONF_MANUAL_IP)) cg.add(var.add_sta(wifi_network(network, ap, ip_config))) for network in config.get(CONF_NETWORKS, []): + if CONF_EAP in network: + has_eap = True cg.with_local_variable(network[CONF_ID], WiFiAP(), add_sta, network) if CONF_AP in config: @@ -396,6 +401,10 @@ async def to_code(config): add_idf_sdkconfig_option("CONFIG_ESP_WIFI_SOFTAP_SUPPORT", False) add_idf_sdkconfig_option("CONFIG_LWIP_DHCPS", False) + # Disable Enterprise WiFi support if no EAP is configured + if CORE.is_esp32 and CORE.using_esp_idf and not has_eap: + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT", False) + cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT])) cg.add(var.set_power_save_mode(config[CONF_POWER_SAVE_MODE])) cg.add(var.set_fast_connect(config[CONF_FAST_CONNECT])) From 8ea1a3ed648f8718abb1eb5e3910690ab4cb28b7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 14 Aug 2025 17:50:03 -0500 Subject: [PATCH 013/208] [core] Trigger clean build when components are removed from configuration (#10235) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/writer.py | 18 ++- tests/unit_tests/test_writer.py | 220 ++++++++++++++++++++++++++++++++ 2 files changed, 234 insertions(+), 4 deletions(-) create mode 100644 tests/unit_tests/test_writer.py diff --git a/esphome/writer.py b/esphome/writer.py index b5c834722a..4b25a25f7e 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -80,13 +80,16 @@ def replace_file_content(text, pattern, repl): return content_new, count -def storage_should_clean(old: StorageJSON, new: StorageJSON) -> bool: +def storage_should_clean(old: StorageJSON | None, new: StorageJSON) -> bool: if old is None: return True if old.src_version != new.src_version: return True - return old.build_path != new.build_path + if old.build_path != new.build_path: + return True + # Check if any components have been removed + return bool(old.loaded_integrations - new.loaded_integrations) def storage_should_update_cmake_cache(old: StorageJSON, new: StorageJSON) -> bool: @@ -100,7 +103,7 @@ def storage_should_update_cmake_cache(old: StorageJSON, new: StorageJSON) -> boo return False -def update_storage_json(): +def update_storage_json() -> None: path = storage_path() old = StorageJSON.load(path) new = StorageJSON.from_esphome_core(CORE, old) @@ -108,7 +111,14 @@ def update_storage_json(): return if storage_should_clean(old, new): - _LOGGER.info("Core config, version changed, cleaning build files...") + if old is not None and old.loaded_integrations - new.loaded_integrations: + removed = old.loaded_integrations - new.loaded_integrations + _LOGGER.info( + "Components removed (%s), cleaning build files...", + ", ".join(sorted(removed)), + ) + else: + _LOGGER.info("Core config or version changed, cleaning build files...") clean_build() elif storage_should_update_cmake_cache(old, new): _LOGGER.info("Integrations changed, cleaning cmake cache...") diff --git a/tests/unit_tests/test_writer.py b/tests/unit_tests/test_writer.py new file mode 100644 index 0000000000..f47947ff37 --- /dev/null +++ b/tests/unit_tests/test_writer.py @@ -0,0 +1,220 @@ +"""Test writer module functionality.""" + +from collections.abc import Callable +from typing import Any +from unittest.mock import MagicMock, patch + +import pytest + +from esphome.storage_json import StorageJSON +from esphome.writer import storage_should_clean, update_storage_json + + +@pytest.fixture +def create_storage() -> Callable[..., StorageJSON]: + """Factory fixture to create StorageJSON instances.""" + + def _create( + loaded_integrations: list[str] | None = None, **kwargs: Any + ) -> StorageJSON: + return StorageJSON( + storage_version=kwargs.get("storage_version", 1), + name=kwargs.get("name", "test"), + friendly_name=kwargs.get("friendly_name", "Test Device"), + comment=kwargs.get("comment"), + esphome_version=kwargs.get("esphome_version", "2025.1.0"), + src_version=kwargs.get("src_version", 1), + address=kwargs.get("address", "test.local"), + web_port=kwargs.get("web_port", 80), + target_platform=kwargs.get("target_platform", "ESP32"), + build_path=kwargs.get("build_path", "/build"), + firmware_bin_path=kwargs.get("firmware_bin_path", "/firmware.bin"), + loaded_integrations=set(loaded_integrations or []), + loaded_platforms=kwargs.get("loaded_platforms", set()), + no_mdns=kwargs.get("no_mdns", False), + framework=kwargs.get("framework", "arduino"), + core_platform=kwargs.get("core_platform", "esp32"), + ) + + return _create + + +def test_storage_should_clean_when_old_is_none( + create_storage: Callable[..., StorageJSON], +) -> None: + """Test that clean is triggered when old storage is None.""" + new = create_storage(loaded_integrations=["api", "wifi"]) + assert storage_should_clean(None, new) is True + + +def test_storage_should_clean_when_src_version_changes( + create_storage: Callable[..., StorageJSON], +) -> None: + """Test that clean is triggered when src_version changes.""" + old = create_storage(loaded_integrations=["api", "wifi"], src_version=1) + new = create_storage(loaded_integrations=["api", "wifi"], src_version=2) + assert storage_should_clean(old, new) is True + + +def test_storage_should_clean_when_build_path_changes( + create_storage: Callable[..., StorageJSON], +) -> None: + """Test that clean is triggered when build_path changes.""" + old = create_storage(loaded_integrations=["api", "wifi"], build_path="/build1") + new = create_storage(loaded_integrations=["api", "wifi"], build_path="/build2") + assert storage_should_clean(old, new) is True + + +def test_storage_should_clean_when_component_removed( + create_storage: Callable[..., StorageJSON], +) -> None: + """Test that clean is triggered when a component is removed.""" + old = create_storage( + loaded_integrations=["api", "wifi", "bluetooth_proxy", "esp32_ble_tracker"] + ) + new = create_storage(loaded_integrations=["api", "wifi", "esp32_ble_tracker"]) + assert storage_should_clean(old, new) is True + + +def test_storage_should_clean_when_multiple_components_removed( + create_storage: Callable[..., StorageJSON], +) -> None: + """Test that clean is triggered when multiple components are removed.""" + old = create_storage( + loaded_integrations=["api", "wifi", "ota", "web_server", "logger"] + ) + new = create_storage(loaded_integrations=["api", "wifi", "logger"]) + assert storage_should_clean(old, new) is True + + +def test_storage_should_not_clean_when_nothing_changes( + create_storage: Callable[..., StorageJSON], +) -> None: + """Test that clean is not triggered when nothing changes.""" + old = create_storage(loaded_integrations=["api", "wifi", "logger"]) + new = create_storage(loaded_integrations=["api", "wifi", "logger"]) + assert storage_should_clean(old, new) is False + + +def test_storage_should_not_clean_when_component_added( + create_storage: Callable[..., StorageJSON], +) -> None: + """Test that clean is not triggered when a component is only added.""" + old = create_storage(loaded_integrations=["api", "wifi"]) + new = create_storage(loaded_integrations=["api", "wifi", "ota"]) + assert storage_should_clean(old, new) is False + + +def test_storage_should_not_clean_when_other_fields_change( + create_storage: Callable[..., StorageJSON], +) -> None: + """Test that clean is not triggered when non-relevant fields change.""" + old = create_storage( + loaded_integrations=["api", "wifi"], + friendly_name="Old Name", + esphome_version="2024.12.0", + ) + new = create_storage( + loaded_integrations=["api", "wifi"], + friendly_name="New Name", + esphome_version="2025.1.0", + ) + assert storage_should_clean(old, new) is False + + +def test_storage_edge_case_empty_integrations( + create_storage: Callable[..., StorageJSON], +) -> None: + """Test edge case when old has integrations but new has none.""" + old = create_storage(loaded_integrations=["api", "wifi"]) + new = create_storage(loaded_integrations=[]) + assert storage_should_clean(old, new) is True + + +def test_storage_edge_case_from_empty_integrations( + create_storage: Callable[..., StorageJSON], +) -> None: + """Test edge case when old has no integrations but new has some.""" + old = create_storage(loaded_integrations=[]) + new = create_storage(loaded_integrations=["api", "wifi"]) + assert storage_should_clean(old, new) is False + + +@patch("esphome.writer.clean_build") +@patch("esphome.writer.StorageJSON") +@patch("esphome.writer.storage_path") +@patch("esphome.writer.CORE") +def test_update_storage_json_logging_when_old_is_none( + mock_core: MagicMock, + mock_storage_path: MagicMock, + mock_storage_json_class: MagicMock, + mock_clean_build: MagicMock, + create_storage: Callable[..., StorageJSON], + caplog: pytest.LogCaptureFixture, +) -> None: + """Test that update_storage_json doesn't crash when old storage is None. + + This is a regression test for the AttributeError that occurred when + old was None and we tried to access old.loaded_integrations. + """ + # Setup mocks + mock_storage_path.return_value = "/test/path" + mock_storage_json_class.load.return_value = None # Old storage is None + + new_storage = create_storage(loaded_integrations=["api", "wifi"]) + new_storage.save = MagicMock() # Mock the save method + mock_storage_json_class.from_esphome_core.return_value = new_storage + + # Call the function - should not raise AttributeError + with caplog.at_level("INFO"): + update_storage_json() + + # Verify clean_build was called + mock_clean_build.assert_called_once() + + # Verify the correct log message was used (not the component removal message) + assert "Core config or version changed, cleaning build files..." in caplog.text + assert "Components removed" not in caplog.text + + # Verify save was called + new_storage.save.assert_called_once_with("/test/path") + + +@patch("esphome.writer.clean_build") +@patch("esphome.writer.StorageJSON") +@patch("esphome.writer.storage_path") +@patch("esphome.writer.CORE") +def test_update_storage_json_logging_components_removed( + mock_core: MagicMock, + mock_storage_path: MagicMock, + mock_storage_json_class: MagicMock, + mock_clean_build: MagicMock, + create_storage: Callable[..., StorageJSON], + caplog: pytest.LogCaptureFixture, +) -> None: + """Test that update_storage_json logs removed components correctly.""" + # Setup mocks + mock_storage_path.return_value = "/test/path" + + old_storage = create_storage(loaded_integrations=["api", "wifi", "bluetooth_proxy"]) + new_storage = create_storage(loaded_integrations=["api", "wifi"]) + new_storage.save = MagicMock() # Mock the save method + + mock_storage_json_class.load.return_value = old_storage + mock_storage_json_class.from_esphome_core.return_value = new_storage + + # Call the function + with caplog.at_level("INFO"): + update_storage_json() + + # Verify clean_build was called + mock_clean_build.assert_called_once() + + # Verify the correct log message was used with component names + assert ( + "Components removed (bluetooth_proxy), cleaning build files..." in caplog.text + ) + assert "Core config or version changed" not in caplog.text + + # Verify save was called + new_storage.save.assert_called_once_with("/test/path") From 117cffd2b0db96f466ddec2f0ff79108001fd55f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 14 Aug 2025 17:51:15 -0500 Subject: [PATCH 014/208] [bluetooth_proxy] Remove redundant connection type check after V1 removal (#10208) --- .../components/bluetooth_proxy/bluetooth_connection.cpp | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp index 347f60c28f..d2cbdeb984 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp @@ -133,7 +133,7 @@ void BluetoothConnection::loop() { // Check if we should disable the loop // - For V3_WITH_CACHE: Services are never sent, disable after INIT state - // - For other connections: Disable only after service discovery is complete + // - For V3_WITHOUT_CACHE: Disable only after service discovery is complete // (send_service_ == DONE_SENDING_SERVICES, which is only set after services are sent) if (this->state_ != espbt::ClientState::INIT && (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE || this->send_service_ == DONE_SENDING_SERVICES)) { @@ -160,10 +160,7 @@ void BluetoothConnection::send_service_for_discovery_() { if (this->send_service_ >= this->service_count_) { this->send_service_ = DONE_SENDING_SERVICES; this->proxy_->send_gatt_services_done(this->address_); - if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE || - this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) { - this->release_services(); - } + this->release_services(); return; } From 5d18afcd99addd56b4ef783afc4c712d49d81500 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 14 Aug 2025 22:54:35 +0000 Subject: [PATCH 015/208] Bump ruff from 0.12.8 to 0.12.9 (#10239) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston --- .pre-commit-config.yaml | 2 +- requirements_test.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1830e7881c..5540733131 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ ci: repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.12.8 + rev: v0.12.9 hooks: # Run the linter. - id: ruff diff --git a/requirements_test.txt b/requirements_test.txt index 9ad4591a04..f0a16fd7f3 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==3.3.8 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating -ruff==0.12.8 # also change in .pre-commit-config.yaml when updating +ruff==0.12.9 # also change in .pre-commit-config.yaml when updating pyupgrade==3.20.0 # also change in .pre-commit-config.yaml when updating pre-commit From 5fa84439c2816e94fece2bcd7d5ee36be24fb962 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 14 Aug 2025 20:26:09 -0500 Subject: [PATCH 016/208] [api] Optimize message buffer allocation and eliminate redundant methods (#10231) --- esphome/components/api/api_connection.cpp | 32 +++++++++--------- esphome/components/api/api_connection.h | 41 ++++------------------- 2 files changed, 24 insertions(+), 49 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index cdeabb5cac..ced0f489be 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -289,16 +289,26 @@ uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint8_t mess return 0; // Doesn't fit } - // Allocate buffer space - pass payload size, allocation functions add header/footer space - ProtoWriteBuffer buffer = is_single ? conn->allocate_single_message_buffer(calculated_size) - : conn->allocate_batch_message_buffer(calculated_size); - // Get buffer size after allocation (which includes header padding) std::vector &shared_buf = conn->parent_->get_shared_buffer_ref(); - size_t size_before_encode = shared_buf.size(); + + if (is_single || conn->flags_.batch_first_message) { + // Single message or first batch message + conn->prepare_first_message_buffer(shared_buf, header_padding, total_calculated_size); + if (conn->flags_.batch_first_message) { + conn->flags_.batch_first_message = false; + } + } else { + // Batch message second or later + // Add padding for previous message footer + this message header + size_t current_size = shared_buf.size(); + shared_buf.reserve(current_size + total_calculated_size); + shared_buf.resize(current_size + footer_size + header_padding); + } // Encode directly into buffer - msg.encode(buffer); + size_t size_before_encode = shared_buf.size(); + msg.encode({&shared_buf}); // Calculate actual encoded size (not including header that was already added) size_t actual_payload_size = shared_buf.size() - size_before_encode; @@ -1620,14 +1630,6 @@ bool APIConnection::schedule_batch_() { return true; } -ProtoWriteBuffer APIConnection::allocate_single_message_buffer(uint16_t size) { return this->create_buffer(size); } - -ProtoWriteBuffer APIConnection::allocate_batch_message_buffer(uint16_t size) { - ProtoWriteBuffer result = this->prepare_message_buffer(size, this->flags_.batch_first_message); - this->flags_.batch_first_message = false; - return result; -} - void APIConnection::process_batch_() { // Ensure PacketInfo remains trivially destructible for our placement new approach static_assert(std::is_trivially_destructible::value, @@ -1735,7 +1737,7 @@ void APIConnection::process_batch_() { } remaining_size -= payload_size; // Calculate where the next message's header padding will start - // Current buffer size + footer space (that prepare_message_buffer will add for this message) + // Current buffer size + footer space for this message current_offset = shared_buf.size() + footer_size; } diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index f0f308c248..076dccfad7 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -252,44 +252,21 @@ class APIConnection : public APIServerConnection { // Get header padding size - used for both reserve and insert uint8_t header_padding = this->helper_->frame_header_padding(); - // Get shared buffer from parent server std::vector &shared_buf = this->parent_->get_shared_buffer_ref(); + this->prepare_first_message_buffer(shared_buf, header_padding, + reserve_size + header_padding + this->helper_->frame_footer_size()); + return {&shared_buf}; + } + + void prepare_first_message_buffer(std::vector &shared_buf, size_t header_padding, size_t total_size) { shared_buf.clear(); // Reserve space for header padding + message + footer // - Header padding: space for protocol headers (7 bytes for Noise, 6 for Plaintext) // - Footer: space for MAC (16 bytes for Noise, 0 for Plaintext) - shared_buf.reserve(reserve_size + header_padding + this->helper_->frame_footer_size()); + shared_buf.reserve(total_size); // Resize to add header padding so message encoding starts at the correct position shared_buf.resize(header_padding); - return {&shared_buf}; - } - - // Prepare buffer for next message in batch - ProtoWriteBuffer prepare_message_buffer(uint16_t message_size, bool is_first_message) { - // Get reference to shared buffer (it maintains state between batch messages) - std::vector &shared_buf = this->parent_->get_shared_buffer_ref(); - - if (is_first_message) { - shared_buf.clear(); - } - - size_t current_size = shared_buf.size(); - - // Calculate padding to add: - // - First message: just header padding - // - Subsequent messages: footer for previous message + header padding for this message - size_t padding_to_add = is_first_message - ? this->helper_->frame_header_padding() - : this->helper_->frame_header_padding() + this->helper_->frame_footer_size(); - - // Reserve space for padding + message - shared_buf.reserve(current_size + padding_to_add + message_size); - - // Resize to add the padding bytes - shared_buf.resize(current_size + padding_to_add); - - return {&shared_buf}; } bool try_to_clear_buffer(bool log_out_of_space); @@ -297,10 +274,6 @@ class APIConnection : public APIServerConnection { std::string get_client_combined_info() const { return this->client_info_.get_combined_info(); } - // Buffer allocator methods for batch processing - ProtoWriteBuffer allocate_single_message_buffer(uint16_t size); - ProtoWriteBuffer allocate_batch_message_buffer(uint16_t size); - protected: // Helper function to handle authentication completion void complete_authentication_(); From af9ecf3429b410046162ba951b765ab8c3e256a8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 15 Aug 2025 02:38:27 -0500 Subject: [PATCH 017/208] [esp32_ble] Optimize BLE event memory usage by eliminating std::vector overhead (#10247) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/esp32_ble/ble_event.h | 57 +++++++++++++++++------- 1 file changed, 41 insertions(+), 16 deletions(-) diff --git a/esphome/components/esp32_ble/ble_event.h b/esphome/components/esp32_ble/ble_event.h index 884fc9ba65..2c0ab1d34e 100644 --- a/esphome/components/esp32_ble/ble_event.h +++ b/esphome/components/esp32_ble/ble_event.h @@ -3,8 +3,7 @@ #ifdef USE_ESP32 #include // for offsetof -#include - +#include // for memcpy #include #include #include @@ -64,7 +63,7 @@ static_assert(offsetof(esp_ble_gap_cb_param_t, read_rssi_cmpl.remote_addr) == si // 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. +// GAP events (99% of traffic) don't have the heap allocation overhead. // GATTC/GATTS events use heap allocation for their param and data. // // Event flow: @@ -145,16 +144,18 @@ class BLEEvent { } if (this->type_ == GATTC) { delete this->event_.gattc.gattc_param; - delete this->event_.gattc.data; + delete[] this->event_.gattc.data; this->event_.gattc.gattc_param = nullptr; this->event_.gattc.data = nullptr; + this->event_.gattc.data_len = 0; return; } if (this->type_ == GATTS) { delete this->event_.gatts.gatts_param; - delete this->event_.gatts.data; + delete[] this->event_.gatts.data; this->event_.gatts.gatts_param = nullptr; this->event_.gatts.data = nullptr; + this->event_.gatts.data_len = 0; } } @@ -209,17 +210,19 @@ class BLEEvent { esp_gattc_cb_event_t gattc_event; esp_gatt_if_t gattc_if; esp_ble_gattc_cb_param_t *gattc_param; // Heap-allocated - std::vector *data; // Heap-allocated - } gattc; // 16 bytes (pointers only) + uint8_t *data; // Heap-allocated raw buffer (manually managed) + uint16_t data_len; // Track size separately + } gattc; // NOLINTNEXTLINE(readability-identifier-naming) struct gatts_event { esp_gatts_cb_event_t gatts_event; esp_gatt_if_t gatts_if; esp_ble_gatts_cb_param_t *gatts_param; // Heap-allocated - std::vector *data; // Heap-allocated - } gatts; // 16 bytes (pointers only) - } event_; // 80 bytes + uint8_t *data; // Heap-allocated raw buffer (manually managed) + uint16_t data_len; // Track size separately + } gatts; + } event_; // 80 bytes ble_event_t type_; @@ -319,6 +322,7 @@ class BLEEvent { if (p == nullptr) { this->event_.gattc.gattc_param = nullptr; this->event_.gattc.data = nullptr; + this->event_.gattc.data_len = 0; return; // Invalid event, but we can't log in header file } @@ -336,16 +340,29 @@ class BLEEvent { // 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(p->notify.value, p->notify.value + p->notify.value_len); - this->event_.gattc.gattc_param->notify.value = this->event_.gattc.data->data(); + this->event_.gattc.data_len = p->notify.value_len; + if (p->notify.value_len > 0) { + this->event_.gattc.data = new uint8_t[p->notify.value_len]; + memcpy(this->event_.gattc.data, p->notify.value, p->notify.value_len); + } else { + this->event_.gattc.data = nullptr; + } + this->event_.gattc.gattc_param->notify.value = this->event_.gattc.data; break; case ESP_GATTC_READ_CHAR_EVT: case ESP_GATTC_READ_DESCR_EVT: - this->event_.gattc.data = new std::vector(p->read.value, p->read.value + p->read.value_len); - this->event_.gattc.gattc_param->read.value = this->event_.gattc.data->data(); + this->event_.gattc.data_len = p->read.value_len; + if (p->read.value_len > 0) { + this->event_.gattc.data = new uint8_t[p->read.value_len]; + memcpy(this->event_.gattc.data, p->read.value, p->read.value_len); + } else { + this->event_.gattc.data = nullptr; + } + this->event_.gattc.gattc_param->read.value = this->event_.gattc.data; break; default: this->event_.gattc.data = nullptr; + this->event_.gattc.data_len = 0; break; } } @@ -358,6 +375,7 @@ class BLEEvent { if (p == nullptr) { this->event_.gatts.gatts_param = nullptr; this->event_.gatts.data = nullptr; + this->event_.gatts.data_len = 0; return; // Invalid event, but we can't log in header file } @@ -375,11 +393,18 @@ class BLEEvent { // 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(p->write.value, p->write.value + p->write.len); - this->event_.gatts.gatts_param->write.value = this->event_.gatts.data->data(); + this->event_.gatts.data_len = p->write.len; + if (p->write.len > 0) { + this->event_.gatts.data = new uint8_t[p->write.len]; + memcpy(this->event_.gatts.data, p->write.value, p->write.len); + } else { + this->event_.gatts.data = nullptr; + } + this->event_.gatts.gatts_param->write.value = this->event_.gatts.data; break; default: this->event_.gatts.data = nullptr; + this->event_.gatts.data_len = 0; break; } } From abecc0e8d8dca98bdb2978eb83c395b14554f7b6 Mon Sep 17 00:00:00 2001 From: RFDarter Date: Fri, 15 Aug 2025 16:44:24 +0200 Subject: [PATCH 018/208] [web_server] fix cover_all_json_generator wrong detail (#10252) --- esphome/components/web_server/web_server.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 92c5961f87..399b8785ae 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -813,7 +813,7 @@ std::string WebServer::cover_state_json_generator(WebServer *web_server, void *s return web_server->cover_json((cover::Cover *) (source), DETAIL_STATE); } std::string WebServer::cover_all_json_generator(WebServer *web_server, void *source) { - return web_server->cover_json((cover::Cover *) (source), DETAIL_STATE); + return web_server->cover_json((cover::Cover *) (source), DETAIL_ALL); } std::string WebServer::cover_json(cover::Cover *obj, JsonDetail start_config) { return json::build_json([this, obj, start_config](JsonObject root) { From 6c5632a0b3db4e1c0c23b4f9bb8cba70d1b904b7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 15 Aug 2025 10:11:49 -0500 Subject: [PATCH 019/208] [esp32] Optimize preferences is_changed() by replacing temporary vector with unique_ptr (#10246) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/esp32/preferences.cpp | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/esphome/components/esp32/preferences.cpp b/esphome/components/esp32/preferences.cpp index e53cdd90d3..c5b07b497c 100644 --- a/esphome/components/esp32/preferences.cpp +++ b/esphome/components/esp32/preferences.cpp @@ -8,6 +8,7 @@ #include #include #include +#include namespace esphome { namespace esp32 { @@ -156,20 +157,23 @@ class ESP32Preferences : public ESPPreferences { return failed == 0; } bool is_changed(const uint32_t nvs_handle, const NVSData &to_save) { - NVSData stored_data{}; size_t actual_len; esp_err_t err = nvs_get_blob(nvs_handle, to_save.key.c_str(), nullptr, &actual_len); if (err != 0) { ESP_LOGV(TAG, "nvs_get_blob('%s'): %s - the key might not be set yet", to_save.key.c_str(), esp_err_to_name(err)); return true; } - stored_data.data.resize(actual_len); - err = nvs_get_blob(nvs_handle, to_save.key.c_str(), stored_data.data.data(), &actual_len); + // Check size first before allocating memory + if (actual_len != to_save.data.size()) { + return true; + } + auto stored_data = std::make_unique(actual_len); + err = nvs_get_blob(nvs_handle, to_save.key.c_str(), stored_data.get(), &actual_len); if (err != 0) { ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", to_save.key.c_str(), esp_err_to_name(err)); return true; } - return to_save.data != stored_data.data; + return memcmp(to_save.data.data(), stored_data.get(), to_save.data.size()) != 0; } bool reset() override { From daf8ec36abd48bbc51484196a3faa3403f73c256 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 15 Aug 2025 22:26:48 -0400 Subject: [PATCH 020/208] [core] Remove unnecessary FD_SETSIZE check on ESP32 and improve logging (#10255) --- esphome/core/application.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 73bf13ab7c..d2d47fe171 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -475,11 +475,16 @@ bool Application::register_socket_fd(int fd) { if (fd < 0) return false; +#ifndef USE_ESP32 + // Only check on non-ESP32 platforms + // On ESP32 (both Arduino and ESP-IDF), CONFIG_LWIP_MAX_SOCKETS is always <= FD_SETSIZE by design + // (LWIP_SOCKET_OFFSET = FD_SETSIZE - CONFIG_LWIP_MAX_SOCKETS per lwipopts.h) + // Other platforms may not have this guarantee if (fd >= FD_SETSIZE) { - ESP_LOGE(TAG, "Cannot monitor socket fd %d: exceeds FD_SETSIZE (%d)", fd, FD_SETSIZE); - ESP_LOGE(TAG, "Socket will not be monitored for data - may cause performance issues!"); + ESP_LOGE(TAG, "fd %d exceeds FD_SETSIZE %d", fd, FD_SETSIZE); return false; } +#endif this->socket_fds_.push_back(fd); this->socket_fds_changed_ = true; From 75f3adcd9537e158b9b0688e885190eb35349c3f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 17 Aug 2025 15:49:50 -0400 Subject: [PATCH 021/208] [esp32_ble] Store GATTC/GATTS param and small data inline to nearly eliminate heap allocations (#10249) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/esp32_ble/ble.cpp | 4 +- esphome/components/esp32_ble/ble_event.h | 219 ++++++++++++++--------- 2 files changed, 133 insertions(+), 90 deletions(-) diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index d1ee7af4ea..e22d43c0cc 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -306,7 +306,7 @@ void ESP32BLE::loop() { case BLEEvent::GATTS: { esp_gatts_cb_event_t event = ble_event->event_.gatts.gatts_event; esp_gatt_if_t gatts_if = ble_event->event_.gatts.gatts_if; - esp_ble_gatts_cb_param_t *param = ble_event->event_.gatts.gatts_param; + esp_ble_gatts_cb_param_t *param = &ble_event->event_.gatts.gatts_param; ESP_LOGV(TAG, "gatts_event [esp_gatt_if: %d] - %d", gatts_if, event); for (auto *gatts_handler : this->gatts_event_handlers_) { gatts_handler->gatts_event_handler(event, gatts_if, param); @@ -316,7 +316,7 @@ void ESP32BLE::loop() { case BLEEvent::GATTC: { esp_gattc_cb_event_t event = ble_event->event_.gattc.gattc_event; esp_gatt_if_t gattc_if = ble_event->event_.gattc.gattc_if; - esp_ble_gattc_cb_param_t *param = ble_event->event_.gattc.gattc_param; + esp_ble_gattc_cb_param_t *param = &ble_event->event_.gattc.gattc_param; ESP_LOGV(TAG, "gattc_event [esp_gatt_if: %d] - %d", gattc_if, event); for (auto *gattc_handler : this->gattc_event_handlers_) { gattc_handler->gattc_event_handler(event, gattc_if, param); diff --git a/esphome/components/esp32_ble/ble_event.h b/esphome/components/esp32_ble/ble_event.h index 2c0ab1d34e..299fd7705f 100644 --- a/esphome/components/esp32_ble/ble_event.h +++ b/esphome/components/esp32_ble/ble_event.h @@ -61,10 +61,24 @@ static_assert(offsetof(esp_ble_gap_cb_param_t, read_rssi_cmpl.rssi) == sizeof(es 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"); +// Param struct sizes on ESP32 +static constexpr size_t GATTC_PARAM_SIZE = 28; +static constexpr size_t GATTS_PARAM_SIZE = 32; + +// Maximum size for inline storage of data +// GATTC: 80 - 28 (param) - 8 (other fields) = 44 bytes for data +// GATTS: 80 - 32 (param) - 8 (other fields) = 40 bytes for data +static constexpr size_t GATTC_INLINE_DATA_SIZE = 44; +static constexpr size_t GATTS_INLINE_DATA_SIZE = 40; + +// Verify param struct sizes +static_assert(sizeof(esp_ble_gattc_cb_param_t) == GATTC_PARAM_SIZE, "GATTC param size unexpected"); +static_assert(sizeof(esp_ble_gatts_cb_param_t) == GATTS_PARAM_SIZE, "GATTS param size unexpected"); + // 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 heap allocation overhead. -// GATTC/GATTS events use heap allocation for their param and data. +// GATTC/GATTS events use heap allocation for their param and inline storage for small data. // // Event flow: // 1. ESP-IDF BLE stack calls our static handlers in the BLE task context @@ -111,21 +125,21 @@ class BLEEvent { this->init_gap_data_(e, p); } - // Constructor for GATTC events - uses heap allocation - // 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. + // Constructor for GATTC events - param stored inline, data may use heap + // IMPORTANT: We MUST copy the param struct because the pointer from ESP-IDF + // is only valid during the callback execution. Since BLE events are processed + // asynchronously in the main loop, we store our own copy inline 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->init_gattc_data_(e, i, p); } - // Constructor for GATTS events - uses heap allocation - // 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. + // Constructor for GATTS events - param stored inline, data may use heap + // IMPORTANT: We MUST copy the param struct because the pointer from ESP-IDF + // is only valid during the callback execution. Since BLE events are processed + // asynchronously in the main loop, we store our own copy inline 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->init_gatts_data_(e, i, p); @@ -135,27 +149,32 @@ class BLEEvent { ~BLEEvent() { this->release(); } // Default constructor for pre-allocation in pool - BLEEvent() : type_(GAP) {} + BLEEvent() : event_{}, type_(GAP) {} // Invoked on return to EventPool - clean up any heap-allocated data void release() { - if (this->type_ == GAP) { - return; - } - if (this->type_ == GATTC) { - delete this->event_.gattc.gattc_param; - delete[] this->event_.gattc.data; - this->event_.gattc.gattc_param = nullptr; - this->event_.gattc.data = nullptr; - this->event_.gattc.data_len = 0; - return; - } - if (this->type_ == GATTS) { - delete this->event_.gatts.gatts_param; - delete[] this->event_.gatts.data; - this->event_.gatts.gatts_param = nullptr; - this->event_.gatts.data = nullptr; - this->event_.gatts.data_len = 0; + switch (this->type_) { + case GAP: + // GAP events don't have heap allocations + break; + case GATTC: + // Param is now stored inline, only delete heap data if it was heap-allocated + if (!this->event_.gattc.is_inline && this->event_.gattc.data.heap_data != nullptr) { + delete[] this->event_.gattc.data.heap_data; + } + // Clear critical fields to prevent issues if type changes + this->event_.gattc.is_inline = false; + this->event_.gattc.data.heap_data = nullptr; + break; + case GATTS: + // Param is now stored inline, only delete heap data if it was heap-allocated + if (!this->event_.gatts.is_inline && this->event_.gatts.data.heap_data != nullptr) { + delete[] this->event_.gatts.data.heap_data; + } + // Clear critical fields to prevent issues if type changes + this->event_.gatts.is_inline = false; + this->event_.gatts.data.heap_data = nullptr; + break; } } @@ -207,22 +226,30 @@ class BLEEvent { // NOLINTNEXTLINE(readability-identifier-naming) struct gattc_event { - esp_gattc_cb_event_t gattc_event; - esp_gatt_if_t gattc_if; - esp_ble_gattc_cb_param_t *gattc_param; // Heap-allocated - uint8_t *data; // Heap-allocated raw buffer (manually managed) - uint16_t data_len; // Track size separately - } gattc; + esp_ble_gattc_cb_param_t gattc_param; // Stored inline (28 bytes) + esp_gattc_cb_event_t gattc_event; // 4 bytes + union { + uint8_t *heap_data; // 4 bytes when heap-allocated + uint8_t inline_data[GATTC_INLINE_DATA_SIZE]; // 44 bytes when stored inline + } data; // 44 bytes total + uint16_t data_len; // 2 bytes + esp_gatt_if_t gattc_if; // 1 byte + bool is_inline; // 1 byte - true when data is stored inline + } gattc; // Total: 80 bytes // NOLINTNEXTLINE(readability-identifier-naming) struct gatts_event { - esp_gatts_cb_event_t gatts_event; - esp_gatt_if_t gatts_if; - esp_ble_gatts_cb_param_t *gatts_param; // Heap-allocated - uint8_t *data; // Heap-allocated raw buffer (manually managed) - uint16_t data_len; // Track size separately - } gatts; - } event_; // 80 bytes + esp_ble_gatts_cb_param_t gatts_param; // Stored inline (32 bytes) + esp_gatts_cb_event_t gatts_event; // 4 bytes + union { + uint8_t *heap_data; // 4 bytes when heap-allocated + uint8_t inline_data[GATTS_INLINE_DATA_SIZE]; // 40 bytes when stored inline + } data; // 40 bytes total + uint16_t data_len; // 2 bytes + esp_gatt_if_t gatts_if; // 1 byte + bool is_inline; // 1 byte - true when data is stored inline + } gatts; // Total: 80 bytes + } event_; // 80 bytes ble_event_t type_; @@ -236,6 +263,29 @@ class BLEEvent { const esp_ble_sec_t &security() const { return event_.gap.security; } private: + // Helper to copy data with inline storage optimization + template + void copy_data_with_inline_storage_(EventStruct &event, const uint8_t *src_data, uint16_t len, + uint8_t **param_value_ptr) { + event.data_len = len; + if (len > 0) { + if (len <= InlineSize) { + event.is_inline = true; + memcpy(event.data.inline_data, src_data, len); + *param_value_ptr = event.data.inline_data; + } else { + event.is_inline = false; + event.data.heap_data = new uint8_t[len]; + memcpy(event.data.heap_data, src_data, len); + *param_value_ptr = event.data.heap_data; + } + } else { + event.is_inline = false; + event.data.heap_data = nullptr; + *param_value_ptr = nullptr; + } + } + // 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; @@ -320,48 +370,37 @@ class BLEEvent { this->event_.gattc.gattc_if = i; if (p == nullptr) { - this->event_.gattc.gattc_param = nullptr; - this->event_.gattc.data = nullptr; + // Zero out the param struct when null + memset(&this->event_.gattc.gattc_param, 0, sizeof(this->event_.gattc.gattc_param)); + this->event_.gattc.is_inline = false; + this->event_.gattc.data.heap_data = nullptr; this->event_.gattc.data_len = 0; 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 param struct inline (no heap allocation!) + // GATTC/GATTS events are rare (<1% of events) but we can still store them inline + // along with small data payloads, eliminating all heap allocations for typical BLE operations + // CRITICAL: This copy is REQUIRED for memory safety - the ESP-IDF param pointer + // is only valid during the callback and will be reused/freed after we return + this->event_.gattc.gattc_param = *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_len = p->notify.value_len; - if (p->notify.value_len > 0) { - this->event_.gattc.data = new uint8_t[p->notify.value_len]; - memcpy(this->event_.gattc.data, p->notify.value, p->notify.value_len); - } else { - this->event_.gattc.data = nullptr; - } - this->event_.gattc.gattc_param->notify.value = this->event_.gattc.data; + copy_data_with_inline_storage_event_.gattc), GATTC_INLINE_DATA_SIZE>( + this->event_.gattc, p->notify.value, p->notify.value_len, &this->event_.gattc.gattc_param.notify.value); break; case ESP_GATTC_READ_CHAR_EVT: case ESP_GATTC_READ_DESCR_EVT: - this->event_.gattc.data_len = p->read.value_len; - if (p->read.value_len > 0) { - this->event_.gattc.data = new uint8_t[p->read.value_len]; - memcpy(this->event_.gattc.data, p->read.value, p->read.value_len); - } else { - this->event_.gattc.data = nullptr; - } - this->event_.gattc.gattc_param->read.value = this->event_.gattc.data; + copy_data_with_inline_storage_event_.gattc), GATTC_INLINE_DATA_SIZE>( + this->event_.gattc, p->read.value, p->read.value_len, &this->event_.gattc.gattc_param.read.value); break; default: - this->event_.gattc.data = nullptr; + this->event_.gattc.is_inline = false; + this->event_.gattc.data.heap_data = nullptr; this->event_.gattc.data_len = 0; break; } @@ -373,37 +412,32 @@ class BLEEvent { this->event_.gatts.gatts_if = i; if (p == nullptr) { - this->event_.gatts.gatts_param = nullptr; - this->event_.gatts.data = nullptr; + // Zero out the param struct when null + memset(&this->event_.gatts.gatts_param, 0, sizeof(this->event_.gatts.gatts_param)); + this->event_.gatts.is_inline = false; + this->event_.gatts.data.heap_data = nullptr; this->event_.gatts.data_len = 0; 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 param struct inline (no heap allocation!) + // GATTC/GATTS events are rare (<1% of events) but we can still store them inline + // along with small data payloads, eliminating all heap allocations for typical BLE operations + // CRITICAL: This copy is REQUIRED for memory safety - the ESP-IDF param pointer + // is only valid during the callback and will be reused/freed after we return + this->event_.gatts.gatts_param = *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_len = p->write.len; - if (p->write.len > 0) { - this->event_.gatts.data = new uint8_t[p->write.len]; - memcpy(this->event_.gatts.data, p->write.value, p->write.len); - } else { - this->event_.gatts.data = nullptr; - } - this->event_.gatts.gatts_param->write.value = this->event_.gatts.data; + copy_data_with_inline_storage_event_.gatts), GATTS_INLINE_DATA_SIZE>( + this->event_.gatts, p->write.value, p->write.len, &this->event_.gatts.gatts_param.write.value); break; default: - this->event_.gatts.data = nullptr; + this->event_.gatts.is_inline = false; + this->event_.gatts.data.heap_data = nullptr; this->event_.gatts.data_len = 0; break; } @@ -414,6 +448,15 @@ class BLEEvent { // 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 GATTC and GATTS structs don't exceed GAP struct size +// This ensures the union size is determined by GAP (the most common event type) +static_assert(sizeof(decltype(((BLEEvent *) nullptr)->event_.gattc)) <= + sizeof(decltype(((BLEEvent *) nullptr)->event_.gap)), + "gattc_event struct exceeds gap_event size - union size would increase"); +static_assert(sizeof(decltype(((BLEEvent *) nullptr)->event_.gatts)) <= + sizeof(decltype(((BLEEvent *) nullptr)->event_.gap)), + "gatts_event struct exceeds gap_event size - union size would increase"); + // 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"); From 2a3f80a82cc673036167d5371edc5abc169923a4 Mon Sep 17 00:00:00 2001 From: Ben Winslow Date: Sun, 17 Aug 2025 22:09:42 -0400 Subject: [PATCH 022/208] [senseair] Discard 0 ppm readings with "Out Of Range" bit set. (#10275) --- esphome/components/senseair/senseair.cpp | 10 +++++++--- esphome/components/senseair/senseair.h | 11 +++++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/esphome/components/senseair/senseair.cpp b/esphome/components/senseair/senseair.cpp index e58ee157f7..84520d407d 100644 --- a/esphome/components/senseair/senseair.cpp +++ b/esphome/components/senseair/senseair.cpp @@ -53,10 +53,14 @@ void SenseAirComponent::update() { this->status_clear_warning(); const uint8_t length = response[2]; - const uint16_t status = (uint16_t(response[3]) << 8) | response[4]; - const int16_t ppm = int16_t((response[length + 1] << 8) | response[length + 2]); + const uint16_t status = encode_uint16(response[3], response[4]); + const uint16_t ppm = encode_uint16(response[length + 1], response[length + 2]); - ESP_LOGD(TAG, "SenseAir Received CO₂=%dppm Status=0x%02X", ppm, status); + ESP_LOGD(TAG, "SenseAir Received CO₂=%uppm Status=0x%02X", ppm, status); + if (ppm == 0 && (status & SenseAirStatus::OUT_OF_RANGE_ERROR) != 0) { + ESP_LOGD(TAG, "Discarding 0 ppm reading with out-of-range status."); + return; + } if (this->co2_sensor_ != nullptr) this->co2_sensor_->publish_state(ppm); } diff --git a/esphome/components/senseair/senseair.h b/esphome/components/senseair/senseair.h index 9f939d5b07..5b66860f1a 100644 --- a/esphome/components/senseair/senseair.h +++ b/esphome/components/senseair/senseair.h @@ -8,6 +8,17 @@ namespace esphome { namespace senseair { +enum SenseAirStatus : uint8_t { + FATAL_ERROR = 1 << 0, + OFFSET_ERROR = 1 << 1, + ALGORITHM_ERROR = 1 << 2, + OUTPUT_ERROR = 1 << 3, + SELF_DIAGNOSTIC_ERROR = 1 << 4, + OUT_OF_RANGE_ERROR = 1 << 5, + MEMORY_ERROR = 1 << 6, + RESERVED = 1 << 7 +}; + class SenseAirComponent : public PollingComponent, public uart::UARTDevice { public: void set_co2_sensor(sensor::Sensor *co2_sensor) { co2_sensor_ = co2_sensor; } From c29f8d0187b65bd19848f1a2713d2719a1c48fa2 Mon Sep 17 00:00:00 2001 From: Ben Winslow Date: Sun, 17 Aug 2025 22:36:35 -0400 Subject: [PATCH 023/208] [core] Fix post-OTA logs display when using esphome run and MQTT (#10274) --- esphome/__main__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index 7cc8296e7e..8e8fc7d5d9 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -476,7 +476,7 @@ def show_logs(config: ConfigType, args: ArgsProtocol, devices: list[str]) -> int from esphome.components.api.client import run_logs return run_logs(config, addresses_to_use) - if get_port_type(port) == "MQTT" and "mqtt" in config: + if get_port_type(port) in ("NETWORK", "MQTT") and "mqtt" in config: from esphome import mqtt return mqtt.show_logs( From 0a774230731c2741c1feb0f52f8a75a64a99df78 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 18 Aug 2025 09:01:39 -0400 Subject: [PATCH 024/208] [esp8266] Replace std::vector with std::unique_ptr in preferences to save flash (#10245) --- esphome/components/esp8266/preferences.cpp | 41 ++++++++++------------ 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/esphome/components/esp8266/preferences.cpp b/esphome/components/esp8266/preferences.cpp index efd226e8f8..bb7e436bea 100644 --- a/esphome/components/esp8266/preferences.cpp +++ b/esphome/components/esp8266/preferences.cpp @@ -12,7 +12,7 @@ extern "C" { #include "preferences.h" #include -#include +#include namespace esphome { namespace esp8266 { @@ -67,6 +67,8 @@ static uint32_t get_esp8266_flash_sector() { } static uint32_t get_esp8266_flash_address() { return get_esp8266_flash_sector() * SPI_FLASH_SEC_SIZE; } +static inline size_t bytes_to_words(size_t bytes) { return (bytes + 3) / 4; } + template uint32_t calculate_crc(It first, It last, uint32_t type) { uint32_t crc = type; while (first != last) { @@ -123,41 +125,36 @@ class ESP8266PreferenceBackend : public ESPPreferenceBackend { size_t length_words = 0; bool save(const uint8_t *data, size_t len) override { - if ((len + 3) / 4 != length_words) { + if (bytes_to_words(len) != length_words) { return false; } - std::vector buffer; - buffer.resize(length_words + 1); - memcpy(buffer.data(), data, len); - buffer[buffer.size() - 1] = calculate_crc(buffer.begin(), buffer.end() - 1, type); + size_t buffer_size = length_words + 1; + std::unique_ptr buffer(new uint32_t[buffer_size]()); // Note the () for zero-initialization + memcpy(buffer.get(), data, len); + buffer[length_words] = calculate_crc(buffer.get(), buffer.get() + length_words, type); if (in_flash) { - return save_to_flash(offset, buffer.data(), buffer.size()); - } else { - return save_to_rtc(offset, buffer.data(), buffer.size()); + return save_to_flash(offset, buffer.get(), buffer_size); } + return save_to_rtc(offset, buffer.get(), buffer_size); } bool load(uint8_t *data, size_t len) override { - if ((len + 3) / 4 != length_words) { + if (bytes_to_words(len) != length_words) { return false; } - std::vector buffer; - buffer.resize(length_words + 1); - bool ret; - if (in_flash) { - ret = load_from_flash(offset, buffer.data(), buffer.size()); - } else { - ret = load_from_rtc(offset, buffer.data(), buffer.size()); - } + size_t buffer_size = length_words + 1; + std::unique_ptr buffer(new uint32_t[buffer_size]()); + bool ret = in_flash ? load_from_flash(offset, buffer.get(), buffer_size) + : load_from_rtc(offset, buffer.get(), buffer_size); if (!ret) return false; - uint32_t crc = calculate_crc(buffer.begin(), buffer.end() - 1, type); - if (buffer[buffer.size() - 1] != crc) { + uint32_t crc = calculate_crc(buffer.get(), buffer.get() + length_words, type); + if (buffer[length_words] != crc) { return false; } - memcpy(data, buffer.data(), len); + memcpy(data, buffer.get(), len); return true; } }; @@ -178,7 +175,7 @@ class ESP8266Preferences : public ESPPreferences { } ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash) override { - uint32_t length_words = (length + 3) / 4; + uint32_t length_words = bytes_to_words(length); if (in_flash) { uint32_t start = current_flash_offset; uint32_t end = start + length_words + 1; From 6818439109f33e1b782ca9fe28205c0276b1dc12 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 18 Aug 2025 11:14:41 -0400 Subject: [PATCH 025/208] [core] Fix scheduler race condition where cancelled items still execute (#10268) --- esphome/core/scheduler.cpp | 37 ++++- esphome/core/scheduler.h | 55 ++++++- .../fixtures/scheduler_removed_item_race.yaml | 139 ++++++++++++++++++ .../test_scheduler_removed_item_race.py | 102 +++++++++++++ 4 files changed, 328 insertions(+), 5 deletions(-) create mode 100644 tests/integration/fixtures/scheduler_removed_item_race.yaml create mode 100644 tests/integration/test_scheduler_removed_item_race.py diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 6269a66543..c3ade260ac 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -82,7 +82,13 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type item->set_name(name_cstr, !is_static_string); item->type = type; item->callback = std::move(func); + // Initialize remove to false (though it should already be from constructor) + // Not using mark_item_removed_ helper since we're setting to false, not true +#ifdef ESPHOME_THREAD_MULTI_ATOMICS + item->remove.store(false, std::memory_order_relaxed); +#else item->remove = false; +#endif item->is_retry = is_retry; #ifndef ESPHOME_THREAD_SINGLE @@ -398,6 +404,31 @@ void HOT Scheduler::call(uint32_t now) { this->pop_raw_(); continue; } + + // Check if item is marked for removal + // This handles two cases: + // 1. Item was marked for removal after cleanup_() but before we got here + // 2. Item is marked for removal but wasn't at the front of the heap during cleanup_() +#ifdef ESPHOME_THREAD_MULTI_NO_ATOMICS + // Multi-threaded platforms without atomics: must take lock to safely read remove flag + { + LockGuard guard{this->lock_}; + if (is_item_removed_(item.get())) { + this->pop_raw_(); + this->to_remove_--; + continue; + } + } +#else + // Single-threaded or multi-threaded with atomics: can check without lock + if (is_item_removed_(item.get())) { + LockGuard guard{this->lock_}; + this->pop_raw_(); + this->to_remove_--; + continue; + } +#endif + #ifdef ESPHOME_DEBUG_SCHEDULER const char *item_name = item->get_name(); ESP_LOGV(TAG, "Running %s '%s/%s' with interval=%" PRIu32 " next_execution=%" PRIu64 " (now=%" PRIu64 ")", @@ -518,7 +549,7 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c if (type == SchedulerItem::TIMEOUT) { for (auto &item : this->defer_queue_) { if (this->matches_item_(item, component, name_cstr, type, match_retry)) { - item->remove = true; + this->mark_item_removed_(item.get()); total_cancelled++; } } @@ -528,7 +559,7 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c // Cancel items in the main heap for (auto &item : this->items_) { if (this->matches_item_(item, component, name_cstr, type, match_retry)) { - item->remove = true; + this->mark_item_removed_(item.get()); total_cancelled++; this->to_remove_++; // Track removals for heap items } @@ -537,7 +568,7 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c // Cancel items in to_add_ for (auto &item : this->to_add_) { if (this->matches_item_(item, component, name_cstr, type, match_retry)) { - item->remove = true; + this->mark_item_removed_(item.get()); total_cancelled++; // Don't track removals for to_add_ items } diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index a6092e1b1e..c73bd55d5d 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -97,22 +97,42 @@ class Scheduler { std::function callback; - // Bit-packed fields to minimize padding +#ifdef ESPHOME_THREAD_MULTI_ATOMICS + // Multi-threaded with atomics: use atomic for lock-free access + // Place atomic separately since it can't be packed with bit fields + std::atomic remove{false}; + + // Bit-packed fields (3 bits used, 5 bits padding in 1 byte) + enum Type : uint8_t { TIMEOUT, INTERVAL } type : 1; + bool name_is_dynamic : 1; // True if name was dynamically allocated (needs delete[]) + bool is_retry : 1; // True if this is a retry timeout + // 5 bits padding +#else + // Single-threaded or multi-threaded without atomics: can pack all fields together + // Bit-packed fields (4 bits used, 4 bits padding in 1 byte) enum Type : uint8_t { TIMEOUT, INTERVAL } type : 1; bool remove : 1; bool name_is_dynamic : 1; // True if name was dynamically allocated (needs delete[]) bool is_retry : 1; // True if this is a retry timeout - // 4 bits padding + // 4 bits padding +#endif // Constructor SchedulerItem() : component(nullptr), interval(0), next_execution_(0), +#ifdef ESPHOME_THREAD_MULTI_ATOMICS + // remove is initialized in the member declaration as std::atomic{false} + type(TIMEOUT), + name_is_dynamic(false), + is_retry(false) { +#else type(TIMEOUT), remove(false), name_is_dynamic(false), is_retry(false) { +#endif name_.static_name = nullptr; } @@ -219,6 +239,37 @@ class Scheduler { return item->remove || (item->component != nullptr && item->component->is_failed()); } + // Helper to check if item is marked for removal (platform-specific) + // Returns true if item should be skipped, handles platform-specific synchronization + // For ESPHOME_THREAD_MULTI_NO_ATOMICS platforms, the caller must hold the scheduler lock before calling this + // function. + bool is_item_removed_(SchedulerItem *item) const { +#ifdef ESPHOME_THREAD_MULTI_ATOMICS + // Multi-threaded with atomics: use atomic load for lock-free access + return item->remove.load(std::memory_order_acquire); +#else + // Single-threaded (ESPHOME_THREAD_SINGLE) or + // multi-threaded without atomics (ESPHOME_THREAD_MULTI_NO_ATOMICS): direct read + // For ESPHOME_THREAD_MULTI_NO_ATOMICS, caller MUST hold lock! + return item->remove; +#endif + } + + // Helper to mark item for removal (platform-specific) + // For ESPHOME_THREAD_MULTI_NO_ATOMICS platforms, the caller must hold the scheduler lock before calling this + // function. + void mark_item_removed_(SchedulerItem *item) { +#ifdef ESPHOME_THREAD_MULTI_ATOMICS + // Multi-threaded with atomics: use atomic store + item->remove.store(true, std::memory_order_release); +#else + // Single-threaded (ESPHOME_THREAD_SINGLE) or + // multi-threaded without atomics (ESPHOME_THREAD_MULTI_NO_ATOMICS): direct write + // For ESPHOME_THREAD_MULTI_NO_ATOMICS, caller MUST hold lock! + item->remove = true; +#endif + } + // Template helper to check if any item in a container matches our criteria template bool has_cancelled_timeout_in_container_(const Container &container, Component *component, const char *name_cstr, diff --git a/tests/integration/fixtures/scheduler_removed_item_race.yaml b/tests/integration/fixtures/scheduler_removed_item_race.yaml new file mode 100644 index 0000000000..2f8a7fb987 --- /dev/null +++ b/tests/integration/fixtures/scheduler_removed_item_race.yaml @@ -0,0 +1,139 @@ +esphome: + name: scheduler-removed-item-race + +host: + +api: + services: + - service: run_test + then: + - script.execute: run_test_script + +logger: + level: DEBUG + +globals: + - id: test_passed + type: bool + initial_value: 'true' + - id: removed_item_executed + type: int + initial_value: '0' + - id: normal_item_executed + type: int + initial_value: '0' + +sensor: + - platform: template + id: test_sensor + name: "Test Sensor" + update_interval: never + lambda: return 0.0; + +script: + - id: run_test_script + then: + - logger.log: "=== Starting Removed Item Race Test ===" + + # This test creates a scenario where: + # 1. First item in heap is NOT cancelled (cleanup stops immediately) + # 2. Items behind it ARE cancelled (remain in heap after cleanup) + # 3. All items execute at the same time, including cancelled ones + + - lambda: |- + // The key to hitting the race: + // 1. Add items in a specific order to control heap structure + // 2. Cancel ONLY items that won't be at the front + // 3. Ensure the first item stays non-cancelled so cleanup_() stops immediately + + // Schedule all items to execute at the SAME time (1ms from now) + // Using 1ms instead of 0 to avoid defer queue on multi-core platforms + // This ensures they'll all be ready together and go through the heap + const uint32_t exec_time = 1; + + // CRITICAL: Add a non-cancellable item FIRST + // This will be at the front of the heap and block cleanup_() + App.scheduler.set_timeout(id(test_sensor), "blocker", exec_time, []() { + ESP_LOGD("test", "Blocker timeout executed (expected) - was at front of heap"); + id(normal_item_executed)++; + }); + + // Now add items that we WILL cancel + // These will be behind the blocker in the heap + App.scheduler.set_timeout(id(test_sensor), "cancel_1", exec_time, []() { + ESP_LOGE("test", "RACE: Cancelled timeout 1 executed after being cancelled!"); + id(removed_item_executed)++; + id(test_passed) = false; + }); + + App.scheduler.set_timeout(id(test_sensor), "cancel_2", exec_time, []() { + ESP_LOGE("test", "RACE: Cancelled timeout 2 executed after being cancelled!"); + id(removed_item_executed)++; + id(test_passed) = false; + }); + + App.scheduler.set_timeout(id(test_sensor), "cancel_3", exec_time, []() { + ESP_LOGE("test", "RACE: Cancelled timeout 3 executed after being cancelled!"); + id(removed_item_executed)++; + id(test_passed) = false; + }); + + // Add some more normal items + App.scheduler.set_timeout(id(test_sensor), "normal_1", exec_time, []() { + ESP_LOGD("test", "Normal timeout 1 executed (expected)"); + id(normal_item_executed)++; + }); + + App.scheduler.set_timeout(id(test_sensor), "normal_2", exec_time, []() { + ESP_LOGD("test", "Normal timeout 2 executed (expected)"); + id(normal_item_executed)++; + }); + + App.scheduler.set_timeout(id(test_sensor), "normal_3", exec_time, []() { + ESP_LOGD("test", "Normal timeout 3 executed (expected)"); + id(normal_item_executed)++; + }); + + // Force items into the heap before cancelling + App.scheduler.process_to_add(); + + // NOW cancel the items - they're behind "blocker" in the heap + // When cleanup_() runs, it will see "blocker" (not removed) at the front + // and stop immediately, leaving cancel_1, cancel_2, cancel_3 in the heap + bool c1 = App.scheduler.cancel_timeout(id(test_sensor), "cancel_1"); + bool c2 = App.scheduler.cancel_timeout(id(test_sensor), "cancel_2"); + bool c3 = App.scheduler.cancel_timeout(id(test_sensor), "cancel_3"); + + ESP_LOGD("test", "Cancelled items (behind blocker): %s, %s, %s", + c1 ? "true" : "false", + c2 ? "true" : "false", + c3 ? "true" : "false"); + + // The heap now has: + // - "blocker" at front (not cancelled) + // - cancelled items behind it (marked remove=true but still in heap) + // - When all execute at once, cleanup_() stops at "blocker" + // - The loop then executes ALL ready items including cancelled ones + + ESP_LOGD("test", "Setup complete. Blocker at front prevents cleanup of cancelled items behind it"); + + # Wait for all timeouts to execute (or not) + - delay: 20ms + + # Check results + - lambda: |- + ESP_LOGI("test", "=== Test Results ==="); + ESP_LOGI("test", "Normal items executed: %d (expected 4)", id(normal_item_executed)); + ESP_LOGI("test", "Removed items executed: %d (expected 0)", id(removed_item_executed)); + + if (id(removed_item_executed) > 0) { + ESP_LOGE("test", "TEST FAILED: %d cancelled items were executed!", id(removed_item_executed)); + id(test_passed) = false; + } else if (id(normal_item_executed) != 4) { + ESP_LOGE("test", "TEST FAILED: Expected 4 normal items, got %d", id(normal_item_executed)); + id(test_passed) = false; + } else { + ESP_LOGI("test", "TEST PASSED: No cancelled items were executed"); + } + + ESP_LOGI("test", "=== Test Complete ==="); diff --git a/tests/integration/test_scheduler_removed_item_race.py b/tests/integration/test_scheduler_removed_item_race.py new file mode 100644 index 0000000000..3e72bacc0d --- /dev/null +++ b/tests/integration/test_scheduler_removed_item_race.py @@ -0,0 +1,102 @@ +"""Test for scheduler race condition where removed items still execute.""" + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_scheduler_removed_item_race( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that items marked for removal don't execute. + + This test verifies the fix for a race condition where: + 1. cleanup_() only removes items from the front of the heap + 2. Items in the middle of the heap marked for removal still execute + 3. This causes cancelled timeouts to run when they shouldn't + """ + + loop = asyncio.get_running_loop() + test_complete_future: asyncio.Future[bool] = loop.create_future() + + # Track test results + test_passed = False + removed_executed = 0 + normal_executed = 0 + + # Patterns to match + race_pattern = re.compile(r"RACE: .* executed after being cancelled!") + passed_pattern = re.compile(r"TEST PASSED") + failed_pattern = re.compile(r"TEST FAILED") + complete_pattern = re.compile(r"=== Test Complete ===") + normal_count_pattern = re.compile(r"Normal items executed: (\d+)") + removed_count_pattern = re.compile(r"Removed items executed: (\d+)") + + def check_output(line: str) -> None: + """Check log output for test results.""" + nonlocal test_passed, removed_executed, normal_executed + + if race_pattern.search(line): + # Race condition detected - a cancelled item executed + test_passed = False + + if passed_pattern.search(line): + test_passed = True + elif failed_pattern.search(line): + test_passed = False + + normal_match = normal_count_pattern.search(line) + if normal_match: + normal_executed = int(normal_match.group(1)) + + removed_match = removed_count_pattern.search(line) + if removed_match: + removed_executed = int(removed_match.group(1)) + + if not test_complete_future.done() and complete_pattern.search(line): + test_complete_future.set_result(True) + + async with ( + run_compiled(yaml_config, line_callback=check_output), + api_client_connected() as client, + ): + # Verify we can connect + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "scheduler-removed-item-race" + + # List services + _, services = await asyncio.wait_for( + client.list_entities_services(), timeout=5.0 + ) + + # Find run_test service + run_test_service = next((s for s in services if s.name == "run_test"), None) + assert run_test_service is not None, "run_test service not found" + + # Execute the test + client.execute_service(run_test_service, {}) + + # Wait for test completion + try: + await asyncio.wait_for(test_complete_future, timeout=5.0) + except TimeoutError: + pytest.fail("Test did not complete within timeout") + + # Verify results + assert test_passed, ( + f"Test failed! Removed items executed: {removed_executed}, " + f"Normal items executed: {normal_executed}" + ) + assert removed_executed == 0, ( + f"Cancelled items should not execute, but {removed_executed} did" + ) + assert normal_executed == 4, ( + f"Expected 4 normal items to execute, got {normal_executed}" + ) From 1f5548689636385ce385c3ff27518c2d79d9ebca Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 18 Aug 2025 16:55:11 -0400 Subject: [PATCH 026/208] [api] Optimize APIFrameHelper virtual methods and mark implementations as final (#10278) --- esphome/components/api/api_frame_helper.h | 4 ++-- esphome/components/api/api_frame_helper_noise.h | 6 +----- esphome/components/api/api_frame_helper_plaintext.h | 5 +---- 3 files changed, 4 insertions(+), 11 deletions(-) diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index 76dfe1366c..43e9d95fbe 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -104,9 +104,9 @@ class APIFrameHelper { // The buffer contains all messages with appropriate padding before each virtual APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span packets) = 0; // Get the frame header padding required by this protocol - virtual uint8_t frame_header_padding() = 0; + uint8_t frame_header_padding() const { return frame_header_padding_; } // Get the frame footer size required by this protocol - virtual uint8_t frame_footer_size() = 0; + uint8_t frame_footer_size() const { return frame_footer_size_; } // Check if socket has data ready to read bool is_socket_ready() const { return socket_ != nullptr && socket_->ready(); } diff --git a/esphome/components/api/api_frame_helper_noise.h b/esphome/components/api/api_frame_helper_noise.h index e82e5daadb..49bc6f8854 100644 --- a/esphome/components/api/api_frame_helper_noise.h +++ b/esphome/components/api/api_frame_helper_noise.h @@ -7,7 +7,7 @@ namespace esphome::api { -class APINoiseFrameHelper : public APIFrameHelper { +class APINoiseFrameHelper final : public APIFrameHelper { public: APINoiseFrameHelper(std::unique_ptr socket, std::shared_ptr ctx, const ClientInfo *client_info) @@ -25,10 +25,6 @@ class APINoiseFrameHelper : public APIFrameHelper { APIError read_packet(ReadPacketBuffer *buffer) override; APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override; APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span packets) override; - // Get the frame header padding required by this protocol - uint8_t frame_header_padding() override { return frame_header_padding_; } - // Get the frame footer size required by this protocol - uint8_t frame_footer_size() override { return frame_footer_size_; } protected: APIError state_action_(); diff --git a/esphome/components/api/api_frame_helper_plaintext.h b/esphome/components/api/api_frame_helper_plaintext.h index b50902dd75..55a6d0f744 100644 --- a/esphome/components/api/api_frame_helper_plaintext.h +++ b/esphome/components/api/api_frame_helper_plaintext.h @@ -5,7 +5,7 @@ namespace esphome::api { -class APIPlaintextFrameHelper : public APIFrameHelper { +class APIPlaintextFrameHelper final : public APIFrameHelper { public: APIPlaintextFrameHelper(std::unique_ptr socket, const ClientInfo *client_info) : APIFrameHelper(std::move(socket), client_info) { @@ -22,9 +22,6 @@ class APIPlaintextFrameHelper : public APIFrameHelper { APIError read_packet(ReadPacketBuffer *buffer) override; APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override; APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span packets) override; - uint8_t frame_header_padding() override { return frame_header_padding_; } - // Get the frame footer size required by this protocol - uint8_t frame_footer_size() override { return frame_footer_size_; } protected: APIError try_read_frame_(std::vector *frame); From 761c6c6685ed0c5a53cc671bc7315c3546876925 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 18 Aug 2025 16:55:30 -0400 Subject: [PATCH 027/208] [api] Mark protobuf message classes as final to enable compiler optimizations (#10276) --- esphome/components/api/api_pb2.h | 278 ++++++++++++++-------------- script/api_protobuf/api_protobuf.py | 4 +- 2 files changed, 141 insertions(+), 141 deletions(-) diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index edf839be55..abdf0e6121 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -321,7 +321,7 @@ class CommandProtoMessage : public ProtoDecodableMessage { protected: }; -class HelloRequest : public ProtoDecodableMessage { +class HelloRequest final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 1; static constexpr uint8_t ESTIMATED_SIZE = 17; @@ -339,7 +339,7 @@ class HelloRequest : public ProtoDecodableMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class HelloResponse : public ProtoMessage { +class HelloResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 2; static constexpr uint8_t ESTIMATED_SIZE = 26; @@ -360,7 +360,7 @@ class HelloResponse : public ProtoMessage { protected: }; -class ConnectRequest : public ProtoDecodableMessage { +class ConnectRequest final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 3; static constexpr uint8_t ESTIMATED_SIZE = 9; @@ -375,7 +375,7 @@ class ConnectRequest : public ProtoDecodableMessage { protected: bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; }; -class ConnectResponse : public ProtoMessage { +class ConnectResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 4; static constexpr uint8_t ESTIMATED_SIZE = 2; @@ -391,7 +391,7 @@ class ConnectResponse : public ProtoMessage { protected: }; -class DisconnectRequest : public ProtoMessage { +class DisconnectRequest final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 5; static constexpr uint8_t ESTIMATED_SIZE = 0; @@ -404,7 +404,7 @@ class DisconnectRequest : public ProtoMessage { protected: }; -class DisconnectResponse : public ProtoMessage { +class DisconnectResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 6; static constexpr uint8_t ESTIMATED_SIZE = 0; @@ -417,7 +417,7 @@ class DisconnectResponse : public ProtoMessage { protected: }; -class PingRequest : public ProtoMessage { +class PingRequest final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 7; static constexpr uint8_t ESTIMATED_SIZE = 0; @@ -430,7 +430,7 @@ class PingRequest : public ProtoMessage { protected: }; -class PingResponse : public ProtoMessage { +class PingResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 8; static constexpr uint8_t ESTIMATED_SIZE = 0; @@ -443,7 +443,7 @@ class PingResponse : public ProtoMessage { protected: }; -class DeviceInfoRequest : public ProtoMessage { +class DeviceInfoRequest final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 9; static constexpr uint8_t ESTIMATED_SIZE = 0; @@ -457,7 +457,7 @@ class DeviceInfoRequest : public ProtoMessage { protected: }; #ifdef USE_AREAS -class AreaInfo : public ProtoMessage { +class AreaInfo final : public ProtoMessage { public: uint32_t area_id{0}; StringRef name_ref_{}; @@ -472,7 +472,7 @@ class AreaInfo : public ProtoMessage { }; #endif #ifdef USE_DEVICES -class DeviceInfo : public ProtoMessage { +class DeviceInfo final : public ProtoMessage { public: uint32_t device_id{0}; StringRef name_ref_{}; @@ -487,7 +487,7 @@ class DeviceInfo : public ProtoMessage { protected: }; #endif -class DeviceInfoResponse : public ProtoMessage { +class DeviceInfoResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 10; static constexpr uint8_t ESTIMATED_SIZE = 247; @@ -559,7 +559,7 @@ class DeviceInfoResponse : public ProtoMessage { protected: }; -class ListEntitiesRequest : public ProtoMessage { +class ListEntitiesRequest final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 11; static constexpr uint8_t ESTIMATED_SIZE = 0; @@ -572,7 +572,7 @@ class ListEntitiesRequest : public ProtoMessage { protected: }; -class ListEntitiesDoneResponse : public ProtoMessage { +class ListEntitiesDoneResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 19; static constexpr uint8_t ESTIMATED_SIZE = 0; @@ -585,7 +585,7 @@ class ListEntitiesDoneResponse : public ProtoMessage { protected: }; -class SubscribeStatesRequest : public ProtoMessage { +class SubscribeStatesRequest final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 20; static constexpr uint8_t ESTIMATED_SIZE = 0; @@ -599,7 +599,7 @@ class SubscribeStatesRequest : public ProtoMessage { protected: }; #ifdef USE_BINARY_SENSOR -class ListEntitiesBinarySensorResponse : public InfoResponseProtoMessage { +class ListEntitiesBinarySensorResponse final : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 12; static constexpr uint8_t ESTIMATED_SIZE = 51; @@ -617,7 +617,7 @@ class ListEntitiesBinarySensorResponse : public InfoResponseProtoMessage { protected: }; -class BinarySensorStateResponse : public StateResponseProtoMessage { +class BinarySensorStateResponse final : public StateResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 21; static constexpr uint8_t ESTIMATED_SIZE = 13; @@ -636,7 +636,7 @@ class BinarySensorStateResponse : public StateResponseProtoMessage { }; #endif #ifdef USE_COVER -class ListEntitiesCoverResponse : public InfoResponseProtoMessage { +class ListEntitiesCoverResponse final : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 13; static constexpr uint8_t ESTIMATED_SIZE = 57; @@ -657,7 +657,7 @@ class ListEntitiesCoverResponse : public InfoResponseProtoMessage { protected: }; -class CoverStateResponse : public StateResponseProtoMessage { +class CoverStateResponse final : public StateResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 22; static constexpr uint8_t ESTIMATED_SIZE = 21; @@ -675,7 +675,7 @@ class CoverStateResponse : public StateResponseProtoMessage { protected: }; -class CoverCommandRequest : public CommandProtoMessage { +class CoverCommandRequest final : public CommandProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 30; static constexpr uint8_t ESTIMATED_SIZE = 25; @@ -697,7 +697,7 @@ class CoverCommandRequest : public CommandProtoMessage { }; #endif #ifdef USE_FAN -class ListEntitiesFanResponse : public InfoResponseProtoMessage { +class ListEntitiesFanResponse final : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 14; static constexpr uint8_t ESTIMATED_SIZE = 68; @@ -717,7 +717,7 @@ class ListEntitiesFanResponse : public InfoResponseProtoMessage { protected: }; -class FanStateResponse : public StateResponseProtoMessage { +class FanStateResponse final : public StateResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 23; static constexpr uint8_t ESTIMATED_SIZE = 28; @@ -738,7 +738,7 @@ class FanStateResponse : public StateResponseProtoMessage { protected: }; -class FanCommandRequest : public CommandProtoMessage { +class FanCommandRequest final : public CommandProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 31; static constexpr uint8_t ESTIMATED_SIZE = 38; @@ -766,7 +766,7 @@ class FanCommandRequest : public CommandProtoMessage { }; #endif #ifdef USE_LIGHT -class ListEntitiesLightResponse : public InfoResponseProtoMessage { +class ListEntitiesLightResponse final : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 15; static constexpr uint8_t ESTIMATED_SIZE = 73; @@ -785,7 +785,7 @@ class ListEntitiesLightResponse : public InfoResponseProtoMessage { protected: }; -class LightStateResponse : public StateResponseProtoMessage { +class LightStateResponse final : public StateResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 24; static constexpr uint8_t ESTIMATED_SIZE = 67; @@ -813,7 +813,7 @@ class LightStateResponse : public StateResponseProtoMessage { protected: }; -class LightCommandRequest : public CommandProtoMessage { +class LightCommandRequest final : public CommandProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 32; static constexpr uint8_t ESTIMATED_SIZE = 112; @@ -857,7 +857,7 @@ class LightCommandRequest : public CommandProtoMessage { }; #endif #ifdef USE_SENSOR -class ListEntitiesSensorResponse : public InfoResponseProtoMessage { +class ListEntitiesSensorResponse final : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 16; static constexpr uint8_t ESTIMATED_SIZE = 66; @@ -879,7 +879,7 @@ class ListEntitiesSensorResponse : public InfoResponseProtoMessage { protected: }; -class SensorStateResponse : public StateResponseProtoMessage { +class SensorStateResponse final : public StateResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 25; static constexpr uint8_t ESTIMATED_SIZE = 16; @@ -898,7 +898,7 @@ class SensorStateResponse : public StateResponseProtoMessage { }; #endif #ifdef USE_SWITCH -class ListEntitiesSwitchResponse : public InfoResponseProtoMessage { +class ListEntitiesSwitchResponse final : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 17; static constexpr uint8_t ESTIMATED_SIZE = 51; @@ -916,7 +916,7 @@ class ListEntitiesSwitchResponse : public InfoResponseProtoMessage { protected: }; -class SwitchStateResponse : public StateResponseProtoMessage { +class SwitchStateResponse final : public StateResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 26; static constexpr uint8_t ESTIMATED_SIZE = 11; @@ -932,7 +932,7 @@ class SwitchStateResponse : public StateResponseProtoMessage { protected: }; -class SwitchCommandRequest : public CommandProtoMessage { +class SwitchCommandRequest final : public CommandProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 33; static constexpr uint8_t ESTIMATED_SIZE = 11; @@ -950,7 +950,7 @@ class SwitchCommandRequest : public CommandProtoMessage { }; #endif #ifdef USE_TEXT_SENSOR -class ListEntitiesTextSensorResponse : public InfoResponseProtoMessage { +class ListEntitiesTextSensorResponse final : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 18; static constexpr uint8_t ESTIMATED_SIZE = 49; @@ -967,7 +967,7 @@ class ListEntitiesTextSensorResponse : public InfoResponseProtoMessage { protected: }; -class TextSensorStateResponse : public StateResponseProtoMessage { +class TextSensorStateResponse final : public StateResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 27; static constexpr uint8_t ESTIMATED_SIZE = 20; @@ -986,7 +986,7 @@ class TextSensorStateResponse : public StateResponseProtoMessage { protected: }; #endif -class SubscribeLogsRequest : public ProtoDecodableMessage { +class SubscribeLogsRequest final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 28; static constexpr uint8_t ESTIMATED_SIZE = 4; @@ -1002,7 +1002,7 @@ class SubscribeLogsRequest : public ProtoDecodableMessage { protected: bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class SubscribeLogsResponse : public ProtoMessage { +class SubscribeLogsResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 29; static constexpr uint8_t ESTIMATED_SIZE = 11; @@ -1025,7 +1025,7 @@ class SubscribeLogsResponse : public ProtoMessage { protected: }; #ifdef USE_API_NOISE -class NoiseEncryptionSetKeyRequest : public ProtoDecodableMessage { +class NoiseEncryptionSetKeyRequest final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 124; static constexpr uint8_t ESTIMATED_SIZE = 9; @@ -1040,7 +1040,7 @@ class NoiseEncryptionSetKeyRequest : public ProtoDecodableMessage { protected: bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; }; -class NoiseEncryptionSetKeyResponse : public ProtoMessage { +class NoiseEncryptionSetKeyResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 125; static constexpr uint8_t ESTIMATED_SIZE = 2; @@ -1058,7 +1058,7 @@ class NoiseEncryptionSetKeyResponse : public ProtoMessage { }; #endif #ifdef USE_API_HOMEASSISTANT_SERVICES -class SubscribeHomeassistantServicesRequest : public ProtoMessage { +class SubscribeHomeassistantServicesRequest final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 34; static constexpr uint8_t ESTIMATED_SIZE = 0; @@ -1071,7 +1071,7 @@ class SubscribeHomeassistantServicesRequest : public ProtoMessage { protected: }; -class HomeassistantServiceMap : public ProtoMessage { +class HomeassistantServiceMap final : public ProtoMessage { public: StringRef key_ref_{}; void set_key(const StringRef &ref) { this->key_ref_ = ref; } @@ -1084,7 +1084,7 @@ class HomeassistantServiceMap : public ProtoMessage { protected: }; -class HomeassistantServiceResponse : public ProtoMessage { +class HomeassistantServiceResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 35; static constexpr uint8_t ESTIMATED_SIZE = 113; @@ -1107,7 +1107,7 @@ class HomeassistantServiceResponse : public ProtoMessage { }; #endif #ifdef USE_API_HOMEASSISTANT_STATES -class SubscribeHomeAssistantStatesRequest : public ProtoMessage { +class SubscribeHomeAssistantStatesRequest final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 38; static constexpr uint8_t ESTIMATED_SIZE = 0; @@ -1120,7 +1120,7 @@ class SubscribeHomeAssistantStatesRequest : public ProtoMessage { protected: }; -class SubscribeHomeAssistantStateResponse : public ProtoMessage { +class SubscribeHomeAssistantStateResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 39; static constexpr uint8_t ESTIMATED_SIZE = 20; @@ -1140,7 +1140,7 @@ class SubscribeHomeAssistantStateResponse : public ProtoMessage { protected: }; -class HomeAssistantStateResponse : public ProtoDecodableMessage { +class HomeAssistantStateResponse final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 40; static constexpr uint8_t ESTIMATED_SIZE = 27; @@ -1158,7 +1158,7 @@ class HomeAssistantStateResponse : public ProtoDecodableMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; }; #endif -class GetTimeRequest : public ProtoMessage { +class GetTimeRequest final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 36; static constexpr uint8_t ESTIMATED_SIZE = 0; @@ -1171,7 +1171,7 @@ class GetTimeRequest : public ProtoMessage { protected: }; -class GetTimeResponse : public ProtoDecodableMessage { +class GetTimeResponse final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 37; static constexpr uint8_t ESTIMATED_SIZE = 5; @@ -1189,7 +1189,7 @@ class GetTimeResponse : public ProtoDecodableMessage { bool decode_32bit(uint32_t field_id, Proto32Bit value) override; }; #ifdef USE_API_SERVICES -class ListEntitiesServicesArgument : public ProtoMessage { +class ListEntitiesServicesArgument final : public ProtoMessage { public: StringRef name_ref_{}; void set_name(const StringRef &ref) { this->name_ref_ = ref; } @@ -1202,7 +1202,7 @@ class ListEntitiesServicesArgument : public ProtoMessage { protected: }; -class ListEntitiesServicesResponse : public ProtoMessage { +class ListEntitiesServicesResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 41; static constexpr uint8_t ESTIMATED_SIZE = 48; @@ -1221,7 +1221,7 @@ class ListEntitiesServicesResponse : public ProtoMessage { protected: }; -class ExecuteServiceArgument : public ProtoDecodableMessage { +class ExecuteServiceArgument final : public ProtoDecodableMessage { public: bool bool_{false}; int32_t legacy_int{0}; @@ -1241,7 +1241,7 @@ class ExecuteServiceArgument : public ProtoDecodableMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class ExecuteServiceRequest : public ProtoDecodableMessage { +class ExecuteServiceRequest final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 42; static constexpr uint8_t ESTIMATED_SIZE = 39; @@ -1260,7 +1260,7 @@ class ExecuteServiceRequest : public ProtoDecodableMessage { }; #endif #ifdef USE_CAMERA -class ListEntitiesCameraResponse : public InfoResponseProtoMessage { +class ListEntitiesCameraResponse final : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 43; static constexpr uint8_t ESTIMATED_SIZE = 40; @@ -1275,7 +1275,7 @@ class ListEntitiesCameraResponse : public InfoResponseProtoMessage { protected: }; -class CameraImageResponse : public StateResponseProtoMessage { +class CameraImageResponse final : public StateResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 44; static constexpr uint8_t ESTIMATED_SIZE = 20; @@ -1297,7 +1297,7 @@ class CameraImageResponse : public StateResponseProtoMessage { protected: }; -class CameraImageRequest : public ProtoDecodableMessage { +class CameraImageRequest final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 45; static constexpr uint8_t ESTIMATED_SIZE = 4; @@ -1315,7 +1315,7 @@ class CameraImageRequest : public ProtoDecodableMessage { }; #endif #ifdef USE_CLIMATE -class ListEntitiesClimateResponse : public InfoResponseProtoMessage { +class ListEntitiesClimateResponse final : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 46; static constexpr uint8_t ESTIMATED_SIZE = 145; @@ -1347,7 +1347,7 @@ class ListEntitiesClimateResponse : public InfoResponseProtoMessage { protected: }; -class ClimateStateResponse : public StateResponseProtoMessage { +class ClimateStateResponse final : public StateResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 47; static constexpr uint8_t ESTIMATED_SIZE = 68; @@ -1377,7 +1377,7 @@ class ClimateStateResponse : public StateResponseProtoMessage { protected: }; -class ClimateCommandRequest : public CommandProtoMessage { +class ClimateCommandRequest final : public CommandProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 48; static constexpr uint8_t ESTIMATED_SIZE = 84; @@ -1415,7 +1415,7 @@ class ClimateCommandRequest : public CommandProtoMessage { }; #endif #ifdef USE_NUMBER -class ListEntitiesNumberResponse : public InfoResponseProtoMessage { +class ListEntitiesNumberResponse final : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 49; static constexpr uint8_t ESTIMATED_SIZE = 75; @@ -1438,7 +1438,7 @@ class ListEntitiesNumberResponse : public InfoResponseProtoMessage { protected: }; -class NumberStateResponse : public StateResponseProtoMessage { +class NumberStateResponse final : public StateResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 50; static constexpr uint8_t ESTIMATED_SIZE = 16; @@ -1455,7 +1455,7 @@ class NumberStateResponse : public StateResponseProtoMessage { protected: }; -class NumberCommandRequest : public CommandProtoMessage { +class NumberCommandRequest final : public CommandProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 51; static constexpr uint8_t ESTIMATED_SIZE = 14; @@ -1473,7 +1473,7 @@ class NumberCommandRequest : public CommandProtoMessage { }; #endif #ifdef USE_SELECT -class ListEntitiesSelectResponse : public InfoResponseProtoMessage { +class ListEntitiesSelectResponse final : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 52; static constexpr uint8_t ESTIMATED_SIZE = 58; @@ -1489,7 +1489,7 @@ class ListEntitiesSelectResponse : public InfoResponseProtoMessage { protected: }; -class SelectStateResponse : public StateResponseProtoMessage { +class SelectStateResponse final : public StateResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 53; static constexpr uint8_t ESTIMATED_SIZE = 20; @@ -1507,7 +1507,7 @@ class SelectStateResponse : public StateResponseProtoMessage { protected: }; -class SelectCommandRequest : public CommandProtoMessage { +class SelectCommandRequest final : public CommandProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 54; static constexpr uint8_t ESTIMATED_SIZE = 18; @@ -1526,7 +1526,7 @@ class SelectCommandRequest : public CommandProtoMessage { }; #endif #ifdef USE_SIREN -class ListEntitiesSirenResponse : public InfoResponseProtoMessage { +class ListEntitiesSirenResponse final : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 55; static constexpr uint8_t ESTIMATED_SIZE = 62; @@ -1544,7 +1544,7 @@ class ListEntitiesSirenResponse : public InfoResponseProtoMessage { protected: }; -class SirenStateResponse : public StateResponseProtoMessage { +class SirenStateResponse final : public StateResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 56; static constexpr uint8_t ESTIMATED_SIZE = 11; @@ -1560,7 +1560,7 @@ class SirenStateResponse : public StateResponseProtoMessage { protected: }; -class SirenCommandRequest : public CommandProtoMessage { +class SirenCommandRequest final : public CommandProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 57; static constexpr uint8_t ESTIMATED_SIZE = 37; @@ -1586,7 +1586,7 @@ class SirenCommandRequest : public CommandProtoMessage { }; #endif #ifdef USE_LOCK -class ListEntitiesLockResponse : public InfoResponseProtoMessage { +class ListEntitiesLockResponse final : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 58; static constexpr uint8_t ESTIMATED_SIZE = 55; @@ -1606,7 +1606,7 @@ class ListEntitiesLockResponse : public InfoResponseProtoMessage { protected: }; -class LockStateResponse : public StateResponseProtoMessage { +class LockStateResponse final : public StateResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 59; static constexpr uint8_t ESTIMATED_SIZE = 11; @@ -1622,7 +1622,7 @@ class LockStateResponse : public StateResponseProtoMessage { protected: }; -class LockCommandRequest : public CommandProtoMessage { +class LockCommandRequest final : public CommandProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 60; static constexpr uint8_t ESTIMATED_SIZE = 22; @@ -1643,7 +1643,7 @@ class LockCommandRequest : public CommandProtoMessage { }; #endif #ifdef USE_BUTTON -class ListEntitiesButtonResponse : public InfoResponseProtoMessage { +class ListEntitiesButtonResponse final : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 61; static constexpr uint8_t ESTIMATED_SIZE = 49; @@ -1660,7 +1660,7 @@ class ListEntitiesButtonResponse : public InfoResponseProtoMessage { protected: }; -class ButtonCommandRequest : public CommandProtoMessage { +class ButtonCommandRequest final : public CommandProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 62; static constexpr uint8_t ESTIMATED_SIZE = 9; @@ -1677,7 +1677,7 @@ class ButtonCommandRequest : public CommandProtoMessage { }; #endif #ifdef USE_MEDIA_PLAYER -class MediaPlayerSupportedFormat : public ProtoMessage { +class MediaPlayerSupportedFormat final : public ProtoMessage { public: StringRef format_ref_{}; void set_format(const StringRef &ref) { this->format_ref_ = ref; } @@ -1693,7 +1693,7 @@ class MediaPlayerSupportedFormat : public ProtoMessage { protected: }; -class ListEntitiesMediaPlayerResponse : public InfoResponseProtoMessage { +class ListEntitiesMediaPlayerResponse final : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 63; static constexpr uint8_t ESTIMATED_SIZE = 80; @@ -1711,7 +1711,7 @@ class ListEntitiesMediaPlayerResponse : public InfoResponseProtoMessage { protected: }; -class MediaPlayerStateResponse : public StateResponseProtoMessage { +class MediaPlayerStateResponse final : public StateResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 64; static constexpr uint8_t ESTIMATED_SIZE = 18; @@ -1729,7 +1729,7 @@ class MediaPlayerStateResponse : public StateResponseProtoMessage { protected: }; -class MediaPlayerCommandRequest : public CommandProtoMessage { +class MediaPlayerCommandRequest final : public CommandProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 65; static constexpr uint8_t ESTIMATED_SIZE = 35; @@ -1755,7 +1755,7 @@ class MediaPlayerCommandRequest : public CommandProtoMessage { }; #endif #ifdef USE_BLUETOOTH_PROXY -class SubscribeBluetoothLEAdvertisementsRequest : public ProtoDecodableMessage { +class SubscribeBluetoothLEAdvertisementsRequest final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 66; static constexpr uint8_t ESTIMATED_SIZE = 4; @@ -1770,7 +1770,7 @@ class SubscribeBluetoothLEAdvertisementsRequest : public ProtoDecodableMessage { protected: bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class BluetoothLERawAdvertisement : public ProtoMessage { +class BluetoothLERawAdvertisement final : public ProtoMessage { public: uint64_t address{0}; int32_t rssi{0}; @@ -1785,7 +1785,7 @@ class BluetoothLERawAdvertisement : public ProtoMessage { protected: }; -class BluetoothLERawAdvertisementsResponse : public ProtoMessage { +class BluetoothLERawAdvertisementsResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 93; static constexpr uint8_t ESTIMATED_SIZE = 136; @@ -1802,7 +1802,7 @@ class BluetoothLERawAdvertisementsResponse : public ProtoMessage { protected: }; -class BluetoothDeviceRequest : public ProtoDecodableMessage { +class BluetoothDeviceRequest final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 68; static constexpr uint8_t ESTIMATED_SIZE = 12; @@ -1820,7 +1820,7 @@ class BluetoothDeviceRequest : public ProtoDecodableMessage { protected: bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class BluetoothDeviceConnectionResponse : public ProtoMessage { +class BluetoothDeviceConnectionResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 69; static constexpr uint8_t ESTIMATED_SIZE = 14; @@ -1839,7 +1839,7 @@ class BluetoothDeviceConnectionResponse : public ProtoMessage { protected: }; -class BluetoothGATTGetServicesRequest : public ProtoDecodableMessage { +class BluetoothGATTGetServicesRequest final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 70; static constexpr uint8_t ESTIMATED_SIZE = 4; @@ -1854,7 +1854,7 @@ class BluetoothGATTGetServicesRequest : public ProtoDecodableMessage { protected: bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class BluetoothGATTDescriptor : public ProtoMessage { +class BluetoothGATTDescriptor final : public ProtoMessage { public: std::array uuid{}; uint32_t handle{0}; @@ -1867,7 +1867,7 @@ class BluetoothGATTDescriptor : public ProtoMessage { protected: }; -class BluetoothGATTCharacteristic : public ProtoMessage { +class BluetoothGATTCharacteristic final : public ProtoMessage { public: std::array uuid{}; uint32_t handle{0}; @@ -1882,7 +1882,7 @@ class BluetoothGATTCharacteristic : public ProtoMessage { protected: }; -class BluetoothGATTService : public ProtoMessage { +class BluetoothGATTService final : public ProtoMessage { public: std::array uuid{}; uint32_t handle{0}; @@ -1896,7 +1896,7 @@ class BluetoothGATTService : public ProtoMessage { protected: }; -class BluetoothGATTGetServicesResponse : public ProtoMessage { +class BluetoothGATTGetServicesResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 71; static constexpr uint8_t ESTIMATED_SIZE = 38; @@ -1913,7 +1913,7 @@ class BluetoothGATTGetServicesResponse : public ProtoMessage { protected: }; -class BluetoothGATTGetServicesDoneResponse : public ProtoMessage { +class BluetoothGATTGetServicesDoneResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 72; static constexpr uint8_t ESTIMATED_SIZE = 4; @@ -1929,7 +1929,7 @@ class BluetoothGATTGetServicesDoneResponse : public ProtoMessage { protected: }; -class BluetoothGATTReadRequest : public ProtoDecodableMessage { +class BluetoothGATTReadRequest final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 73; static constexpr uint8_t ESTIMATED_SIZE = 8; @@ -1945,7 +1945,7 @@ class BluetoothGATTReadRequest : public ProtoDecodableMessage { protected: bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class BluetoothGATTReadResponse : public ProtoMessage { +class BluetoothGATTReadResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 74; static constexpr uint8_t ESTIMATED_SIZE = 17; @@ -1968,7 +1968,7 @@ class BluetoothGATTReadResponse : public ProtoMessage { protected: }; -class BluetoothGATTWriteRequest : public ProtoDecodableMessage { +class BluetoothGATTWriteRequest final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 75; static constexpr uint8_t ESTIMATED_SIZE = 19; @@ -1987,7 +1987,7 @@ class BluetoothGATTWriteRequest : public ProtoDecodableMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class BluetoothGATTReadDescriptorRequest : public ProtoDecodableMessage { +class BluetoothGATTReadDescriptorRequest final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 76; static constexpr uint8_t ESTIMATED_SIZE = 8; @@ -2003,7 +2003,7 @@ class BluetoothGATTReadDescriptorRequest : public ProtoDecodableMessage { protected: bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class BluetoothGATTWriteDescriptorRequest : public ProtoDecodableMessage { +class BluetoothGATTWriteDescriptorRequest final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 77; static constexpr uint8_t ESTIMATED_SIZE = 17; @@ -2021,7 +2021,7 @@ class BluetoothGATTWriteDescriptorRequest : public ProtoDecodableMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class BluetoothGATTNotifyRequest : public ProtoDecodableMessage { +class BluetoothGATTNotifyRequest final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 78; static constexpr uint8_t ESTIMATED_SIZE = 10; @@ -2038,7 +2038,7 @@ class BluetoothGATTNotifyRequest : public ProtoDecodableMessage { protected: bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class BluetoothGATTNotifyDataResponse : public ProtoMessage { +class BluetoothGATTNotifyDataResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 79; static constexpr uint8_t ESTIMATED_SIZE = 17; @@ -2061,7 +2061,7 @@ class BluetoothGATTNotifyDataResponse : public ProtoMessage { protected: }; -class SubscribeBluetoothConnectionsFreeRequest : public ProtoMessage { +class SubscribeBluetoothConnectionsFreeRequest final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 80; static constexpr uint8_t ESTIMATED_SIZE = 0; @@ -2074,7 +2074,7 @@ class SubscribeBluetoothConnectionsFreeRequest : public ProtoMessage { protected: }; -class BluetoothConnectionsFreeResponse : public ProtoMessage { +class BluetoothConnectionsFreeResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 81; static constexpr uint8_t ESTIMATED_SIZE = 20; @@ -2092,7 +2092,7 @@ class BluetoothConnectionsFreeResponse : public ProtoMessage { protected: }; -class BluetoothGATTErrorResponse : public ProtoMessage { +class BluetoothGATTErrorResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 82; static constexpr uint8_t ESTIMATED_SIZE = 12; @@ -2110,7 +2110,7 @@ class BluetoothGATTErrorResponse : public ProtoMessage { protected: }; -class BluetoothGATTWriteResponse : public ProtoMessage { +class BluetoothGATTWriteResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 83; static constexpr uint8_t ESTIMATED_SIZE = 8; @@ -2127,7 +2127,7 @@ class BluetoothGATTWriteResponse : public ProtoMessage { protected: }; -class BluetoothGATTNotifyResponse : public ProtoMessage { +class BluetoothGATTNotifyResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 84; static constexpr uint8_t ESTIMATED_SIZE = 8; @@ -2144,7 +2144,7 @@ class BluetoothGATTNotifyResponse : public ProtoMessage { protected: }; -class BluetoothDevicePairingResponse : public ProtoMessage { +class BluetoothDevicePairingResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 85; static constexpr uint8_t ESTIMATED_SIZE = 10; @@ -2162,7 +2162,7 @@ class BluetoothDevicePairingResponse : public ProtoMessage { protected: }; -class BluetoothDeviceUnpairingResponse : public ProtoMessage { +class BluetoothDeviceUnpairingResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 86; static constexpr uint8_t ESTIMATED_SIZE = 10; @@ -2180,7 +2180,7 @@ class BluetoothDeviceUnpairingResponse : public ProtoMessage { protected: }; -class UnsubscribeBluetoothLEAdvertisementsRequest : public ProtoMessage { +class UnsubscribeBluetoothLEAdvertisementsRequest final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 87; static constexpr uint8_t ESTIMATED_SIZE = 0; @@ -2193,7 +2193,7 @@ class UnsubscribeBluetoothLEAdvertisementsRequest : public ProtoMessage { protected: }; -class BluetoothDeviceClearCacheResponse : public ProtoMessage { +class BluetoothDeviceClearCacheResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 88; static constexpr uint8_t ESTIMATED_SIZE = 10; @@ -2211,7 +2211,7 @@ class BluetoothDeviceClearCacheResponse : public ProtoMessage { protected: }; -class BluetoothScannerStateResponse : public ProtoMessage { +class BluetoothScannerStateResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 126; static constexpr uint8_t ESTIMATED_SIZE = 4; @@ -2228,7 +2228,7 @@ class BluetoothScannerStateResponse : public ProtoMessage { protected: }; -class BluetoothScannerSetModeRequest : public ProtoDecodableMessage { +class BluetoothScannerSetModeRequest final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 127; static constexpr uint8_t ESTIMATED_SIZE = 2; @@ -2245,7 +2245,7 @@ class BluetoothScannerSetModeRequest : public ProtoDecodableMessage { }; #endif #ifdef USE_VOICE_ASSISTANT -class SubscribeVoiceAssistantRequest : public ProtoDecodableMessage { +class SubscribeVoiceAssistantRequest final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 89; static constexpr uint8_t ESTIMATED_SIZE = 6; @@ -2261,7 +2261,7 @@ class SubscribeVoiceAssistantRequest : public ProtoDecodableMessage { protected: bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class VoiceAssistantAudioSettings : public ProtoMessage { +class VoiceAssistantAudioSettings final : public ProtoMessage { public: uint32_t noise_suppression_level{0}; uint32_t auto_gain{0}; @@ -2274,7 +2274,7 @@ class VoiceAssistantAudioSettings : public ProtoMessage { protected: }; -class VoiceAssistantRequest : public ProtoMessage { +class VoiceAssistantRequest final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 90; static constexpr uint8_t ESTIMATED_SIZE = 41; @@ -2296,7 +2296,7 @@ class VoiceAssistantRequest : public ProtoMessage { protected: }; -class VoiceAssistantResponse : public ProtoDecodableMessage { +class VoiceAssistantResponse final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 91; static constexpr uint8_t ESTIMATED_SIZE = 6; @@ -2312,7 +2312,7 @@ class VoiceAssistantResponse : public ProtoDecodableMessage { protected: bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class VoiceAssistantEventData : public ProtoDecodableMessage { +class VoiceAssistantEventData final : public ProtoDecodableMessage { public: std::string name{}; std::string value{}; @@ -2323,7 +2323,7 @@ class VoiceAssistantEventData : public ProtoDecodableMessage { protected: bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; }; -class VoiceAssistantEventResponse : public ProtoDecodableMessage { +class VoiceAssistantEventResponse final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 92; static constexpr uint8_t ESTIMATED_SIZE = 36; @@ -2340,7 +2340,7 @@ class VoiceAssistantEventResponse : public ProtoDecodableMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class VoiceAssistantAudio : public ProtoDecodableMessage { +class VoiceAssistantAudio final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 106; static constexpr uint8_t ESTIMATED_SIZE = 11; @@ -2365,7 +2365,7 @@ class VoiceAssistantAudio : public ProtoDecodableMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class VoiceAssistantTimerEventResponse : public ProtoDecodableMessage { +class VoiceAssistantTimerEventResponse final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 115; static constexpr uint8_t ESTIMATED_SIZE = 30; @@ -2386,7 +2386,7 @@ class VoiceAssistantTimerEventResponse : public ProtoDecodableMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class VoiceAssistantAnnounceRequest : public ProtoDecodableMessage { +class VoiceAssistantAnnounceRequest final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 119; static constexpr uint8_t ESTIMATED_SIZE = 29; @@ -2405,7 +2405,7 @@ class VoiceAssistantAnnounceRequest : public ProtoDecodableMessage { bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; -class VoiceAssistantAnnounceFinished : public ProtoMessage { +class VoiceAssistantAnnounceFinished final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 120; static constexpr uint8_t ESTIMATED_SIZE = 2; @@ -2421,7 +2421,7 @@ class VoiceAssistantAnnounceFinished : public ProtoMessage { protected: }; -class VoiceAssistantWakeWord : public ProtoMessage { +class VoiceAssistantWakeWord final : public ProtoMessage { public: StringRef id_ref_{}; void set_id(const StringRef &ref) { this->id_ref_ = ref; } @@ -2436,7 +2436,7 @@ class VoiceAssistantWakeWord : public ProtoMessage { protected: }; -class VoiceAssistantConfigurationRequest : public ProtoMessage { +class VoiceAssistantConfigurationRequest final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 121; static constexpr uint8_t ESTIMATED_SIZE = 0; @@ -2449,7 +2449,7 @@ class VoiceAssistantConfigurationRequest : public ProtoMessage { protected: }; -class VoiceAssistantConfigurationResponse : public ProtoMessage { +class VoiceAssistantConfigurationResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 122; static constexpr uint8_t ESTIMATED_SIZE = 56; @@ -2467,7 +2467,7 @@ class VoiceAssistantConfigurationResponse : public ProtoMessage { protected: }; -class VoiceAssistantSetConfiguration : public ProtoDecodableMessage { +class VoiceAssistantSetConfiguration final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 123; static constexpr uint8_t ESTIMATED_SIZE = 18; @@ -2484,7 +2484,7 @@ class VoiceAssistantSetConfiguration : public ProtoDecodableMessage { }; #endif #ifdef USE_ALARM_CONTROL_PANEL -class ListEntitiesAlarmControlPanelResponse : public InfoResponseProtoMessage { +class ListEntitiesAlarmControlPanelResponse final : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 94; static constexpr uint8_t ESTIMATED_SIZE = 48; @@ -2502,7 +2502,7 @@ class ListEntitiesAlarmControlPanelResponse : public InfoResponseProtoMessage { protected: }; -class AlarmControlPanelStateResponse : public StateResponseProtoMessage { +class AlarmControlPanelStateResponse final : public StateResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 95; static constexpr uint8_t ESTIMATED_SIZE = 11; @@ -2518,7 +2518,7 @@ class AlarmControlPanelStateResponse : public StateResponseProtoMessage { protected: }; -class AlarmControlPanelCommandRequest : public CommandProtoMessage { +class AlarmControlPanelCommandRequest final : public CommandProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 96; static constexpr uint8_t ESTIMATED_SIZE = 20; @@ -2538,7 +2538,7 @@ class AlarmControlPanelCommandRequest : public CommandProtoMessage { }; #endif #ifdef USE_TEXT -class ListEntitiesTextResponse : public InfoResponseProtoMessage { +class ListEntitiesTextResponse final : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 97; static constexpr uint8_t ESTIMATED_SIZE = 59; @@ -2558,7 +2558,7 @@ class ListEntitiesTextResponse : public InfoResponseProtoMessage { protected: }; -class TextStateResponse : public StateResponseProtoMessage { +class TextStateResponse final : public StateResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 98; static constexpr uint8_t ESTIMATED_SIZE = 20; @@ -2576,7 +2576,7 @@ class TextStateResponse : public StateResponseProtoMessage { protected: }; -class TextCommandRequest : public CommandProtoMessage { +class TextCommandRequest final : public CommandProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 99; static constexpr uint8_t ESTIMATED_SIZE = 18; @@ -2595,7 +2595,7 @@ class TextCommandRequest : public CommandProtoMessage { }; #endif #ifdef USE_DATETIME_DATE -class ListEntitiesDateResponse : public InfoResponseProtoMessage { +class ListEntitiesDateResponse final : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 100; static constexpr uint8_t ESTIMATED_SIZE = 40; @@ -2610,7 +2610,7 @@ class ListEntitiesDateResponse : public InfoResponseProtoMessage { protected: }; -class DateStateResponse : public StateResponseProtoMessage { +class DateStateResponse final : public StateResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 101; static constexpr uint8_t ESTIMATED_SIZE = 23; @@ -2629,7 +2629,7 @@ class DateStateResponse : public StateResponseProtoMessage { protected: }; -class DateCommandRequest : public CommandProtoMessage { +class DateCommandRequest final : public CommandProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 102; static constexpr uint8_t ESTIMATED_SIZE = 21; @@ -2649,7 +2649,7 @@ class DateCommandRequest : public CommandProtoMessage { }; #endif #ifdef USE_DATETIME_TIME -class ListEntitiesTimeResponse : public InfoResponseProtoMessage { +class ListEntitiesTimeResponse final : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 103; static constexpr uint8_t ESTIMATED_SIZE = 40; @@ -2664,7 +2664,7 @@ class ListEntitiesTimeResponse : public InfoResponseProtoMessage { protected: }; -class TimeStateResponse : public StateResponseProtoMessage { +class TimeStateResponse final : public StateResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 104; static constexpr uint8_t ESTIMATED_SIZE = 23; @@ -2683,7 +2683,7 @@ class TimeStateResponse : public StateResponseProtoMessage { protected: }; -class TimeCommandRequest : public CommandProtoMessage { +class TimeCommandRequest final : public CommandProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 105; static constexpr uint8_t ESTIMATED_SIZE = 21; @@ -2703,7 +2703,7 @@ class TimeCommandRequest : public CommandProtoMessage { }; #endif #ifdef USE_EVENT -class ListEntitiesEventResponse : public InfoResponseProtoMessage { +class ListEntitiesEventResponse final : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 107; static constexpr uint8_t ESTIMATED_SIZE = 67; @@ -2721,7 +2721,7 @@ class ListEntitiesEventResponse : public InfoResponseProtoMessage { protected: }; -class EventResponse : public StateResponseProtoMessage { +class EventResponse final : public StateResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 108; static constexpr uint8_t ESTIMATED_SIZE = 18; @@ -2740,7 +2740,7 @@ class EventResponse : public StateResponseProtoMessage { }; #endif #ifdef USE_VALVE -class ListEntitiesValveResponse : public InfoResponseProtoMessage { +class ListEntitiesValveResponse final : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 109; static constexpr uint8_t ESTIMATED_SIZE = 55; @@ -2760,7 +2760,7 @@ class ListEntitiesValveResponse : public InfoResponseProtoMessage { protected: }; -class ValveStateResponse : public StateResponseProtoMessage { +class ValveStateResponse final : public StateResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 110; static constexpr uint8_t ESTIMATED_SIZE = 16; @@ -2777,7 +2777,7 @@ class ValveStateResponse : public StateResponseProtoMessage { protected: }; -class ValveCommandRequest : public CommandProtoMessage { +class ValveCommandRequest final : public CommandProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 111; static constexpr uint8_t ESTIMATED_SIZE = 18; @@ -2797,7 +2797,7 @@ class ValveCommandRequest : public CommandProtoMessage { }; #endif #ifdef USE_DATETIME_DATETIME -class ListEntitiesDateTimeResponse : public InfoResponseProtoMessage { +class ListEntitiesDateTimeResponse final : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 112; static constexpr uint8_t ESTIMATED_SIZE = 40; @@ -2812,7 +2812,7 @@ class ListEntitiesDateTimeResponse : public InfoResponseProtoMessage { protected: }; -class DateTimeStateResponse : public StateResponseProtoMessage { +class DateTimeStateResponse final : public StateResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 113; static constexpr uint8_t ESTIMATED_SIZE = 16; @@ -2829,7 +2829,7 @@ class DateTimeStateResponse : public StateResponseProtoMessage { protected: }; -class DateTimeCommandRequest : public CommandProtoMessage { +class DateTimeCommandRequest final : public CommandProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 114; static constexpr uint8_t ESTIMATED_SIZE = 14; @@ -2847,7 +2847,7 @@ class DateTimeCommandRequest : public CommandProtoMessage { }; #endif #ifdef USE_UPDATE -class ListEntitiesUpdateResponse : public InfoResponseProtoMessage { +class ListEntitiesUpdateResponse final : public InfoResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 116; static constexpr uint8_t ESTIMATED_SIZE = 49; @@ -2864,7 +2864,7 @@ class ListEntitiesUpdateResponse : public InfoResponseProtoMessage { protected: }; -class UpdateStateResponse : public StateResponseProtoMessage { +class UpdateStateResponse final : public StateResponseProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 117; static constexpr uint8_t ESTIMATED_SIZE = 65; @@ -2893,7 +2893,7 @@ class UpdateStateResponse : public StateResponseProtoMessage { protected: }; -class UpdateCommandRequest : public CommandProtoMessage { +class UpdateCommandRequest final : public CommandProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 118; static constexpr uint8_t ESTIMATED_SIZE = 11; diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 3396e5ad05..511d70d3ec 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -1952,7 +1952,7 @@ def build_message_type( dump_impl += "}\n" if base_class: - out = f"class {desc.name} : public {base_class} {{\n" + out = f"class {desc.name} final : public {base_class} {{\n" else: # Check if message has any non-deprecated fields has_fields = any(not field.options.deprecated for field in desc.field) @@ -1961,7 +1961,7 @@ def build_message_type( base_class = "ProtoDecodableMessage" else: base_class = "ProtoMessage" - out = f"class {desc.name} : public {base_class} {{\n" + out = f"class {desc.name} final : public {base_class} {{\n" out += " public:\n" out += indent("\n".join(public_content)) + "\n" out += "\n" From efaeb9180301b1fdf12a0f0d7d74458731626b22 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 18 Aug 2025 17:01:45 -0400 Subject: [PATCH 028/208] [api] Mark APIConnection as final for compiler optimizations (#10279) --- esphome/components/api/api_connection.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 076dccfad7..f711502746 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -44,7 +44,7 @@ static constexpr size_t MAX_PACKETS_PER_BATCH = 64; // ESP32 has 8KB+ stack, HO static constexpr size_t MAX_PACKETS_PER_BATCH = 32; // ESP8266/RP2040/etc have smaller stacks #endif -class APIConnection : public APIServerConnection { +class APIConnection final : public APIServerConnection { public: friend class APIServer; friend class ListEntitiesIterator; From 44bd8e5b54fcd4d13ee0976164203dc41246a584 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 18 Aug 2025 17:14:20 -0400 Subject: [PATCH 029/208] [api] Optimize protobuf decode loop for better performance and maintainability (#10277) --- esphome/components/api/proto.cpp | 70 +++++++++++++++----------------- 1 file changed, 33 insertions(+), 37 deletions(-) diff --git a/esphome/components/api/proto.cpp b/esphome/components/api/proto.cpp index cb6c07ec3c..afda5d32ba 100644 --- a/esphome/components/api/proto.cpp +++ b/esphome/components/api/proto.cpp @@ -8,74 +8,70 @@ namespace esphome::api { static const char *const TAG = "api.proto"; void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) { - uint32_t i = 0; - bool error = false; - while (i < length) { + const uint8_t *ptr = buffer; + const uint8_t *end = buffer + length; + + while (ptr < end) { uint32_t consumed; - auto res = ProtoVarInt::parse(&buffer[i], length - i, &consumed); + + // Parse field header + auto res = ProtoVarInt::parse(ptr, end - ptr, &consumed); if (!res.has_value()) { - ESP_LOGV(TAG, "Invalid field start at %" PRIu32, i); - break; + ESP_LOGV(TAG, "Invalid field start at offset %ld", (long) (ptr - buffer)); + return; } - uint32_t field_type = (res->as_uint32()) & 0b111; - uint32_t field_id = (res->as_uint32()) >> 3; - i += consumed; + uint32_t tag = res->as_uint32(); + uint32_t field_type = tag & 0b111; + uint32_t field_id = tag >> 3; + ptr += consumed; switch (field_type) { case 0: { // VarInt - res = ProtoVarInt::parse(&buffer[i], length - i, &consumed); + res = ProtoVarInt::parse(ptr, end - ptr, &consumed); if (!res.has_value()) { - ESP_LOGV(TAG, "Invalid VarInt at %" PRIu32, i); - error = true; - break; + ESP_LOGV(TAG, "Invalid VarInt at offset %ld", (long) (ptr - buffer)); + return; } if (!this->decode_varint(field_id, *res)) { ESP_LOGV(TAG, "Cannot decode VarInt field %" PRIu32 " with value %" PRIu32 "!", field_id, res->as_uint32()); } - i += consumed; + ptr += consumed; break; } case 2: { // Length-delimited - res = ProtoVarInt::parse(&buffer[i], length - i, &consumed); + res = ProtoVarInt::parse(ptr, end - ptr, &consumed); if (!res.has_value()) { - ESP_LOGV(TAG, "Invalid Length Delimited at %" PRIu32, i); - error = true; - break; + ESP_LOGV(TAG, "Invalid Length Delimited at offset %ld", (long) (ptr - buffer)); + return; } uint32_t field_length = res->as_uint32(); - i += consumed; - if (field_length > length - i) { - ESP_LOGV(TAG, "Out-of-bounds Length Delimited at %" PRIu32, i); - error = true; - break; + ptr += consumed; + if (ptr + field_length > end) { + ESP_LOGV(TAG, "Out-of-bounds Length Delimited at offset %ld", (long) (ptr - buffer)); + return; } - if (!this->decode_length(field_id, ProtoLengthDelimited(&buffer[i], field_length))) { + if (!this->decode_length(field_id, ProtoLengthDelimited(ptr, field_length))) { ESP_LOGV(TAG, "Cannot decode Length Delimited field %" PRIu32 "!", field_id); } - i += field_length; + ptr += field_length; break; } case 5: { // 32-bit - if (length - i < 4) { - ESP_LOGV(TAG, "Out-of-bounds Fixed32-bit at %" PRIu32, i); - error = true; - break; + if (ptr + 4 > end) { + ESP_LOGV(TAG, "Out-of-bounds Fixed32-bit at offset %ld", (long) (ptr - buffer)); + return; } - uint32_t val = encode_uint32(buffer[i + 3], buffer[i + 2], buffer[i + 1], buffer[i]); + uint32_t val = encode_uint32(ptr[3], ptr[2], ptr[1], ptr[0]); if (!this->decode_32bit(field_id, Proto32Bit(val))) { ESP_LOGV(TAG, "Cannot decode 32-bit field %" PRIu32 " with value %" PRIu32 "!", field_id, val); } - i += 4; + ptr += 4; break; } default: - ESP_LOGV(TAG, "Invalid field type at %" PRIu32, i); - error = true; - break; - } - if (error) { - break; + ESP_LOGV(TAG, "Invalid field type %u at offset %ld", field_type, (long) (ptr - buffer)); + return; } } } From 7118bea031b74b17beb765d041ef08f6581f2466 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 19 Aug 2025 12:17:34 +1200 Subject: [PATCH 030/208] [esp32] Write variant to sdkconfig file (#10267) --- esphome/components/esp32/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index c219b8851a..ac236f4eb3 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -824,8 +824,9 @@ async def to_code(config): cg.set_cpp_standard("gnu++20") 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]}") - cg.add_define("ESPHOME_VARIANT", VARIANT_FRIENDLY[config[CONF_VARIANT]]) + variant = config[CONF_VARIANT] + cg.add_build_flag(f"-DUSE_ESP32_VARIANT_{variant}") + cg.add_define("ESPHOME_VARIANT", VARIANT_FRIENDLY[variant]) cg.add_define(ThreadModel.MULTI_ATOMICS) cg.add_platformio_option("lib_ldf_mode", "off") @@ -859,6 +860,7 @@ async def to_code(config): cg.add_platformio_option( "platform_packages", ["espressif/toolchain-esp32ulp@2.35.0-20220830"] ) + add_idf_sdkconfig_option(f"CONFIG_IDF_TARGET_{variant}", True) add_idf_sdkconfig_option( f"CONFIG_ESPTOOLPY_FLASHSIZE_{config[CONF_FLASH_SIZE]}", True ) From 3a6a66537c97189b99b2fddc79f8cf38274d52fe Mon Sep 17 00:00:00 2001 From: Ben Winslow Date: Mon, 18 Aug 2025 20:20:13 -0400 Subject: [PATCH 031/208] [nextion] Don't include terminating NUL in nextion text_sensor states (#10273) --- esphome/components/nextion/nextion.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index 133bd2947c..b348bc9920 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -764,7 +764,8 @@ void Nextion::process_nextion_commands_() { variable_name = to_process.substr(0, index); ++index; - text_value = to_process.substr(index); + // Get variable value without terminating NUL byte. Length check above ensures substr len >= 0. + text_value = to_process.substr(index, to_process_length - index - 1); ESP_LOGN(TAG, "Text sensor: %s='%s'", variable_name.c_str(), text_value.c_str()); From 8f118232e43e5b1d03cbd7fc0c5a363bd260d162 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 19 Aug 2025 15:35:48 +1200 Subject: [PATCH 032/208] [CI] Rename and expand needs-docs workflow (#10299) --- .github/workflows/needs-docs.yml | 24 ------------------ .github/workflows/status-check-labels.yml | 30 +++++++++++++++++++++++ 2 files changed, 30 insertions(+), 24 deletions(-) delete mode 100644 .github/workflows/needs-docs.yml create mode 100644 .github/workflows/status-check-labels.yml diff --git a/.github/workflows/needs-docs.yml b/.github/workflows/needs-docs.yml deleted file mode 100644 index 628b5cc5e3..0000000000 --- a/.github/workflows/needs-docs.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: Needs Docs - -on: - pull_request: - types: [labeled, unlabeled] - -jobs: - check: - name: Check - runs-on: ubuntu-latest - steps: - - name: Check for needs-docs label - uses: actions/github-script@v7.0.1 - with: - script: | - const { data: labels } = await github.rest.issues.listLabelsOnIssue({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number - }); - const needsDocs = labels.find(label => label.name === 'needs-docs'); - if (needsDocs) { - core.setFailed('Pull request needs docs'); - } diff --git a/.github/workflows/status-check-labels.yml b/.github/workflows/status-check-labels.yml new file mode 100644 index 0000000000..157f60f3a1 --- /dev/null +++ b/.github/workflows/status-check-labels.yml @@ -0,0 +1,30 @@ +name: Status check labels + +on: + pull_request: + types: [labeled, unlabeled] + +jobs: + check: + name: Check ${{ matrix.label }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + label: + - needs-docs + - merge-after-release + steps: + - name: Check for ${{ matrix.label }} label + uses: actions/github-script@v7.0.1 + with: + script: | + const { data: labels } = await github.rest.issues.listLabelsOnIssue({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number + }); + const hasLabel = labels.find(label => label.name === '${{ matrix.label }}'); + if (hasLabel) { + core.setFailed('Pull request cannot be merged, it is labeled as ${{ matrix.label }}'); + } From 7e23d865e6c4dfe857ba1f8cbe1ec7c9f9dcf61e Mon Sep 17 00:00:00 2001 From: Ben Winslow Date: Mon, 18 Aug 2025 23:45:30 -0400 Subject: [PATCH 033/208] [atm90e32] Only read 1 register per SPI transaction per datasheet. (#10258) --- esphome/components/atm90e32/atm90e32.cpp | 21 +++++---------------- esphome/components/atm90e32/atm90e32.h | 1 - 2 files changed, 5 insertions(+), 17 deletions(-) diff --git a/esphome/components/atm90e32/atm90e32.cpp b/esphome/components/atm90e32/atm90e32.cpp index d5fee6ea04..634260b5e9 100644 --- a/esphome/components/atm90e32/atm90e32.cpp +++ b/esphome/components/atm90e32/atm90e32.cpp @@ -382,20 +382,15 @@ float ATM90E32Component::get_setup_priority() const { return setup_priority::IO; // R/C registers can conly be cleared after the LastSPIData register is updated (register 78H) // Peakdetect period: 05H. Bit 15:8 are PeakDet_period in ms. 7:0 are Sag_period // Default is 143FH (20ms, 63ms) -uint16_t ATM90E32Component::read16_transaction_(uint16_t a_register) { +uint16_t ATM90E32Component::read16_(uint16_t a_register) { + this->enable(); + delay_microseconds_safe(1); // min delay between CS low and first SCK is 200ns - 1us is plenty uint8_t addrh = (1 << 7) | ((a_register >> 8) & 0x03); uint8_t addrl = (a_register & 0xFF); uint8_t data[4] = {addrh, addrl, 0x00, 0x00}; this->transfer_array(data, 4); uint16_t output = encode_uint16(data[2], data[3]); ESP_LOGVV(TAG, "read16_ 0x%04" PRIX16 " output 0x%04" PRIX16, a_register, output); - return output; -} - -uint16_t ATM90E32Component::read16_(uint16_t a_register) { - this->enable(); - delay_microseconds_safe(1); // min delay between CS low and first SCK is 200ns - 1us is plenty - uint16_t output = this->read16_transaction_(a_register); delay_microseconds_safe(1); // allow the last clock to propagate before releasing CS this->disable(); delay_microseconds_safe(1); // meet minimum CS high time before next transaction @@ -403,14 +398,8 @@ uint16_t ATM90E32Component::read16_(uint16_t a_register) { } int ATM90E32Component::read32_(uint16_t addr_h, uint16_t addr_l) { - this->enable(); - delay_microseconds_safe(1); - const uint16_t val_h = this->read16_transaction_(addr_h); - delay_microseconds_safe(1); - const uint16_t val_l = this->read16_transaction_(addr_l); - delay_microseconds_safe(1); - this->disable(); - delay_microseconds_safe(1); + const uint16_t val_h = this->read16_(addr_h); + const uint16_t val_l = this->read16_(addr_l); const int32_t val = (val_h << 16) | val_l; ESP_LOGVV(TAG, diff --git a/esphome/components/atm90e32/atm90e32.h b/esphome/components/atm90e32/atm90e32.h index afbd9cf941..938ce512ce 100644 --- a/esphome/components/atm90e32/atm90e32.h +++ b/esphome/components/atm90e32/atm90e32.h @@ -140,7 +140,6 @@ class ATM90E32Component : public PollingComponent, number::Number *ref_currents_[3]{nullptr, nullptr, nullptr}; #endif uint16_t read16_(uint16_t a_register); - uint16_t read16_transaction_(uint16_t a_register); int read32_(uint16_t addr_h, uint16_t addr_l); void write16_(uint16_t a_register, uint16_t val, bool validate = true); float get_local_phase_voltage_(uint8_t phase); From 4dab9c44003d5d9afc51be347ab5456f80752300 Mon Sep 17 00:00:00 2001 From: Patrick Date: Tue, 19 Aug 2025 05:52:01 +0200 Subject: [PATCH 034/208] [pipsolar] fix faults_present, fix update interval (#10289) --- esphome/components/pipsolar/pipsolar.cpp | 96 +++++++++++++----------- esphome/components/pipsolar/pipsolar.h | 9 ++- 2 files changed, 59 insertions(+), 46 deletions(-) diff --git a/esphome/components/pipsolar/pipsolar.cpp b/esphome/components/pipsolar/pipsolar.cpp index 40405114a4..5751ad59f5 100644 --- a/esphome/components/pipsolar/pipsolar.cpp +++ b/esphome/components/pipsolar/pipsolar.cpp @@ -23,20 +23,18 @@ void Pipsolar::loop() { // Read message if (this->state_ == STATE_IDLE) { this->empty_uart_buffer_(); - switch (this->send_next_command_()) { - case 0: - // no command send (empty queue) time to poll - if (millis() - this->last_poll_ > this->update_interval_) { - this->send_next_poll_(); - this->last_poll_ = millis(); - } - return; - break; - case 1: - // command send - return; - break; + + if (this->send_next_command_()) { + // command sent + return; } + + if (this->send_next_poll_()) { + // poll sent + return; + } + + return; } if (this->state_ == STATE_COMMAND_COMPLETE) { if (this->check_incoming_length_(4)) { @@ -530,7 +528,7 @@ void Pipsolar::loop() { // '(00000000000000000000000000000000' // iterate over all available flag (as not all models have all flags, but at least in the same order) this->value_warnings_present_ = false; - this->value_faults_present_ = true; + this->value_faults_present_ = false; for (size_t i = 1; i < strlen(tmp); i++) { enabled = tmp[i] == '1'; @@ -708,6 +706,7 @@ void Pipsolar::loop() { return; } // crc ok + this->used_polling_commands_[this->last_polling_command_].needs_update = false; this->state_ = STATE_POLL_CHECKED; return; } else { @@ -788,7 +787,7 @@ uint8_t Pipsolar::check_incoming_crc_() { } // send next command used -uint8_t Pipsolar::send_next_command_() { +bool Pipsolar::send_next_command_() { uint16_t crc16; if (!this->command_queue_[this->command_queue_position_].empty()) { const char *command = this->command_queue_[this->command_queue_position_].c_str(); @@ -809,37 +808,43 @@ uint8_t Pipsolar::send_next_command_() { // end Byte this->write(0x0D); ESP_LOGD(TAG, "Sending command from queue: %s with length %d", command, length); - return 1; + return true; } - return 0; + return false; } -void Pipsolar::send_next_poll_() { +bool Pipsolar::send_next_poll_() { uint16_t crc16; - this->last_polling_command_ = (this->last_polling_command_ + 1) % 15; - if (this->used_polling_commands_[this->last_polling_command_].length == 0) { - this->last_polling_command_ = 0; + + for (uint8_t i = 0; i < POLLING_COMMANDS_MAX; i++) { + this->last_polling_command_ = (this->last_polling_command_ + 1) % POLLING_COMMANDS_MAX; + if (this->used_polling_commands_[this->last_polling_command_].length == 0) { + // not enabled + continue; + } + if (!this->used_polling_commands_[this->last_polling_command_].needs_update) { + // no update requested + continue; + } + this->state_ = STATE_POLL; + this->command_start_millis_ = millis(); + this->empty_uart_buffer_(); + this->read_pos_ = 0; + crc16 = this->pipsolar_crc_(this->used_polling_commands_[this->last_polling_command_].command, + this->used_polling_commands_[this->last_polling_command_].length); + this->write_array(this->used_polling_commands_[this->last_polling_command_].command, + this->used_polling_commands_[this->last_polling_command_].length); + // checksum + this->write(((uint8_t) ((crc16) >> 8))); // highbyte + this->write(((uint8_t) ((crc16) &0xff))); // lowbyte + // end Byte + this->write(0x0D); + ESP_LOGD(TAG, "Sending polling command : %s with length %d", + this->used_polling_commands_[this->last_polling_command_].command, + this->used_polling_commands_[this->last_polling_command_].length); + return true; } - if (this->used_polling_commands_[this->last_polling_command_].length == 0) { - // no command specified - return; - } - this->state_ = STATE_POLL; - this->command_start_millis_ = millis(); - this->empty_uart_buffer_(); - this->read_pos_ = 0; - crc16 = this->pipsolar_crc_(this->used_polling_commands_[this->last_polling_command_].command, - this->used_polling_commands_[this->last_polling_command_].length); - this->write_array(this->used_polling_commands_[this->last_polling_command_].command, - this->used_polling_commands_[this->last_polling_command_].length); - // checksum - this->write(((uint8_t) ((crc16) >> 8))); // highbyte - this->write(((uint8_t) ((crc16) &0xff))); // lowbyte - // end Byte - this->write(0x0D); - ESP_LOGD(TAG, "Sending polling command : %s with length %d", - this->used_polling_commands_[this->last_polling_command_].command, - this->used_polling_commands_[this->last_polling_command_].length); + return false; } void Pipsolar::queue_command_(const char *command, uint8_t length) { @@ -869,7 +874,13 @@ void Pipsolar::dump_config() { } } } -void Pipsolar::update() {} +void Pipsolar::update() { + for (auto &used_polling_command : this->used_polling_commands_) { + if (used_polling_command.length != 0) { + used_polling_command.needs_update = true; + } + } +} void Pipsolar::add_polling_command_(const char *command, ENUMPollingCommand polling_command) { for (auto &used_polling_command : this->used_polling_commands_) { @@ -891,6 +902,7 @@ void Pipsolar::add_polling_command_(const char *command, ENUMPollingCommand poll used_polling_command.errors = 0; used_polling_command.identifier = polling_command; used_polling_command.length = length - 1; + used_polling_command.needs_update = true; return; } } diff --git a/esphome/components/pipsolar/pipsolar.h b/esphome/components/pipsolar/pipsolar.h index 373911b2d7..77b18badb9 100644 --- a/esphome/components/pipsolar/pipsolar.h +++ b/esphome/components/pipsolar/pipsolar.h @@ -25,6 +25,7 @@ struct PollingCommand { uint8_t length = 0; uint8_t errors; ENUMPollingCommand identifier; + bool needs_update; }; #define PIPSOLAR_VALUED_ENTITY_(type, name, polling_command, value_type) \ @@ -189,14 +190,14 @@ class Pipsolar : public uart::UARTDevice, public PollingComponent { static const size_t PIPSOLAR_READ_BUFFER_LENGTH = 110; // maximum supported answer length static const size_t COMMAND_QUEUE_LENGTH = 10; static const size_t COMMAND_TIMEOUT = 5000; - uint32_t last_poll_ = 0; + static const size_t POLLING_COMMANDS_MAX = 15; void add_polling_command_(const char *command, ENUMPollingCommand polling_command); void empty_uart_buffer_(); uint8_t check_incoming_crc_(); uint8_t check_incoming_length_(uint8_t length); uint16_t pipsolar_crc_(uint8_t *msg, uint8_t len); - uint8_t send_next_command_(); - void send_next_poll_(); + bool send_next_command_(); + bool send_next_poll_(); void queue_command_(const char *command, uint8_t length); std::string command_queue_[COMMAND_QUEUE_LENGTH]; uint8_t command_queue_position_ = 0; @@ -216,7 +217,7 @@ class Pipsolar : public uart::UARTDevice, public PollingComponent { }; uint8_t last_polling_command_ = 0; - PollingCommand used_polling_commands_[15]; + PollingCommand used_polling_commands_[POLLING_COMMANDS_MAX]; }; } // namespace pipsolar From 2aaf951357d6f5c82e1b8d737043a0225671f6b8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 19 Aug 2025 14:27:22 -0500 Subject: [PATCH 035/208] [bluetooth_proxy] Fix connection slot race by deferring slot release until GATT close (#10303) --- .../bluetooth_proxy/bluetooth_connection.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp index d2cbdeb984..540492f8c5 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_connection.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_connection.cpp @@ -375,10 +375,19 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga switch (event) { case ESP_GATTC_DISCONNECT_EVT: { - this->reset_connection_(param->disconnect.reason); + // Don't reset connection yet - wait for CLOSE_EVT to ensure controller has freed resources + // This prevents race condition where we mark slot as free before controller cleanup is complete + ESP_LOGD(TAG, "[%d] [%s] Disconnect, reason=0x%02x", this->connection_index_, this->address_str_.c_str(), + param->disconnect.reason); + // Send disconnection notification but don't free the slot yet + this->proxy_->send_device_connection(this->address_, false, 0, param->disconnect.reason); break; } case ESP_GATTC_CLOSE_EVT: { + ESP_LOGD(TAG, "[%d] [%s] Close, reason=0x%02x, freeing slot", this->connection_index_, this->address_str_.c_str(), + param->close.reason); + // Now the GATT connection is fully closed and controller resources are freed + // Safe to mark the connection slot as available this->reset_connection_(param->close.reason); break; } From a8775ba60b7292658aaba4d7ddaa01028fa9dd81 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 19 Aug 2025 16:57:24 -0500 Subject: [PATCH 036/208] [safe_mode] Reduce flash usage by 184 bytes through code optimization (#10284) --- esphome/components/safe_mode/safe_mode.cpp | 70 +++++++++++----------- 1 file changed, 36 insertions(+), 34 deletions(-) diff --git a/esphome/components/safe_mode/safe_mode.cpp b/esphome/components/safe_mode/safe_mode.cpp index 5a62604269..62bbca4fb1 100644 --- a/esphome/components/safe_mode/safe_mode.cpp +++ b/esphome/components/safe_mode/safe_mode.cpp @@ -15,11 +15,11 @@ namespace safe_mode { static const char *const TAG = "safe_mode"; void SafeModeComponent::dump_config() { - ESP_LOGCONFIG(TAG, "Safe Mode:"); ESP_LOGCONFIG(TAG, - " Boot considered successful after %" PRIu32 " seconds\n" - " Invoke after %u boot attempts\n" - " Remain for %" PRIu32 " seconds", + "Safe Mode:\n" + " Successful after: %" PRIu32 "s\n" + " Invoke after: %u attempts\n" + " Duration: %" PRIu32 "s", this->safe_mode_boot_is_good_after_ / 1000, // because milliseconds this->safe_mode_num_attempts_, this->safe_mode_enable_time_ / 1000); // because milliseconds @@ -27,7 +27,7 @@ void SafeModeComponent::dump_config() { if (this->safe_mode_rtc_value_ > 1 && this->safe_mode_rtc_value_ != SafeModeComponent::ENTER_SAFE_MODE_MAGIC) { auto remaining_restarts = this->safe_mode_num_attempts_ - this->safe_mode_rtc_value_; if (remaining_restarts) { - ESP_LOGW(TAG, "Last reset occurred too quickly; will be invoked in %" PRIu32 " restarts", remaining_restarts); + ESP_LOGW(TAG, "Last reset too quick; invoke in %" PRIu32 " restarts", remaining_restarts); } else { ESP_LOGW(TAG, "SAFE MODE IS ACTIVE"); } @@ -72,43 +72,45 @@ bool SafeModeComponent::should_enter_safe_mode(uint8_t num_attempts, uint32_t en this->safe_mode_boot_is_good_after_ = boot_is_good_after; this->safe_mode_num_attempts_ = num_attempts; this->rtc_ = global_preferences->make_preference(233825507UL, false); - this->safe_mode_rtc_value_ = this->read_rtc_(); - bool is_manual_safe_mode = this->safe_mode_rtc_value_ == SafeModeComponent::ENTER_SAFE_MODE_MAGIC; + uint32_t rtc_val = this->read_rtc_(); + this->safe_mode_rtc_value_ = rtc_val; - if (is_manual_safe_mode) { - ESP_LOGI(TAG, "Safe mode invoked manually"); + bool is_manual = rtc_val == SafeModeComponent::ENTER_SAFE_MODE_MAGIC; + + if (is_manual) { + ESP_LOGI(TAG, "Manual mode"); } else { - ESP_LOGCONFIG(TAG, "There have been %" PRIu32 " suspected unsuccessful boot attempts", this->safe_mode_rtc_value_); + ESP_LOGCONFIG(TAG, "Unsuccessful boot attempts: %" PRIu32, rtc_val); } - if (this->safe_mode_rtc_value_ >= num_attempts || is_manual_safe_mode) { - this->clean_rtc(); - - if (!is_manual_safe_mode) { - ESP_LOGE(TAG, "Boot loop detected. Proceeding"); - } - - this->status_set_error(); - this->set_timeout(enable_time, []() { - ESP_LOGW(TAG, "Safe mode enable time has elapsed -- restarting"); - App.reboot(); - }); - - // Delay here to allow power to stabilize before Wi-Fi/Ethernet is initialised - delay(300); // NOLINT - App.setup(); - - ESP_LOGW(TAG, "SAFE MODE IS ACTIVE"); - - this->safe_mode_callback_.call(); - - return true; - } else { + if (rtc_val < num_attempts && !is_manual) { // increment counter - this->write_rtc_(this->safe_mode_rtc_value_ + 1); + this->write_rtc_(rtc_val + 1); return false; } + + this->clean_rtc(); + + if (!is_manual) { + ESP_LOGE(TAG, "Boot loop detected"); + } + + this->status_set_error(); + this->set_timeout(enable_time, []() { + ESP_LOGW(TAG, "Timeout, restarting"); + App.reboot(); + }); + + // Delay here to allow power to stabilize before Wi-Fi/Ethernet is initialised + delay(300); // NOLINT + App.setup(); + + ESP_LOGW(TAG, "SAFE MODE IS ACTIVE"); + + this->safe_mode_callback_.call(); + + return true; } void SafeModeComponent::write_rtc_(uint32_t val) { From 0e31bc1a67f0ab57d8e89a6c05a16bb53683e776 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 19 Aug 2025 21:26:53 -0500 Subject: [PATCH 037/208] [api] Add zero-copy StringRef methods for compilation_time and effect_name (#10257) --- esphome/components/api/api_connection.cpp | 8 ++------ esphome/components/light/light_state.cpp | 14 ++++++++++++-- esphome/components/light/light_state.h | 3 +++ esphome/core/application.h | 3 +++ 4 files changed, 20 insertions(+), 8 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index ced0f489be..4b3a3e2fc8 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -465,9 +465,7 @@ uint16_t APIConnection::try_send_light_state(EntityBase *entity, APIConnection * resp.cold_white = values.get_cold_white(); resp.warm_white = values.get_warm_white(); if (light->supports_effects()) { - // get_effect_name() returns temporary std::string - must store it - std::string effect_name = light->get_effect_name(); - resp.set_effect(StringRef(effect_name)); + resp.set_effect(light->get_effect_name_ref()); } return fill_and_encode_entity_state(light, resp, LightStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } @@ -1425,9 +1423,7 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) { static constexpr auto ESPHOME_VERSION_REF = StringRef::from_lit(ESPHOME_VERSION); resp.set_esphome_version(ESPHOME_VERSION_REF); - // get_compilation_time() returns temporary std::string - must store it - std::string compilation_time = App.get_compilation_time(); - resp.set_compilation_time(StringRef(compilation_time)); + resp.set_compilation_time(App.get_compilation_time_ref()); // Compile-time StringRef constants for manufacturers #if defined(USE_ESP8266) || defined(USE_ESP32) diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp index 5b57707d6b..9e42b2f1e2 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -140,12 +140,22 @@ float LightState::get_setup_priority() const { return setup_priority::HARDWARE - void LightState::publish_state() { this->remote_values_callback_.call(); } LightOutput *LightState::get_output() const { return this->output_; } + +static constexpr const char *EFFECT_NONE = "None"; +static constexpr auto EFFECT_NONE_REF = StringRef::from_lit("None"); + std::string LightState::get_effect_name() { if (this->active_effect_index_ > 0) { return this->effects_[this->active_effect_index_ - 1]->get_name(); - } else { - return "None"; } + return EFFECT_NONE; +} + +StringRef LightState::get_effect_name_ref() { + if (this->active_effect_index_ > 0) { + return StringRef(this->effects_[this->active_effect_index_ - 1]->get_name()); + } + return EFFECT_NONE_REF; } void LightState::add_new_remote_values_callback(std::function &&send_callback) { diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h index 72cb99223e..94b81dee61 100644 --- a/esphome/components/light/light_state.h +++ b/esphome/components/light/light_state.h @@ -4,6 +4,7 @@ #include "esphome/core/entity_base.h" #include "esphome/core/optional.h" #include "esphome/core/preferences.h" +#include "esphome/core/string_ref.h" #include "light_call.h" #include "light_color_values.h" #include "light_effect.h" @@ -116,6 +117,8 @@ class LightState : public EntityBase, public Component { /// Return the name of the current effect, or if no effect is active "None". std::string get_effect_name(); + /// Return the name of the current effect as StringRef (for API usage) + StringRef get_effect_name_ref(); /** * This lets front-end components subscribe to light change events. This callback is called once diff --git a/esphome/core/application.h b/esphome/core/application.h index 4120afff53..9cb2a4c638 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -10,6 +10,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/preferences.h" #include "esphome/core/scheduler.h" +#include "esphome/core/string_ref.h" #ifdef USE_DEVICES #include "esphome/core/device.h" @@ -248,6 +249,8 @@ class Application { bool is_name_add_mac_suffix_enabled() const { return this->name_add_mac_suffix_; } std::string get_compilation_time() const { return this->compilation_time_; } + /// Get the compilation time as StringRef (for API usage) + StringRef get_compilation_time_ref() const { return StringRef(this->compilation_time_); } /// Get the cached time in milliseconds from when the current component started its loop execution inline uint32_t IRAM_ATTR HOT get_loop_component_start_time() const { return this->loop_component_start_time_; } From 0b50ef227b3164d8729f6c0011a00d047ef07373 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 20 Aug 2025 14:27:08 +1200 Subject: [PATCH 038/208] [helper] Make crc8 function more flexible to avoid reimplementation in individual components (#10201) --- esphome/components/ags10/ags10.cpp | 21 +-- esphome/components/ags10/ags10.h | 14 +- esphome/components/am2315c/am2315c.cpp | 18 +- esphome/components/am2315c/am2315c.h | 5 +- esphome/components/hte501/hte501.cpp | 24 +-- esphome/components/hte501/hte501.h | 5 +- esphome/components/lc709203f/lc709203f.cpp | 21 +-- esphome/components/lc709203f/lc709203f.h | 5 +- esphome/components/mlx90614/mlx90614.cpp | 17 +- esphome/components/mlx90614/mlx90614.h | 1 - esphome/components/tee501/tee501.cpp | 23 +-- esphome/components/tee501/tee501.h | 6 +- esphome/core/helpers.cpp | 29 ++- esphome/core/helpers.h | 4 +- tests/integration/fixtures/crc8_helper.yaml | 17 ++ .../crc8_test_component/__init__.py | 17 ++ .../crc8_test_component.cpp | 170 ++++++++++++++++++ .../crc8_test_component/crc8_test_component.h | 29 +++ tests/integration/test_crc8_helper.py | 100 +++++++++++ 19 files changed, 379 insertions(+), 147 deletions(-) create mode 100644 tests/integration/fixtures/crc8_helper.yaml create mode 100644 tests/integration/fixtures/external_components/crc8_test_component/__init__.py create mode 100644 tests/integration/fixtures/external_components/crc8_test_component/crc8_test_component.cpp create mode 100644 tests/integration/fixtures/external_components/crc8_test_component/crc8_test_component.h create mode 100644 tests/integration/test_crc8_helper.py diff --git a/esphome/components/ags10/ags10.cpp b/esphome/components/ags10/ags10.cpp index 9a29a979f3..fa7170114c 100644 --- a/esphome/components/ags10/ags10.cpp +++ b/esphome/components/ags10/ags10.cpp @@ -89,7 +89,7 @@ void AGS10Component::dump_config() { bool AGS10Component::new_i2c_address(uint8_t newaddress) { uint8_t rev_newaddress = ~newaddress; std::array data{newaddress, rev_newaddress, newaddress, rev_newaddress, 0}; - data[4] = calc_crc8_(data, 4); + data[4] = crc8(data.data(), 4, 0xFF, 0x31, true); if (!this->write_bytes(REG_ADDRESS, data)) { this->error_code_ = COMMUNICATION_FAILED; this->status_set_warning(); @@ -109,7 +109,7 @@ bool AGS10Component::set_zero_point_with_current_resistance() { return this->set bool AGS10Component::set_zero_point_with(uint16_t value) { std::array data{0x00, 0x0C, (uint8_t) ((value >> 8) & 0xFF), (uint8_t) (value & 0xFF), 0}; - data[4] = calc_crc8_(data, 4); + data[4] = crc8(data.data(), 4, 0xFF, 0x31, true); if (!this->write_bytes(REG_CALIBRATION, data)) { this->error_code_ = COMMUNICATION_FAILED; this->status_set_warning(); @@ -184,7 +184,7 @@ template optional> AGS10Component::read_and_che auto res = *data; auto crc_byte = res[len]; - if (crc_byte != calc_crc8_(res, len)) { + if (crc_byte != crc8(res.data(), len, 0xFF, 0x31, true)) { this->error_code_ = CRC_CHECK_FAILED; ESP_LOGE(TAG, "Reading AGS10 version failed: crc error!"); return optional>(); @@ -192,20 +192,5 @@ template optional> AGS10Component::read_and_che return data; } - -template uint8_t AGS10Component::calc_crc8_(std::array dat, uint8_t num) { - uint8_t i, byte1, crc = 0xFF; - for (byte1 = 0; byte1 < num; byte1++) { - crc ^= (dat[byte1]); - for (i = 0; i < 8; i++) { - if (crc & 0x80) { - crc = (crc << 1) ^ 0x31; - } else { - crc = (crc << 1); - } - } - } - return crc; -} } // namespace ags10 } // namespace esphome diff --git a/esphome/components/ags10/ags10.h b/esphome/components/ags10/ags10.h index 3e184ae176..e0975f14bc 100644 --- a/esphome/components/ags10/ags10.h +++ b/esphome/components/ags10/ags10.h @@ -1,9 +1,9 @@ #pragma once +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/sensor/sensor.h" #include "esphome/core/automation.h" #include "esphome/core/component.h" -#include "esphome/components/sensor/sensor.h" -#include "esphome/components/i2c/i2c.h" namespace esphome { namespace ags10 { @@ -99,16 +99,6 @@ class AGS10Component : public PollingComponent, public i2c::I2CDevice { * Read, checks and returns data from the sensor. */ template optional> read_and_check_(uint8_t a_register); - - /** - * Calculates CRC8 value. - * - * CRC8 calculation, initial value: 0xFF, polynomial: 0x31 (x8+ x5+ x4+1) - * - * @param[in] dat the data buffer - * @param num number of bytes in the buffer - */ - template uint8_t calc_crc8_(std::array dat, uint8_t num); }; template class AGS10NewI2cAddressAction : public Action, public Parented { diff --git a/esphome/components/am2315c/am2315c.cpp b/esphome/components/am2315c/am2315c.cpp index 048c34d749..b20a8c6cbb 100644 --- a/esphome/components/am2315c/am2315c.cpp +++ b/esphome/components/am2315c/am2315c.cpp @@ -29,22 +29,6 @@ namespace am2315c { static const char *const TAG = "am2315c"; -uint8_t AM2315C::crc8_(uint8_t *data, uint8_t len) { - uint8_t crc = 0xFF; - while (len--) { - crc ^= *data++; - for (uint8_t i = 0; i < 8; i++) { - if (crc & 0x80) { - crc <<= 1; - crc ^= 0x31; - } else { - crc <<= 1; - } - } - } - return crc; -} - bool AM2315C::reset_register_(uint8_t reg) { // code based on demo code sent by www.aosong.com // no further documentation. @@ -86,7 +70,7 @@ bool AM2315C::convert_(uint8_t *data, float &humidity, float &temperature) { humidity = raw * 9.5367431640625e-5; raw = ((data[3] & 0x0F) << 16) | (data[4] << 8) | data[5]; temperature = raw * 1.9073486328125e-4 - 50; - return this->crc8_(data, 6) == data[6]; + return crc8(data, 6, 0xFF, 0x31, true) == data[6]; } void AM2315C::setup() { diff --git a/esphome/components/am2315c/am2315c.h b/esphome/components/am2315c/am2315c.h index 9cec40e4c2..c8d01beeaa 100644 --- a/esphome/components/am2315c/am2315c.h +++ b/esphome/components/am2315c/am2315c.h @@ -21,9 +21,9 @@ // SOFTWARE. #pragma once -#include "esphome/core/component.h" -#include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/core/component.h" namespace esphome { namespace am2315c { @@ -39,7 +39,6 @@ class AM2315C : public PollingComponent, public i2c::I2CDevice { void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; } protected: - uint8_t crc8_(uint8_t *data, uint8_t len); bool convert_(uint8_t *data, float &humidity, float &temperature); bool reset_register_(uint8_t reg); diff --git a/esphome/components/hte501/hte501.cpp b/esphome/components/hte501/hte501.cpp index 75770ceffe..fa81640f50 100644 --- a/esphome/components/hte501/hte501.cpp +++ b/esphome/components/hte501/hte501.cpp @@ -12,7 +12,7 @@ void HTE501Component::setup() { this->write(address, 2, false); uint8_t identification[9]; this->read(identification, 9); - if (identification[8] != calc_crc8_(identification, 0, 7)) { + if (identification[8] != crc8(identification, 8, 0xFF, 0x31, true)) { this->error_code_ = CRC_CHECK_FAILED; this->mark_failed(); return; @@ -46,7 +46,8 @@ void HTE501Component::update() { this->set_timeout(50, [this]() { uint8_t i2c_response[6]; this->read(i2c_response, 6); - if (i2c_response[2] != calc_crc8_(i2c_response, 0, 1) && i2c_response[5] != calc_crc8_(i2c_response, 3, 4)) { + if (i2c_response[2] != crc8(i2c_response, 2, 0xFF, 0x31, true) && + i2c_response[5] != crc8(i2c_response + 3, 2, 0xFF, 0x31, true)) { this->error_code_ = CRC_CHECK_FAILED; this->status_set_warning(); return; @@ -67,24 +68,5 @@ void HTE501Component::update() { this->status_clear_warning(); }); } - -unsigned char HTE501Component::calc_crc8_(const unsigned char buf[], unsigned char from, unsigned char to) { - unsigned char crc_val = 0xFF; - unsigned char i = 0; - unsigned char j = 0; - for (i = from; i <= to; i++) { - int cur_val = buf[i]; - for (j = 0; j < 8; j++) { - if (((crc_val ^ cur_val) & 0x80) != 0) // If MSBs are not equal - { - crc_val = ((crc_val << 1) ^ 0x31); - } else { - crc_val = (crc_val << 1); - } - cur_val = cur_val << 1; - } - } - return crc_val; -} } // namespace hte501 } // namespace esphome diff --git a/esphome/components/hte501/hte501.h b/esphome/components/hte501/hte501.h index 0d2c952e81..a7072d5bdb 100644 --- a/esphome/components/hte501/hte501.h +++ b/esphome/components/hte501/hte501.h @@ -1,8 +1,8 @@ #pragma once -#include "esphome/core/component.h" -#include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/core/component.h" namespace esphome { namespace hte501 { @@ -19,7 +19,6 @@ class HTE501Component : public PollingComponent, public i2c::I2CDevice { void update() override; protected: - unsigned char calc_crc8_(const unsigned char buf[], unsigned char from, unsigned char to); sensor::Sensor *temperature_sensor_; sensor::Sensor *humidity_sensor_; diff --git a/esphome/components/lc709203f/lc709203f.cpp b/esphome/components/lc709203f/lc709203f.cpp index e5d12a75d4..f711cb4f0e 100644 --- a/esphome/components/lc709203f/lc709203f.cpp +++ b/esphome/components/lc709203f/lc709203f.cpp @@ -1,5 +1,6 @@ -#include "esphome/core/log.h" #include "lc709203f.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" namespace esphome { namespace lc709203f { @@ -189,7 +190,7 @@ uint8_t Lc709203f::get_register_(uint8_t register_to_read, uint16_t *register_va // Error on the i2c bus this->status_set_warning( str_sprintf("Error code %d when reading from register 0x%02X", return_code, register_to_read).c_str()); - } else if (this->crc8_(read_buffer, 5) != read_buffer[5]) { + } else if (crc8(read_buffer, 5, 0x00, 0x07, true) != read_buffer[5]) { // I2C indicated OK, but the CRC of the data does not matcth. this->status_set_warning(str_sprintf("CRC error reading from register 0x%02X", register_to_read).c_str()); } else { @@ -220,7 +221,7 @@ uint8_t Lc709203f::set_register_(uint8_t register_to_set, uint16_t value_to_set) write_buffer[1] = register_to_set; write_buffer[2] = value_to_set & 0xFF; // Low byte write_buffer[3] = (value_to_set >> 8) & 0xFF; // High byte - write_buffer[4] = this->crc8_(write_buffer, 4); + write_buffer[4] = crc8(write_buffer, 4, 0x00, 0x07, true); for (uint8_t i = 0; i <= LC709203F_I2C_RETRY_COUNT; i++) { // Note: we don't write the first byte of the write buffer to the device. @@ -239,20 +240,6 @@ uint8_t Lc709203f::set_register_(uint8_t register_to_set, uint16_t value_to_set) return return_code; } -uint8_t Lc709203f::crc8_(uint8_t *byte_buffer, uint8_t length_of_crc) { - uint8_t crc = 0x00; - const uint8_t polynomial(0x07); - - for (uint8_t j = length_of_crc; j; --j) { - crc ^= *byte_buffer++; - - for (uint8_t i = 8; i; --i) { - crc = (crc & 0x80) ? (crc << 1) ^ polynomial : (crc << 1); - } - } - return crc; -} - void Lc709203f::set_pack_size(uint16_t pack_size) { static const uint16_t PACK_SIZE_ARRAY[6] = {100, 200, 500, 1000, 2000, 3000}; static const uint16_t APA_ARRAY[6] = {0x08, 0x0B, 0x10, 0x19, 0x2D, 0x36}; diff --git a/esphome/components/lc709203f/lc709203f.h b/esphome/components/lc709203f/lc709203f.h index 3b5b04775f..59988a0079 100644 --- a/esphome/components/lc709203f/lc709203f.h +++ b/esphome/components/lc709203f/lc709203f.h @@ -1,8 +1,8 @@ #pragma once -#include "esphome/core/component.h" -#include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/core/component.h" namespace esphome { namespace lc709203f { @@ -38,7 +38,6 @@ class Lc709203f : public sensor::Sensor, public PollingComponent, public i2c::I2 private: uint8_t get_register_(uint8_t register_to_read, uint16_t *register_value); uint8_t set_register_(uint8_t register_to_set, uint16_t value_to_set); - uint8_t crc8_(uint8_t *byte_buffer, uint8_t length_of_crc); protected: sensor::Sensor *voltage_sensor_{nullptr}; diff --git a/esphome/components/mlx90614/mlx90614.cpp b/esphome/components/mlx90614/mlx90614.cpp index 2e711baf9a..24024df090 100644 --- a/esphome/components/mlx90614/mlx90614.cpp +++ b/esphome/components/mlx90614/mlx90614.cpp @@ -50,28 +50,13 @@ bool MLX90614Component::write_emissivity_() { return true; } -uint8_t MLX90614Component::crc8_pec_(const uint8_t *data, uint8_t len) { - uint8_t crc = 0; - for (uint8_t i = 0; i < len; i++) { - uint8_t in = data[i]; - for (uint8_t j = 0; j < 8; j++) { - bool carry = (crc ^ in) & 0x80; - crc <<= 1; - if (carry) - crc ^= 0x07; - in <<= 1; - } - } - return crc; -} - bool MLX90614Component::write_bytes_(uint8_t reg, uint16_t data) { uint8_t buf[5]; buf[0] = this->address_ << 1; buf[1] = reg; buf[2] = data & 0xFF; buf[3] = data >> 8; - buf[4] = this->crc8_pec_(buf, 4); + buf[4] = crc8(buf, 4, 0x00, 0x07, true); return this->write_bytes(reg, buf + 2, 3); } diff --git a/esphome/components/mlx90614/mlx90614.h b/esphome/components/mlx90614/mlx90614.h index b6bd44172d..fa6fb523bb 100644 --- a/esphome/components/mlx90614/mlx90614.h +++ b/esphome/components/mlx90614/mlx90614.h @@ -22,7 +22,6 @@ class MLX90614Component : public PollingComponent, public i2c::I2CDevice { protected: bool write_emissivity_(); - uint8_t crc8_pec_(const uint8_t *data, uint8_t len); bool write_bytes_(uint8_t reg, uint16_t data); sensor::Sensor *ambient_sensor_{nullptr}; diff --git a/esphome/components/tee501/tee501.cpp b/esphome/components/tee501/tee501.cpp index 460f446865..dc803be775 100644 --- a/esphome/components/tee501/tee501.cpp +++ b/esphome/components/tee501/tee501.cpp @@ -12,7 +12,7 @@ void TEE501Component::setup() { this->write(address, 2, false); uint8_t identification[9]; this->read(identification, 9); - if (identification[8] != calc_crc8_(identification, 0, 7)) { + if (identification[8] != crc8(identification, 8, 0xFF, 0x31, true)) { this->error_code_ = CRC_CHECK_FAILED; this->mark_failed(); return; @@ -45,7 +45,7 @@ void TEE501Component::update() { this->set_timeout(50, [this]() { uint8_t i2c_response[3]; this->read(i2c_response, 3); - if (i2c_response[2] != calc_crc8_(i2c_response, 0, 1)) { + if (i2c_response[2] != crc8(i2c_response, 2, 0xFF, 0x31, true)) { this->error_code_ = CRC_CHECK_FAILED; this->status_set_warning(); return; @@ -62,24 +62,5 @@ void TEE501Component::update() { }); } -unsigned char TEE501Component::calc_crc8_(const unsigned char buf[], unsigned char from, unsigned char to) { - unsigned char crc_val = 0xFF; - unsigned char i = 0; - unsigned char j = 0; - for (i = from; i <= to; i++) { - int cur_val = buf[i]; - for (j = 0; j < 8; j++) { - if (((crc_val ^ cur_val) & 0x80) != 0) // If MSBs are not equal - { - crc_val = ((crc_val << 1) ^ 0x31); - } else { - crc_val = (crc_val << 1); - } - cur_val = cur_val << 1; - } - } - return crc_val; -} - } // namespace tee501 } // namespace esphome diff --git a/esphome/components/tee501/tee501.h b/esphome/components/tee501/tee501.h index fc655e58c9..2437ac92eb 100644 --- a/esphome/components/tee501/tee501.h +++ b/esphome/components/tee501/tee501.h @@ -1,8 +1,8 @@ #pragma once -#include "esphome/core/component.h" -#include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/core/component.h" namespace esphome { namespace tee501 { @@ -16,8 +16,6 @@ class TEE501Component : public sensor::Sensor, public PollingComponent, public i void update() override; protected: - unsigned char calc_crc8_(const unsigned char buf[], unsigned char from, unsigned char to); - enum ErrorCode { NONE = 0, COMMUNICATION_FAILED, CRC_CHECK_FAILED } error_code_{NONE}; }; diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index e84f5a7317..44e9193994 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -41,17 +41,28 @@ static const uint16_t CRC16_1021_BE_LUT_H[] = {0x0000, 0x1231, 0x2462, 0x3653, 0 // Mathematics -uint8_t crc8(const uint8_t *data, uint8_t len) { - uint8_t crc = 0; - +uint8_t crc8(const uint8_t *data, uint8_t len, uint8_t crc, uint8_t poly, bool msb_first) { while ((len--) != 0u) { uint8_t inbyte = *data++; - for (uint8_t i = 8; i != 0u; i--) { - bool mix = (crc ^ inbyte) & 0x01; - crc >>= 1; - if (mix) - crc ^= 0x8C; - inbyte >>= 1; + if (msb_first) { + // MSB first processing (for polynomials like 0x31, 0x07) + crc ^= inbyte; + for (uint8_t i = 8; i != 0u; i--) { + if (crc & 0x80) { + crc = (crc << 1) ^ poly; + } else { + crc <<= 1; + } + } + } else { + // LSB first processing (default for Dallas/Maxim 0x8C) + for (uint8_t i = 8; i != 0u; i--) { + bool mix = (crc ^ inbyte) & 0x01; + crc >>= 1; + if (mix) + crc ^= poly; + inbyte >>= 1; + } } } return crc; diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index b5fe59c4fd..53ec7a2a5a 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -145,8 +145,8 @@ template T remap(U value, U min, U max, T min_out, T max return (value - min) * (max_out - min_out) / (max - min) + min_out; } -/// Calculate a CRC-8 checksum of \p data with size \p len using the CRC-8-Dallas/Maxim polynomial. -uint8_t crc8(const uint8_t *data, uint8_t len); +/// Calculate a CRC-8 checksum of \p data with size \p len. +uint8_t crc8(const uint8_t *data, uint8_t len, uint8_t crc = 0x00, uint8_t poly = 0x8C, bool msb_first = false); /// Calculate a CRC-16 checksum of \p data with size \p len. uint16_t crc16(const uint8_t *data, uint16_t len, uint16_t crc = 0xffff, uint16_t reverse_poly = 0xa001, diff --git a/tests/integration/fixtures/crc8_helper.yaml b/tests/integration/fixtures/crc8_helper.yaml new file mode 100644 index 0000000000..e97e23eab0 --- /dev/null +++ b/tests/integration/fixtures/crc8_helper.yaml @@ -0,0 +1,17 @@ +esphome: + name: crc8-helper-test + +host: + +api: + +logger: + level: INFO + +external_components: + - source: + type: local + path: EXTERNAL_COMPONENT_PATH + components: [crc8_test_component] + +crc8_test_component: diff --git a/tests/integration/fixtures/external_components/crc8_test_component/__init__.py b/tests/integration/fixtures/external_components/crc8_test_component/__init__.py new file mode 100644 index 0000000000..6032b0861f --- /dev/null +++ b/tests/integration/fixtures/external_components/crc8_test_component/__init__.py @@ -0,0 +1,17 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_ID + +crc8_test_component_ns = cg.esphome_ns.namespace("crc8_test_component") +CRC8TestComponent = crc8_test_component_ns.class_("CRC8TestComponent", cg.Component) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(CRC8TestComponent), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) diff --git a/tests/integration/fixtures/external_components/crc8_test_component/crc8_test_component.cpp b/tests/integration/fixtures/external_components/crc8_test_component/crc8_test_component.cpp new file mode 100644 index 0000000000..6c46af19fd --- /dev/null +++ b/tests/integration/fixtures/external_components/crc8_test_component/crc8_test_component.cpp @@ -0,0 +1,170 @@ +#include "crc8_test_component.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace crc8_test_component { + +static const char *const TAG = "crc8_test"; + +void CRC8TestComponent::setup() { + ESP_LOGI(TAG, "CRC8 Helper Function Integration Test Starting"); + + // Run all test suites + test_crc8_dallas_maxim(); + test_crc8_sensirion_style(); + test_crc8_pec_style(); + test_crc8_parameter_equivalence(); + test_crc8_edge_cases(); + test_component_compatibility(); + + ESP_LOGI(TAG, "CRC8 Integration Test Complete"); +} + +void CRC8TestComponent::test_crc8_dallas_maxim() { + ESP_LOGI(TAG, "Testing Dallas/Maxim CRC8 (default parameters)"); + + // Test vectors for Dallas/Maxim CRC8 (polynomial 0x8C, LSB-first, init 0x00) + const uint8_t test1[] = {0x01}; + const uint8_t test2[] = {0xFF}; + const uint8_t test3[] = {0x12, 0x34}; + const uint8_t test4[] = {0xAA, 0xBB, 0xCC}; + const uint8_t test5[] = {0x01, 0x02, 0x03, 0x04, 0x05}; + + bool all_passed = true; + all_passed &= verify_crc8("Dallas [0x01]", test1, sizeof(test1), 0x5E); + all_passed &= verify_crc8("Dallas [0xFF]", test2, sizeof(test2), 0x35); + all_passed &= verify_crc8("Dallas [0x12, 0x34]", test3, sizeof(test3), 0xA2); + all_passed &= verify_crc8("Dallas [0xAA, 0xBB, 0xCC]", test4, sizeof(test4), 0xD4); + all_passed &= verify_crc8("Dallas [0x01...0x05]", test5, sizeof(test5), 0x2A); + + log_test_result("Dallas/Maxim CRC8", all_passed); +} + +void CRC8TestComponent::test_crc8_sensirion_style() { + ESP_LOGI(TAG, "Testing Sensirion CRC8 (0x31 poly, MSB-first, init 0xFF)"); + + const uint8_t test1[] = {0x00}; + const uint8_t test2[] = {0x01}; + const uint8_t test3[] = {0xFF}; + const uint8_t test4[] = {0x12, 0x34}; + const uint8_t test5[] = {0xBE, 0xEF}; + + bool all_passed = true; + all_passed &= verify_crc8("Sensirion [0x00]", test1, sizeof(test1), 0xAC, 0xFF, 0x31, true); + all_passed &= verify_crc8("Sensirion [0x01]", test2, sizeof(test2), 0x9D, 0xFF, 0x31, true); + all_passed &= verify_crc8("Sensirion [0xFF]", test3, sizeof(test3), 0x00, 0xFF, 0x31, true); + all_passed &= verify_crc8("Sensirion [0x12, 0x34]", test4, sizeof(test4), 0x37, 0xFF, 0x31, true); + all_passed &= verify_crc8("Sensirion [0xBE, 0xEF]", test5, sizeof(test5), 0x92, 0xFF, 0x31, true); + + log_test_result("Sensirion CRC8", all_passed); +} + +void CRC8TestComponent::test_crc8_pec_style() { + ESP_LOGI(TAG, "Testing PEC CRC8 (0x07 poly, MSB-first, init 0x00)"); + + const uint8_t test1[] = {0x00}; + const uint8_t test2[] = {0x01}; + const uint8_t test3[] = {0xFF}; + const uint8_t test4[] = {0x12, 0x34}; + const uint8_t test5[] = {0xAA, 0xBB}; + + bool all_passed = true; + all_passed &= verify_crc8("PEC [0x00]", test1, sizeof(test1), 0x00, 0x00, 0x07, true); + all_passed &= verify_crc8("PEC [0x01]", test2, sizeof(test2), 0x07, 0x00, 0x07, true); + all_passed &= verify_crc8("PEC [0xFF]", test3, sizeof(test3), 0xF3, 0x00, 0x07, true); + all_passed &= verify_crc8("PEC [0x12, 0x34]", test4, sizeof(test4), 0xF1, 0x00, 0x07, true); + all_passed &= verify_crc8("PEC [0xAA, 0xBB]", test5, sizeof(test5), 0xB2, 0x00, 0x07, true); + + log_test_result("PEC CRC8", all_passed); +} + +void CRC8TestComponent::test_crc8_parameter_equivalence() { + ESP_LOGI(TAG, "Testing parameter equivalence"); + + const uint8_t test_data[] = {0x12, 0x34, 0x56, 0x78}; + + // Test that default parameters work as expected + uint8_t default_result = crc8(test_data, sizeof(test_data)); + uint8_t explicit_result = crc8(test_data, sizeof(test_data), 0x00, 0x8C, false); + + bool passed = (default_result == explicit_result); + if (!passed) { + ESP_LOGE(TAG, "Parameter equivalence FAILED: default=0x%02X, explicit=0x%02X", default_result, explicit_result); + } + + log_test_result("Parameter equivalence", passed); +} + +void CRC8TestComponent::test_crc8_edge_cases() { + ESP_LOGI(TAG, "Testing edge cases"); + + bool all_passed = true; + + // Empty array test + const uint8_t empty[] = {}; + uint8_t empty_result = crc8(empty, 0); + bool empty_passed = (empty_result == 0x00); // Should return init value + if (!empty_passed) { + ESP_LOGE(TAG, "Empty array test FAILED: expected 0x00, got 0x%02X", empty_result); + } + all_passed &= empty_passed; + + // Single byte tests + const uint8_t single_zero[] = {0x00}; + const uint8_t single_ff[] = {0xFF}; + all_passed &= verify_crc8("Single [0x00]", single_zero, 1, 0x00); + all_passed &= verify_crc8("Single [0xFF]", single_ff, 1, 0x35); + + log_test_result("Edge cases", all_passed); +} + +void CRC8TestComponent::test_component_compatibility() { + ESP_LOGI(TAG, "Testing component compatibility"); + + // Test specific component use cases + bool all_passed = true; + + // AGS10-style data (Sensirion CRC8) + const uint8_t ags10_data[] = {0x12, 0x34, 0x56}; + uint8_t ags10_result = crc8(ags10_data, sizeof(ags10_data), 0xFF, 0x31, true); + ESP_LOGI(TAG, "AGS10-style CRC8: 0x%02X", ags10_result); + + // LC709203F-style data (PEC CRC8) + const uint8_t lc_data[] = {0xAA, 0xBB}; + uint8_t lc_result = crc8(lc_data, sizeof(lc_data), 0x00, 0x07, true); + ESP_LOGI(TAG, "LC709203F-style CRC8: 0x%02X", lc_result); + + // DallasTemperature-style data (Dallas CRC8) + const uint8_t dallas_data[] = {0x28, 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC}; + uint8_t dallas_result = crc8(dallas_data, sizeof(dallas_data)); + ESP_LOGI(TAG, "Dallas-style CRC8: 0x%02X", dallas_result); + + all_passed = true; // These are just demonstration tests + log_test_result("Component compatibility", all_passed); +} + +bool CRC8TestComponent::verify_crc8(const char *test_name, const uint8_t *data, uint8_t len, uint8_t expected, + uint8_t crc, uint8_t poly, bool msb_first) { + uint8_t result = esphome::crc8(data, len, crc, poly, msb_first); + bool passed = (result == expected); + + if (passed) { + ESP_LOGI(TAG, "%s: PASS (0x%02X)", test_name, result); + } else { + ESP_LOGE(TAG, "%s: FAIL - expected 0x%02X, got 0x%02X", test_name, expected, result); + } + + return passed; +} + +void CRC8TestComponent::log_test_result(const char *test_name, bool passed) { + if (passed) { + ESP_LOGI(TAG, "%s: ALL TESTS PASSED", test_name); + } else { + ESP_LOGE(TAG, "%s: SOME TESTS FAILED", test_name); + } +} + +} // namespace crc8_test_component +} // namespace esphome diff --git a/tests/integration/fixtures/external_components/crc8_test_component/crc8_test_component.h b/tests/integration/fixtures/external_components/crc8_test_component/crc8_test_component.h new file mode 100644 index 0000000000..3b8847259c --- /dev/null +++ b/tests/integration/fixtures/external_components/crc8_test_component/crc8_test_component.h @@ -0,0 +1,29 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace crc8_test_component { + +class CRC8TestComponent : public Component { + public: + void setup() override; + + private: + void test_crc8_dallas_maxim(); + void test_crc8_sensirion_style(); + void test_crc8_pec_style(); + void test_crc8_parameter_equivalence(); + void test_crc8_edge_cases(); + void test_component_compatibility(); + void test_old_vs_new_implementations(); + + void log_test_result(const char *test_name, bool passed); + bool verify_crc8(const char *test_name, const uint8_t *data, uint8_t len, uint8_t expected, uint8_t crc = 0x00, + uint8_t poly = 0x8C, bool msb_first = false); +}; + +} // namespace crc8_test_component +} // namespace esphome diff --git a/tests/integration/test_crc8_helper.py b/tests/integration/test_crc8_helper.py new file mode 100644 index 0000000000..ffe6244598 --- /dev/null +++ b/tests/integration/test_crc8_helper.py @@ -0,0 +1,100 @@ +"""Integration test for CRC8 helper function.""" + +from __future__ import annotations + +import asyncio +from pathlib import Path + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_crc8_helper( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test the CRC8 helper function through integration testing.""" + # Get the path to the external components directory + external_components_path = str( + Path(__file__).parent / "fixtures" / "external_components" + ) + + # Replace the placeholder in the YAML config with the actual path + yaml_config = yaml_config.replace( + "EXTERNAL_COMPONENT_PATH", external_components_path + ) + + # Track test completion with asyncio.Event + test_complete = asyncio.Event() + + # Track test results + test_results = { + "dallas_maxim": False, + "sensirion": False, + "pec": False, + "parameter_equivalence": False, + "edge_cases": False, + "component_compatibility": False, + "setup_started": False, + } + + def on_log_line(line): + """Process log lines to track test progress and results.""" + # Track test start + if "CRC8 Helper Function Integration Test Starting" in line: + test_results["setup_started"] = True + + # Track test completion + elif "CRC8 Integration Test Complete" in line: + test_complete.set() + + # Track individual test results + elif "ALL TESTS PASSED" in line: + if "Dallas/Maxim CRC8" in line: + test_results["dallas_maxim"] = True + elif "Sensirion CRC8" in line: + test_results["sensirion"] = True + elif "PEC CRC8" in line: + test_results["pec"] = True + elif "Parameter equivalence" in line: + test_results["parameter_equivalence"] = True + elif "Edge cases" in line: + test_results["edge_cases"] = True + elif "Component compatibility" in line: + test_results["component_compatibility"] = True + + # Log failures for debugging + elif "TEST FAILED:" in line or "SUBTEST FAILED:" in line: + print(f"CRC8 Test Failure: {line}") + + # Compile and run the test + async with ( + run_compiled(yaml_config, line_callback=on_log_line), + api_client_connected() as client, + ): + # Verify device info + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "crc8-helper-test" + + # Wait for tests to complete with timeout + try: + await asyncio.wait_for(test_complete.wait(), timeout=5.0) + except TimeoutError: + pytest.fail("CRC8 integration test timed out after 5 seconds") + + # Verify all tests passed + assert test_results["setup_started"], "CRC8 test setup never started" + assert test_results["dallas_maxim"], "Dallas/Maxim CRC8 test failed" + assert test_results["sensirion"], "Sensirion CRC8 test failed" + assert test_results["pec"], "PEC CRC8 test failed" + assert test_results["parameter_equivalence"], ( + "Parameter equivalence test failed" + ) + assert test_results["edge_cases"], "Edge cases test failed" + assert test_results["component_compatibility"], ( + "Component compatibility test failed" + ) From 5a1533bea9c7b65862f191ef9b68cb81fe8ed801 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 19 Aug 2025 21:28:13 -0500 Subject: [PATCH 039/208] [api] Avoid object_id string allocations for all entity info messages (#10260) --- esphome/components/api/api_connection.h | 12 +++++++++--- esphome/core/entity_base.cpp | 18 ++++++++++++------ esphome/core/entity_base.h | 11 +++++++++++ 3 files changed, 32 insertions(+), 9 deletions(-) diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index f711502746..6254854238 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -301,9 +301,15 @@ class APIConnection final : public APIServerConnection { APIConnection *conn, uint32_t remaining_size, bool is_single) { // Set common fields that are shared by all entity types msg.key = entity->get_object_id_hash(); - // IMPORTANT: get_object_id() may return a temporary std::string - std::string object_id = entity->get_object_id(); - msg.set_object_id(StringRef(object_id)); + // Try to use static reference first to avoid allocation + StringRef static_ref = entity->get_object_id_ref_for_api_(); + if (!static_ref.empty()) { + msg.set_object_id(static_ref); + } else { + // Dynamic case - need to allocate + std::string object_id = entity->get_object_id(); + msg.set_object_id(StringRef(object_id)); + } if (entity->has_own_name()) { msg.set_name(entity->get_name()); diff --git a/esphome/core/entity_base.cpp b/esphome/core/entity_base.cpp index 2ea9c77a3e..411a877bbf 100644 --- a/esphome/core/entity_base.cpp +++ b/esphome/core/entity_base.cpp @@ -1,6 +1,7 @@ #include "esphome/core/entity_base.h" #include "esphome/core/application.h" #include "esphome/core/helpers.h" +#include "esphome/core/string_ref.h" namespace esphome { @@ -50,13 +51,18 @@ std::string EntityBase::get_object_id() const { if (!this->flags_.has_own_name && App.is_name_add_mac_suffix_enabled()) { // `App.get_friendly_name()` is dynamic. return str_sanitize(str_snake_case(App.get_friendly_name())); - } else { - // `App.get_friendly_name()` is constant. - if (this->object_id_c_str_ == nullptr) { - return ""; - } - return this->object_id_c_str_; } + // `App.get_friendly_name()` is constant. + return this->object_id_c_str_ == nullptr ? "" : this->object_id_c_str_; +} +StringRef EntityBase::get_object_id_ref_for_api_() const { + static constexpr auto EMPTY_STRING = StringRef::from_lit(""); + // Return empty for dynamic case (MAC suffix) + if (!this->flags_.has_own_name && App.is_name_add_mac_suffix_enabled()) { + return EMPTY_STRING; + } + // For static case, return the string or empty if null + return this->object_id_c_str_ == nullptr ? EMPTY_STRING : StringRef(this->object_id_c_str_); } void EntityBase::set_object_id(const char *object_id) { this->object_id_c_str_ = object_id; diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index e60e0728bc..68163ce8c3 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -12,6 +12,11 @@ namespace esphome { +// Forward declaration for friend access +namespace api { +class APIConnection; +} // namespace api + enum EntityCategory : uint8_t { ENTITY_CATEGORY_NONE = 0, ENTITY_CATEGORY_CONFIG = 1, @@ -81,6 +86,12 @@ class EntityBase { void set_has_state(bool state) { this->flags_.has_state = state; } protected: + friend class api::APIConnection; + + // Get object_id as StringRef when it's static (for API usage) + // Returns empty StringRef if object_id is dynamic (needs allocation) + StringRef get_object_id_ref_for_api_() const; + /// The hash_base() function has been deprecated. It is kept in this /// class for now, to prevent external components from not compiling. virtual uint32_t hash_base() { return 0L; } From 9b1ebdb6da89a7b4caca6f70fb70b40623486f14 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 19 Aug 2025 21:34:34 -0500 Subject: [PATCH 040/208] [mdns] Reduce flash usage and prevent RAM over-allocation in service compilation (#10287) --- esphome/components/mdns/mdns_component.cpp | 157 +++++++++++++-------- 1 file changed, 99 insertions(+), 58 deletions(-) diff --git a/esphome/components/mdns/mdns_component.cpp b/esphome/components/mdns/mdns_component.cpp index 640750720d..316a10596f 100644 --- a/esphome/components/mdns/mdns_component.cpp +++ b/esphome/components/mdns/mdns_component.cpp @@ -24,100 +24,139 @@ static const char *const TAG = "mdns"; void MDNSComponent::compile_records_() { this->hostname_ = App.get_name(); - this->services_.clear(); + // Calculate exact capacity needed for services vector + size_t services_count = 0; #ifdef USE_API if (api::global_api_server != nullptr) { - MDNSService service{}; + services_count++; + } +#endif +#ifdef USE_PROMETHEUS + services_count++; +#endif +#ifdef USE_WEBSERVER + services_count++; +#endif +#ifdef USE_MDNS_EXTRA_SERVICES + services_count += this->services_extra_.size(); +#endif + // Reserve for fallback service if needed + if (services_count == 0) { + services_count = 1; + } + this->services_.reserve(services_count); + +#ifdef USE_API + if (api::global_api_server != nullptr) { + this->services_.emplace_back(); + auto &service = this->services_.back(); service.service_type = "_esphomelib"; service.proto = "_tcp"; service.port = api::global_api_server->get_port(); - if (!App.get_friendly_name().empty()) { - service.txt_records.push_back({"friendly_name", App.get_friendly_name()}); - } - service.txt_records.push_back({"version", ESPHOME_VERSION}); - service.txt_records.push_back({"mac", get_mac_address()}); - const char *platform = nullptr; -#ifdef USE_ESP8266 - platform = "ESP8266"; -#endif -#ifdef USE_ESP32 - platform = "ESP32"; -#endif -#ifdef USE_RP2040 - platform = "RP2040"; -#endif -#ifdef USE_LIBRETINY - platform = lt_cpu_get_model_name(); -#endif - if (platform != nullptr) { - service.txt_records.push_back({"platform", platform}); - } - service.txt_records.push_back({"board", ESPHOME_BOARD}); + const std::string &friendly_name = App.get_friendly_name(); + bool friendly_name_empty = friendly_name.empty(); + + // Calculate exact capacity for txt_records + size_t txt_count = 3; // version, mac, board (always present) + if (!friendly_name_empty) { + txt_count++; // friendly_name + } +#if defined(USE_ESP8266) || defined(USE_ESP32) || defined(USE_RP2040) || defined(USE_LIBRETINY) + txt_count++; // platform +#endif +#if defined(USE_WIFI) || defined(USE_ETHERNET) || defined(USE_OPENTHREAD) + txt_count++; // network +#endif +#ifdef USE_API_NOISE + txt_count++; // api_encryption or api_encryption_supported +#endif +#ifdef ESPHOME_PROJECT_NAME + txt_count += 2; // project_name and project_version +#endif +#ifdef USE_DASHBOARD_IMPORT + txt_count++; // package_import_url +#endif + + auto &txt_records = service.txt_records; + txt_records.reserve(txt_count); + + if (!friendly_name_empty) { + txt_records.emplace_back(MDNSTXTRecord{"friendly_name", friendly_name}); + } + txt_records.emplace_back(MDNSTXTRecord{"version", ESPHOME_VERSION}); + txt_records.emplace_back(MDNSTXTRecord{"mac", get_mac_address()}); + +#ifdef USE_ESP8266 + txt_records.emplace_back(MDNSTXTRecord{"platform", "ESP8266"}); +#elif defined(USE_ESP32) + txt_records.emplace_back(MDNSTXTRecord{"platform", "ESP32"}); +#elif defined(USE_RP2040) + txt_records.emplace_back(MDNSTXTRecord{"platform", "RP2040"}); +#elif defined(USE_LIBRETINY) + txt_records.emplace_back(MDNSTXTRecord{"platform", lt_cpu_get_model_name()}); +#endif + + txt_records.emplace_back(MDNSTXTRecord{"board", ESPHOME_BOARD}); #if defined(USE_WIFI) - service.txt_records.push_back({"network", "wifi"}); + txt_records.emplace_back(MDNSTXTRecord{"network", "wifi"}); #elif defined(USE_ETHERNET) - service.txt_records.push_back({"network", "ethernet"}); + txt_records.emplace_back(MDNSTXTRecord{"network", "ethernet"}); #elif defined(USE_OPENTHREAD) - service.txt_records.push_back({"network", "thread"}); + txt_records.emplace_back(MDNSTXTRecord{"network", "thread"}); #endif #ifdef USE_API_NOISE + static constexpr const char *NOISE_ENCRYPTION = "Noise_NNpsk0_25519_ChaChaPoly_SHA256"; if (api::global_api_server->get_noise_ctx()->has_psk()) { - service.txt_records.push_back({"api_encryption", "Noise_NNpsk0_25519_ChaChaPoly_SHA256"}); + txt_records.emplace_back(MDNSTXTRecord{"api_encryption", NOISE_ENCRYPTION}); } else { - service.txt_records.push_back({"api_encryption_supported", "Noise_NNpsk0_25519_ChaChaPoly_SHA256"}); + txt_records.emplace_back(MDNSTXTRecord{"api_encryption_supported", NOISE_ENCRYPTION}); } #endif #ifdef ESPHOME_PROJECT_NAME - service.txt_records.push_back({"project_name", ESPHOME_PROJECT_NAME}); - service.txt_records.push_back({"project_version", ESPHOME_PROJECT_VERSION}); + txt_records.emplace_back(MDNSTXTRecord{"project_name", ESPHOME_PROJECT_NAME}); + txt_records.emplace_back(MDNSTXTRecord{"project_version", ESPHOME_PROJECT_VERSION}); #endif // ESPHOME_PROJECT_NAME #ifdef USE_DASHBOARD_IMPORT - service.txt_records.push_back({"package_import_url", dashboard_import::get_package_import_url()}); + txt_records.emplace_back(MDNSTXTRecord{"package_import_url", dashboard_import::get_package_import_url()}); #endif - - this->services_.push_back(service); } #endif // USE_API #ifdef USE_PROMETHEUS - { - MDNSService service{}; - service.service_type = "_prometheus-http"; - service.proto = "_tcp"; - service.port = USE_WEBSERVER_PORT; - this->services_.push_back(service); - } + this->services_.emplace_back(); + auto &prom_service = this->services_.back(); + prom_service.service_type = "_prometheus-http"; + prom_service.proto = "_tcp"; + prom_service.port = USE_WEBSERVER_PORT; #endif #ifdef USE_WEBSERVER - { - MDNSService service{}; - service.service_type = "_http"; - service.proto = "_tcp"; - service.port = USE_WEBSERVER_PORT; - this->services_.push_back(service); - } + this->services_.emplace_back(); + auto &web_service = this->services_.back(); + web_service.service_type = "_http"; + web_service.proto = "_tcp"; + web_service.port = USE_WEBSERVER_PORT; #endif #ifdef USE_MDNS_EXTRA_SERVICES this->services_.insert(this->services_.end(), this->services_extra_.begin(), this->services_extra_.end()); #endif - if (this->services_.empty()) { - // Publish "http" service if not using native API - // This is just to have *some* mDNS service so that .local resolution works - MDNSService service{}; - service.service_type = "_http"; - service.proto = "_tcp"; - service.port = USE_WEBSERVER_PORT; - service.txt_records.push_back({"version", ESPHOME_VERSION}); - this->services_.push_back(service); - } +#if !defined(USE_API) && !defined(USE_PROMETHEUS) && !defined(USE_WEBSERVER) && !defined(USE_MDNS_EXTRA_SERVICES) + // Publish "http" service if not using native API or any other services + // This is just to have *some* mDNS service so that .local resolution works + this->services_.emplace_back(); + auto &fallback_service = this->services_.back(); + fallback_service.service_type = "_http"; + fallback_service.proto = "_tcp"; + fallback_service.port = USE_WEBSERVER_PORT; + fallback_service.txt_records.emplace_back(MDNSTXTRecord{"version", ESPHOME_VERSION}); +#endif } void MDNSComponent::dump_config() { @@ -125,6 +164,7 @@ void MDNSComponent::dump_config() { "mDNS:\n" " Hostname: %s", this->hostname_.c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE ESP_LOGV(TAG, " Services:"); for (const auto &service : this->services_) { ESP_LOGV(TAG, " - %s, %s, %d", service.service_type.c_str(), service.proto.c_str(), @@ -134,6 +174,7 @@ void MDNSComponent::dump_config() { const_cast &>(record.value).value().c_str()); } } +#endif } std::vector MDNSComponent::get_services() { return this->services_; } From a45137434b78b5187dd8b954d160913c46177832 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 20 Aug 2025 14:34:45 +1200 Subject: [PATCH 041/208] [quality] Convert remaining ``to_code`` to ``async`` (#10271) --- esphome/components/airthings_ble/__init__.py | 4 ++-- esphome/components/ble_client/output/__init__.py | 8 ++++---- esphome/components/cover/__init__.py | 6 +++--- esphome/components/pipsolar/__init__.py | 6 +++--- esphome/components/pipsolar/output/__init__.py | 8 ++++---- esphome/components/radon_eye_ble/__init__.py | 4 ++-- esphome/components/remote_base/__init__.py | 14 ++++++-------- esphome/components/shelly_dimmer/light.py | 14 +++++++------- esphome/components/valve/__init__.py | 6 +++--- 9 files changed, 34 insertions(+), 36 deletions(-) diff --git a/esphome/components/airthings_ble/__init__.py b/esphome/components/airthings_ble/__init__.py index eae400ab39..1545110798 100644 --- a/esphome/components/airthings_ble/__init__.py +++ b/esphome/components/airthings_ble/__init__.py @@ -18,6 +18,6 @@ CONFIG_SCHEMA = cv.Schema( ).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield esp32_ble_tracker.register_ble_device(var, config) + await esp32_ble_tracker.register_ble_device(var, config) diff --git a/esphome/components/ble_client/output/__init__.py b/esphome/components/ble_client/output/__init__.py index 729885eb8b..22a6b29442 100644 --- a/esphome/components/ble_client/output/__init__.py +++ b/esphome/components/ble_client/output/__init__.py @@ -27,7 +27,7 @@ CONFIG_SCHEMA = cv.All( ) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) if len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid16_format): cg.add( @@ -63,6 +63,6 @@ def to_code(config): ) cg.add(var.set_char_uuid128(uuid128)) cg.add(var.set_require_response(config[CONF_REQUIRE_RESPONSE])) - yield output.register_output(var, config) - yield ble_client.register_ble_node(var, config) - yield cg.register_component(var, config) + await output.register_output(var, config) + await ble_client.register_ble_node(var, config) + await cg.register_component(var, config) diff --git a/esphome/components/cover/__init__.py b/esphome/components/cover/__init__.py index 0e01eb336f..383cfaf8fb 100644 --- a/esphome/components/cover/__init__.py +++ b/esphome/components/cover/__init__.py @@ -228,9 +228,9 @@ async def cover_stop_to_code(config, action_id, template_arg, args): @automation.register_action("cover.toggle", ToggleAction, COVER_ACTION_SCHEMA) -def cover_toggle_to_code(config, action_id, template_arg, args): - paren = yield cg.get_variable(config[CONF_ID]) - yield cg.new_Pvariable(action_id, template_arg, paren) +async def cover_toggle_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) COVER_CONTROL_ACTION_SCHEMA = cv.Schema( diff --git a/esphome/components/pipsolar/__init__.py b/esphome/components/pipsolar/__init__.py index 1e4ea8492b..e3966aa2cc 100644 --- a/esphome/components/pipsolar/__init__.py +++ b/esphome/components/pipsolar/__init__.py @@ -26,7 +26,7 @@ CONFIG_SCHEMA = cv.All( ) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield uart.register_uart_device(var, config) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) diff --git a/esphome/components/pipsolar/output/__init__.py b/esphome/components/pipsolar/output/__init__.py index 1eb7249119..829f8f7037 100644 --- a/esphome/components/pipsolar/output/__init__.py +++ b/esphome/components/pipsolar/output/__init__.py @@ -99,9 +99,9 @@ async def to_code(config): } ), ) -def output_pipsolar_set_level_to_code(config, action_id, template_arg, args): - paren = yield cg.get_variable(config[CONF_ID]) +async def output_pipsolar_set_level_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - template_ = yield cg.templatable(config[CONF_VALUE], args, float) + template_ = await cg.templatable(config[CONF_VALUE], args, float) cg.add(var.set_level(template_)) - yield var + return var diff --git a/esphome/components/radon_eye_ble/__init__.py b/esphome/components/radon_eye_ble/__init__.py index 01910c81a8..99daef30e5 100644 --- a/esphome/components/radon_eye_ble/__init__.py +++ b/esphome/components/radon_eye_ble/__init__.py @@ -18,6 +18,6 @@ CONFIG_SCHEMA = cv.Schema( ).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) -def to_code(config): +async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield esp32_ble_tracker.register_ble_device(var, config) + await esp32_ble_tracker.register_ble_device(var, config) diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index 8163661c65..42ebae77f7 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -1782,14 +1782,12 @@ def nexa_dumper(var, config): @register_action("nexa", NexaAction, NEXA_SCHEMA) -def nexa_action(var, config, args): - cg.add(var.set_device((yield cg.templatable(config[CONF_DEVICE], args, cg.uint32)))) - cg.add(var.set_group((yield cg.templatable(config[CONF_GROUP], args, cg.uint8)))) - cg.add(var.set_state((yield cg.templatable(config[CONF_STATE], args, cg.uint8)))) - cg.add( - var.set_channel((yield cg.templatable(config[CONF_CHANNEL], args, cg.uint8))) - ) - cg.add(var.set_level((yield cg.templatable(config[CONF_LEVEL], args, cg.uint8)))) +async def nexa_action(var, config, args): + cg.add(var.set_device(await cg.templatable(config[CONF_DEVICE], args, cg.uint32))) + cg.add(var.set_group(await cg.templatable(config[CONF_GROUP], args, cg.uint8))) + cg.add(var.set_state(await cg.templatable(config[CONF_STATE], args, cg.uint8))) + cg.add(var.set_channel(await cg.templatable(config[CONF_CHANNEL], args, cg.uint8))) + cg.add(var.set_level(await cg.templatable(config[CONF_LEVEL], args, cg.uint8))) # Midea diff --git a/esphome/components/shelly_dimmer/light.py b/esphome/components/shelly_dimmer/light.py index bb2c3ceee8..c96bc380d7 100644 --- a/esphome/components/shelly_dimmer/light.py +++ b/esphome/components/shelly_dimmer/light.py @@ -183,7 +183,7 @@ CONFIG_SCHEMA = ( ) -def to_code(config): +async def to_code(config): fw_hex = get_firmware(config[CONF_FIRMWARE]) fw_major, fw_minor = parse_firmware_version(config[CONF_FIRMWARE][CONF_VERSION]) @@ -193,17 +193,17 @@ def to_code(config): cg.add_define("USE_SHD_FIRMWARE_MINOR_VERSION", fw_minor) var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) - yield cg.register_component(var, config) + await cg.register_component(var, config) config.pop( CONF_UPDATE_INTERVAL ) # drop UPDATE_INTERVAL as it does not apply to the light component - yield light.register_light(var, config) - yield uart.register_uart_device(var, config) + await light.register_light(var, config) + await uart.register_uart_device(var, config) - nrst_pin = yield cg.gpio_pin_expression(config[CONF_NRST_PIN]) + nrst_pin = await cg.gpio_pin_expression(config[CONF_NRST_PIN]) cg.add(var.set_nrst_pin(nrst_pin)) - boot0_pin = yield cg.gpio_pin_expression(config[CONF_BOOT0_PIN]) + boot0_pin = await cg.gpio_pin_expression(config[CONF_BOOT0_PIN]) cg.add(var.set_boot0_pin(boot0_pin)) cg.add(var.set_leading_edge(config[CONF_LEADING_EDGE])) @@ -217,5 +217,5 @@ def to_code(config): continue conf = config[key] - sens = yield sensor.new_sensor(conf) + sens = await sensor.new_sensor(conf) cg.add(getattr(var, f"set_{key}_sensor")(sens)) diff --git a/esphome/components/valve/__init__.py b/esphome/components/valve/__init__.py index 53254068af..8185bd6ea2 100644 --- a/esphome/components/valve/__init__.py +++ b/esphome/components/valve/__init__.py @@ -202,9 +202,9 @@ async def valve_stop_to_code(config, action_id, template_arg, args): @automation.register_action("valve.toggle", ToggleAction, VALVE_ACTION_SCHEMA) -def valve_toggle_to_code(config, action_id, template_arg, args): - paren = yield cg.get_variable(config[CONF_ID]) - yield cg.new_Pvariable(action_id, template_arg, paren) +async def valve_toggle_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) VALVE_CONTROL_ACTION_SCHEMA = cv.Schema( From 3964f9794b613c8943f2927eaa6c3d661b92c2a2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 19 Aug 2025 21:35:09 -0500 Subject: [PATCH 042/208] [binary_sensor] Convert LOG_BINARY_SENSOR macro to function to reduce flash usage (#10294) --- esphome/components/binary_sensor/binary_sensor.cpp | 13 +++++++++++++ esphome/components/binary_sensor/binary_sensor.h | 11 ++++------- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/esphome/components/binary_sensor/binary_sensor.cpp b/esphome/components/binary_sensor/binary_sensor.cpp index 02b83af552..e652d302b6 100644 --- a/esphome/components/binary_sensor/binary_sensor.cpp +++ b/esphome/components/binary_sensor/binary_sensor.cpp @@ -7,6 +7,19 @@ namespace binary_sensor { static const char *const TAG = "binary_sensor"; +// Function implementation of LOG_BINARY_SENSOR macro to reduce code size +void log_binary_sensor(const char *tag, const char *prefix, const char *type, BinarySensor *obj) { + if (obj == nullptr) { + return; + } + + ESP_LOGCONFIG(tag, "%s%s '%s'", prefix, type, obj->get_name().c_str()); + + if (!obj->get_device_class().empty()) { + ESP_LOGCONFIG(tag, "%s Device Class: '%s'", prefix, obj->get_device_class().c_str()); + } +} + void BinarySensor::publish_state(bool new_state) { if (this->filter_list_ == nullptr) { this->send_state_internal(new_state); diff --git a/esphome/components/binary_sensor/binary_sensor.h b/esphome/components/binary_sensor/binary_sensor.h index d61be7a49b..2bd17d97c9 100644 --- a/esphome/components/binary_sensor/binary_sensor.h +++ b/esphome/components/binary_sensor/binary_sensor.h @@ -10,13 +10,10 @@ namespace esphome { namespace binary_sensor { -#define LOG_BINARY_SENSOR(prefix, type, obj) \ - if ((obj) != nullptr) { \ - ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ - if (!(obj)->get_device_class().empty()) { \ - ESP_LOGCONFIG(TAG, "%s Device Class: '%s'", prefix, (obj)->get_device_class().c_str()); \ - } \ - } +class BinarySensor; +void log_binary_sensor(const char *tag, const char *prefix, const char *type, BinarySensor *obj); + +#define LOG_BINARY_SENSOR(prefix, type, obj) log_binary_sensor(TAG, prefix, LOG_STR_LITERAL(type), obj) #define SUB_BINARY_SENSOR(name) \ protected: \ From 0eab908b0efefac0e9ab4f2695343c8bde8be51e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 19 Aug 2025 21:35:45 -0500 Subject: [PATCH 043/208] [sensor] Convert LOG_SENSOR macro to function to reduce flash usage (#10290) --- esphome/components/ccs811/ccs811.cpp | 4 +-- .../grove_gas_mc_v2/grove_gas_mc_v2.cpp | 8 +++--- esphome/components/hlw8012/hlw8012.cpp | 8 +++--- esphome/components/ntc/ntc.cpp | 2 +- .../components/pulse_width/pulse_width.cpp | 2 +- esphome/components/sensor/sensor.cpp | 27 +++++++++++++++++++ esphome/components/sensor/sensor.h | 23 +++------------- esphome/components/ufire_ec/ufire_ec.cpp | 6 ++--- esphome/components/ufire_ise/ufire_ise.cpp | 6 ++--- 9 files changed, 48 insertions(+), 38 deletions(-) diff --git a/esphome/components/ccs811/ccs811.cpp b/esphome/components/ccs811/ccs811.cpp index cecb92b3df..2617d7577a 100644 --- a/esphome/components/ccs811/ccs811.cpp +++ b/esphome/components/ccs811/ccs811.cpp @@ -153,8 +153,8 @@ void CCS811Component::dump_config() { ESP_LOGCONFIG(TAG, "CCS811"); LOG_I2C_DEVICE(this) LOG_UPDATE_INTERVAL(this) - LOG_SENSOR(" ", "CO2 Sensor", this->co2_) - LOG_SENSOR(" ", "TVOC Sensor", this->tvoc_) + LOG_SENSOR(" ", "CO2 Sensor", this->co2_); + LOG_SENSOR(" ", "TVOC Sensor", this->tvoc_); LOG_TEXT_SENSOR(" ", "Firmware Version Sensor", this->version_) if (this->baseline_) { ESP_LOGCONFIG(TAG, " Baseline: %04X", *this->baseline_); diff --git a/esphome/components/grove_gas_mc_v2/grove_gas_mc_v2.cpp b/esphome/components/grove_gas_mc_v2/grove_gas_mc_v2.cpp index 4842ee5d06..52ec8433a2 100644 --- a/esphome/components/grove_gas_mc_v2/grove_gas_mc_v2.cpp +++ b/esphome/components/grove_gas_mc_v2/grove_gas_mc_v2.cpp @@ -58,10 +58,10 @@ void GroveGasMultichannelV2Component::dump_config() { ESP_LOGCONFIG(TAG, "Grove Multichannel Gas Sensor V2"); LOG_I2C_DEVICE(this) LOG_UPDATE_INTERVAL(this) - LOG_SENSOR(" ", "Nitrogen Dioxide", this->nitrogen_dioxide_sensor_) - LOG_SENSOR(" ", "Ethanol", this->ethanol_sensor_) - LOG_SENSOR(" ", "Carbon Monoxide", this->carbon_monoxide_sensor_) - LOG_SENSOR(" ", "TVOC", this->tvoc_sensor_) + LOG_SENSOR(" ", "Nitrogen Dioxide", this->nitrogen_dioxide_sensor_); + LOG_SENSOR(" ", "Ethanol", this->ethanol_sensor_); + LOG_SENSOR(" ", "Carbon Monoxide", this->carbon_monoxide_sensor_); + LOG_SENSOR(" ", "TVOC", this->tvoc_sensor_); if (this->is_failed()) { switch (this->error_code_) { diff --git a/esphome/components/hlw8012/hlw8012.cpp b/esphome/components/hlw8012/hlw8012.cpp index a28678e630..f293185cce 100644 --- a/esphome/components/hlw8012/hlw8012.cpp +++ b/esphome/components/hlw8012/hlw8012.cpp @@ -43,10 +43,10 @@ void HLW8012Component::dump_config() { " Voltage Divider: %.1f", this->change_mode_every_, this->current_resistor_ * 1000.0f, this->voltage_divider_); LOG_UPDATE_INTERVAL(this) - LOG_SENSOR(" ", "Voltage", this->voltage_sensor_) - LOG_SENSOR(" ", "Current", this->current_sensor_) - LOG_SENSOR(" ", "Power", this->power_sensor_) - LOG_SENSOR(" ", "Energy", this->energy_sensor_) + LOG_SENSOR(" ", "Voltage", this->voltage_sensor_); + LOG_SENSOR(" ", "Current", this->current_sensor_); + LOG_SENSOR(" ", "Power", this->power_sensor_); + LOG_SENSOR(" ", "Energy", this->energy_sensor_); } float HLW8012Component::get_setup_priority() const { return setup_priority::DATA; } void HLW8012Component::update() { diff --git a/esphome/components/ntc/ntc.cpp b/esphome/components/ntc/ntc.cpp index 333dbc5a75..b08f84029b 100644 --- a/esphome/components/ntc/ntc.cpp +++ b/esphome/components/ntc/ntc.cpp @@ -11,7 +11,7 @@ void NTC::setup() { if (this->sensor_->has_state()) this->process_(this->sensor_->state); } -void NTC::dump_config() { LOG_SENSOR("", "NTC Sensor", this) } +void NTC::dump_config() { LOG_SENSOR("", "NTC Sensor", this); } float NTC::get_setup_priority() const { return setup_priority::DATA; } void NTC::process_(float value) { if (std::isnan(value)) { diff --git a/esphome/components/pulse_width/pulse_width.cpp b/esphome/components/pulse_width/pulse_width.cpp index 8d66861049..c086ceaa23 100644 --- a/esphome/components/pulse_width/pulse_width.cpp +++ b/esphome/components/pulse_width/pulse_width.cpp @@ -17,7 +17,7 @@ void IRAM_ATTR PulseWidthSensorStore::gpio_intr(PulseWidthSensorStore *arg) { } void PulseWidthSensor::dump_config() { - LOG_SENSOR("", "Pulse Width", this) + LOG_SENSOR("", "Pulse Width", this); LOG_UPDATE_INTERVAL(this) LOG_PIN(" Pin: ", this->pin_); } diff --git a/esphome/components/sensor/sensor.cpp b/esphome/components/sensor/sensor.cpp index 0a82677bc9..6df6347c18 100644 --- a/esphome/components/sensor/sensor.cpp +++ b/esphome/components/sensor/sensor.cpp @@ -6,6 +6,33 @@ namespace sensor { static const char *const TAG = "sensor"; +// Function implementation of LOG_SENSOR macro to reduce code size +void log_sensor(const char *tag, const char *prefix, const char *type, Sensor *obj) { + if (obj == nullptr) { + return; + } + + ESP_LOGCONFIG(tag, + "%s%s '%s'\n" + "%s State Class: '%s'\n" + "%s Unit of Measurement: '%s'\n" + "%s Accuracy Decimals: %d", + prefix, type, obj->get_name().c_str(), prefix, state_class_to_string(obj->get_state_class()).c_str(), + prefix, obj->get_unit_of_measurement().c_str(), prefix, obj->get_accuracy_decimals()); + + if (!obj->get_device_class().empty()) { + ESP_LOGCONFIG(tag, "%s Device Class: '%s'", prefix, obj->get_device_class().c_str()); + } + + if (!obj->get_icon().empty()) { + ESP_LOGCONFIG(tag, "%s Icon: '%s'", prefix, obj->get_icon().c_str()); + } + + if (obj->get_force_update()) { + ESP_LOGV(tag, "%s Force Update: YES", prefix); + } +} + std::string state_class_to_string(StateClass state_class) { switch (state_class) { case STATE_CLASS_MEASUREMENT: diff --git a/esphome/components/sensor/sensor.h b/esphome/components/sensor/sensor.h index c2ded0f2c3..b3206d8dab 100644 --- a/esphome/components/sensor/sensor.h +++ b/esphome/components/sensor/sensor.h @@ -12,26 +12,9 @@ namespace esphome { namespace sensor { -#define LOG_SENSOR(prefix, type, obj) \ - if ((obj) != nullptr) { \ - ESP_LOGCONFIG(TAG, \ - "%s%s '%s'\n" \ - "%s State Class: '%s'\n" \ - "%s Unit of Measurement: '%s'\n" \ - "%s Accuracy Decimals: %d", \ - prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str(), prefix, \ - state_class_to_string((obj)->get_state_class()).c_str(), prefix, \ - (obj)->get_unit_of_measurement().c_str(), prefix, (obj)->get_accuracy_decimals()); \ - if (!(obj)->get_device_class().empty()) { \ - ESP_LOGCONFIG(TAG, "%s Device Class: '%s'", prefix, (obj)->get_device_class().c_str()); \ - } \ - if (!(obj)->get_icon().empty()) { \ - ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon().c_str()); \ - } \ - if ((obj)->get_force_update()) { \ - ESP_LOGV(TAG, "%s Force Update: YES", prefix); \ - } \ - } +void log_sensor(const char *tag, const char *prefix, const char *type, Sensor *obj); + +#define LOG_SENSOR(prefix, type, obj) log_sensor(TAG, prefix, LOG_STR_LITERAL(type), obj) #define SUB_SENSOR(name) \ protected: \ diff --git a/esphome/components/ufire_ec/ufire_ec.cpp b/esphome/components/ufire_ec/ufire_ec.cpp index 364a133776..9e0055a2cc 100644 --- a/esphome/components/ufire_ec/ufire_ec.cpp +++ b/esphome/components/ufire_ec/ufire_ec.cpp @@ -105,9 +105,9 @@ void UFireECComponent::dump_config() { ESP_LOGCONFIG(TAG, "uFire-EC"); LOG_I2C_DEVICE(this) LOG_UPDATE_INTERVAL(this) - LOG_SENSOR(" ", "EC Sensor", this->ec_sensor_) - LOG_SENSOR(" ", "Temperature Sensor", this->temperature_sensor_) - LOG_SENSOR(" ", "Temperature Sensor external", this->temperature_sensor_external_) + LOG_SENSOR(" ", "EC Sensor", this->ec_sensor_); + LOG_SENSOR(" ", "Temperature Sensor", this->temperature_sensor_); + LOG_SENSOR(" ", "Temperature Sensor external", this->temperature_sensor_external_); ESP_LOGCONFIG(TAG, " Temperature Compensation: %f\n" " Temperature Coefficient: %f", diff --git a/esphome/components/ufire_ise/ufire_ise.cpp b/esphome/components/ufire_ise/ufire_ise.cpp index 503d993fb7..9e0e7e265d 100644 --- a/esphome/components/ufire_ise/ufire_ise.cpp +++ b/esphome/components/ufire_ise/ufire_ise.cpp @@ -142,9 +142,9 @@ void UFireISEComponent::dump_config() { ESP_LOGCONFIG(TAG, "uFire-ISE"); LOG_I2C_DEVICE(this) LOG_UPDATE_INTERVAL(this) - LOG_SENSOR(" ", "PH Sensor", this->ph_sensor_) - LOG_SENSOR(" ", "Temperature Sensor", this->temperature_sensor_) - LOG_SENSOR(" ", "Temperature Sensor external", this->temperature_sensor_external_) + LOG_SENSOR(" ", "PH Sensor", this->ph_sensor_); + LOG_SENSOR(" ", "Temperature Sensor", this->temperature_sensor_); + LOG_SENSOR(" ", "Temperature Sensor external", this->temperature_sensor_external_); } } // namespace ufire_ise From 4ccc6aee095f5f0bff362643faf03b5e8c4bf359 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 19 Aug 2025 21:35:53 -0500 Subject: [PATCH 044/208] [button] Convert LOG_BUTTON macro to function to reduce flash usage (#10295) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/button/button.cpp | 13 +++++++++++++ esphome/components/button/button.h | 11 ++++------- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/esphome/components/button/button.cpp b/esphome/components/button/button.cpp index 4c4cb7740c..63d71dcb8a 100644 --- a/esphome/components/button/button.cpp +++ b/esphome/components/button/button.cpp @@ -6,6 +6,19 @@ namespace button { static const char *const TAG = "button"; +// Function implementation of LOG_BUTTON macro to reduce code size +void log_button(const char *tag, const char *prefix, const char *type, Button *obj) { + if (obj == nullptr) { + return; + } + + ESP_LOGCONFIG(tag, "%s%s '%s'", prefix, type, obj->get_name().c_str()); + + if (!obj->get_icon().empty()) { + ESP_LOGCONFIG(tag, "%s Icon: '%s'", prefix, obj->get_icon().c_str()); + } +} + void Button::press() { ESP_LOGD(TAG, "'%s' Pressed.", this->get_name().c_str()); this->press_action(); diff --git a/esphome/components/button/button.h b/esphome/components/button/button.h index 9488eca221..75b76f9dcf 100644 --- a/esphome/components/button/button.h +++ b/esphome/components/button/button.h @@ -7,13 +7,10 @@ namespace esphome { namespace button { -#define LOG_BUTTON(prefix, type, obj) \ - if ((obj) != nullptr) { \ - ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ - if (!(obj)->get_icon().empty()) { \ - ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon().c_str()); \ - } \ - } +class Button; +void log_button(const char *tag, const char *prefix, const char *type, Button *obj); + +#define LOG_BUTTON(prefix, type, obj) log_button(TAG, prefix, LOG_STR_LITERAL(type), obj) #define SUB_BUTTON(name) \ protected: \ From e2a9b85924c3107afb09d61d33a809ae746ecc0d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 19 Aug 2025 21:36:05 -0500 Subject: [PATCH 045/208] [number] Convert LOG_NUMBER macro to function to reduce flash usage (#10293) --- esphome/components/number/number.cpp | 21 +++++++++++++++++++++ esphome/components/number/number.h | 17 ++++------------- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/esphome/components/number/number.cpp b/esphome/components/number/number.cpp index b6a845b19b..4769c1ed12 100644 --- a/esphome/components/number/number.cpp +++ b/esphome/components/number/number.cpp @@ -6,6 +6,27 @@ namespace number { static const char *const TAG = "number"; +// Function implementation of LOG_NUMBER macro to reduce code size +void log_number(const char *tag, const char *prefix, const char *type, Number *obj) { + if (obj == nullptr) { + return; + } + + ESP_LOGCONFIG(tag, "%s%s '%s'", prefix, type, obj->get_name().c_str()); + + if (!obj->get_icon().empty()) { + ESP_LOGCONFIG(tag, "%s Icon: '%s'", prefix, obj->get_icon().c_str()); + } + + if (!obj->traits.get_unit_of_measurement().empty()) { + ESP_LOGCONFIG(tag, "%s Unit of Measurement: '%s'", prefix, obj->traits.get_unit_of_measurement().c_str()); + } + + if (!obj->traits.get_device_class().empty()) { + ESP_LOGCONFIG(tag, "%s Device Class: '%s'", prefix, obj->traits.get_device_class().c_str()); + } +} + void Number::publish_state(float state) { this->set_has_state(true); this->state = state; diff --git a/esphome/components/number/number.h b/esphome/components/number/number.h index 49bcbb857c..da91d70d53 100644 --- a/esphome/components/number/number.h +++ b/esphome/components/number/number.h @@ -9,19 +9,10 @@ namespace esphome { namespace number { -#define LOG_NUMBER(prefix, type, obj) \ - if ((obj) != nullptr) { \ - ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ - if (!(obj)->get_icon().empty()) { \ - ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon().c_str()); \ - } \ - if (!(obj)->traits.get_unit_of_measurement().empty()) { \ - ESP_LOGCONFIG(TAG, "%s Unit of Measurement: '%s'", prefix, (obj)->traits.get_unit_of_measurement().c_str()); \ - } \ - if (!(obj)->traits.get_device_class().empty()) { \ - ESP_LOGCONFIG(TAG, "%s Device Class: '%s'", prefix, (obj)->traits.get_device_class().c_str()); \ - } \ - } +class Number; +void log_number(const char *tag, const char *prefix, const char *type, Number *obj); + +#define LOG_NUMBER(prefix, type, obj) log_number(TAG, prefix, LOG_STR_LITERAL(type), obj) #define SUB_NUMBER(name) \ protected: \ From 634f687c3e448a6ff05eacd17897016c3e1a651c Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Wed, 20 Aug 2025 04:38:13 +0200 Subject: [PATCH 046/208] [light] Add support for querying effects by index (#10195) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- .../light/addressable_light_effect.h | 7 ++++ esphome/components/light/base_light_effects.h | 8 ++++ esphome/components/light/light_effect.cpp | 36 ++++++++++++++++++ esphome/components/light/light_effect.h | 14 +++++++ .../components/light/light_json_schema.cpp | 10 ++++- esphome/components/light/light_state.h | 38 +++++++++++++++++++ 6 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 esphome/components/light/light_effect.cpp diff --git a/esphome/components/light/addressable_light_effect.h b/esphome/components/light/addressable_light_effect.h index d622ec0375..fcf76b3cb0 100644 --- a/esphome/components/light/addressable_light_effect.h +++ b/esphome/components/light/addressable_light_effect.h @@ -44,6 +44,13 @@ class AddressableLightEffect : public LightEffect { this->apply(*this->get_addressable_(), current_color); } + /// Get effect index specifically for addressable effects. + /// Can be used by effects to modify behavior based on their position in the list. + uint32_t get_effect_index() const { return this->get_index(); } + + /// Check if this is the currently running addressable effect. + bool is_current_effect() const { return this->is_active() && this->get_addressable_()->is_effect_active(); } + protected: AddressableLight *get_addressable_() const { return (AddressableLight *) this->state_->get_output(); } }; diff --git a/esphome/components/light/base_light_effects.h b/esphome/components/light/base_light_effects.h index 9e02e889c9..ff6cd1ccfe 100644 --- a/esphome/components/light/base_light_effects.h +++ b/esphome/components/light/base_light_effects.h @@ -125,6 +125,10 @@ class LambdaLightEffect : public LightEffect { } } + /// Get the current effect index for use in lambda functions. + /// This can be useful for lambda effects that need to know their own index. + uint32_t get_current_index() const { return this->get_index(); } + protected: std::function f_; uint32_t update_interval_; @@ -143,6 +147,10 @@ class AutomationLightEffect : public LightEffect { } Trigger<> *get_trig() const { return trig_; } + /// Get the current effect index for use in automations. + /// Useful for automations that need to know which effect is running. + uint32_t get_current_index() const { return this->get_index(); } + protected: Trigger<> *trig_{new Trigger<>}; }; diff --git a/esphome/components/light/light_effect.cpp b/esphome/components/light/light_effect.cpp new file mode 100644 index 0000000000..a210b48e5b --- /dev/null +++ b/esphome/components/light/light_effect.cpp @@ -0,0 +1,36 @@ +#include "light_effect.h" +#include "light_state.h" + +namespace esphome { +namespace light { + +uint32_t LightEffect::get_index() const { + if (this->state_ == nullptr) { + return 0; + } + return this->get_index_in_parent_(); +} + +bool LightEffect::is_active() const { + if (this->state_ == nullptr) { + return false; + } + return this->get_index() != 0 && this->state_->get_current_effect_index() == this->get_index(); +} + +uint32_t LightEffect::get_index_in_parent_() const { + if (this->state_ == nullptr) { + return 0; + } + + const auto &effects = this->state_->get_effects(); + for (size_t i = 0; i < effects.size(); i++) { + if (effects[i] == this) { + return i + 1; // Effects are 1-indexed in the API + } + } + return 0; // Not found +} + +} // namespace light +} // namespace esphome diff --git a/esphome/components/light/light_effect.h b/esphome/components/light/light_effect.h index 8da51fe8b3..dbaf1faf24 100644 --- a/esphome/components/light/light_effect.h +++ b/esphome/components/light/light_effect.h @@ -34,9 +34,23 @@ class LightEffect { this->init(); } + /// Get the index of this effect in the parent light's effect list. + /// Returns 0 if not found or not initialized. + uint32_t get_index() const; + + /// Check if this effect is currently active. + bool is_active() const; + + /// Get a reference to the parent light state. + /// Returns nullptr if not initialized. + LightState *get_light_state() const { return this->state_; } + protected: LightState *state_{nullptr}; std::string name_; + + /// Internal method to find this effect's index in the parent light's effect list. + uint32_t get_index_in_parent_() const; }; } // namespace light diff --git a/esphome/components/light/light_json_schema.cpp b/esphome/components/light/light_json_schema.cpp index 896b821705..010e130612 100644 --- a/esphome/components/light/light_json_schema.cpp +++ b/esphome/components/light/light_json_schema.cpp @@ -36,8 +36,11 @@ static constexpr const char *get_color_mode_json_str(ColorMode mode) { void LightJSONSchema::dump_json(LightState &state, JsonObject root) { // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - if (state.supports_effects()) + if (state.supports_effects()) { root["effect"] = state.get_effect_name(); + root["effect_index"] = state.get_current_effect_index(); + root["effect_count"] = state.get_effect_count(); + } auto values = state.remote_values; auto traits = state.get_output()->get_traits(); @@ -160,6 +163,11 @@ void LightJSONSchema::parse_json(LightState &state, LightCall &call, JsonObject const char *effect = root["effect"]; call.set_effect(effect); } + + if (root["effect_index"].is()) { + uint32_t effect_index = root["effect_index"]; + call.set_effect(effect_index); + } } } // namespace light diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h index 94b81dee61..48323dd3c3 100644 --- a/esphome/components/light/light_state.h +++ b/esphome/components/light/light_state.h @@ -163,6 +163,44 @@ class LightState : public EntityBase, public Component { /// Add effects for this light state. void add_effects(const std::vector &effects); + /// Get the total number of effects available for this light. + size_t get_effect_count() const { return this->effects_.size(); } + + /// Get the currently active effect index (0 = no effect, 1+ = effect index). + uint32_t get_current_effect_index() const { return this->active_effect_index_; } + + /// Get effect index by name. Returns 0 if effect not found. + uint32_t get_effect_index(const std::string &effect_name) const { + if (strcasecmp(effect_name.c_str(), "none") == 0) { + return 0; + } + for (size_t i = 0; i < this->effects_.size(); i++) { + if (strcasecmp(effect_name.c_str(), this->effects_[i]->get_name().c_str()) == 0) { + return i + 1; // Effects are 1-indexed in active_effect_index_ + } + } + return 0; // Effect not found + } + + /// Get effect by index. Returns nullptr if index is invalid. + LightEffect *get_effect_by_index(uint32_t index) const { + if (index == 0 || index > this->effects_.size()) { + return nullptr; + } + return this->effects_[index - 1]; // Effects are 1-indexed in active_effect_index_ + } + + /// Get effect name by index. Returns "None" for index 0, empty string for invalid index. + std::string get_effect_name_by_index(uint32_t index) const { + if (index == 0) { + return "None"; + } + if (index > this->effects_.size()) { + return ""; // Invalid index + } + return this->effects_[index - 1]->get_name(); + } + /// The result of all the current_values_as_* methods have gamma correction applied. void current_values_as_binary(bool *binary); From 6819bbd8f8bf90e307914fef6124b9f18814f645 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 19 Aug 2025 21:38:32 -0500 Subject: [PATCH 047/208] [esp32_ble_client] Add log helper functions to reduce flash usage by 120 bytes (#10243) --- .../esp32_ble_client/ble_client_base.cpp | 42 ++++++++++--------- .../esp32_ble_client/ble_client_base.h | 4 ++ 2 files changed, 27 insertions(+), 19 deletions(-) diff --git a/esphome/components/esp32_ble_client/ble_client_base.cpp b/esphome/components/esp32_ble_client/ble_client_base.cpp index 2d84436d84..e23be2e0c1 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.cpp +++ b/esphome/components/esp32_ble_client/ble_client_base.cpp @@ -159,8 +159,7 @@ void BLEClientBase::disconnect() { return; } if (this->state_ == espbt::ClientState::CONNECTING || this->conn_id_ == UNSET_CONN_ID) { - ESP_LOGW(TAG, "[%d] [%s] Disconnecting before connected, disconnect scheduled.", this->connection_index_, - this->address_str_.c_str()); + this->log_warning_("Disconnect before connected, disconnect scheduled."); this->want_disconnect_ = true; return; } @@ -172,13 +171,11 @@ void BLEClientBase::unconditional_disconnect() { ESP_LOGI(TAG, "[%d] [%s] Disconnecting (conn_id: %d).", this->connection_index_, this->address_str_.c_str(), this->conn_id_); if (this->state_ == espbt::ClientState::DISCONNECTING) { - ESP_LOGE(TAG, "[%d] [%s] Tried to disconnect while already disconnecting.", this->connection_index_, - this->address_str_.c_str()); + this->log_error_("Already disconnecting"); return; } if (this->conn_id_ == UNSET_CONN_ID) { - ESP_LOGE(TAG, "[%d] [%s] No connection ID set, cannot disconnect.", this->connection_index_, - this->address_str_.c_str()); + this->log_error_("conn id unset, cannot disconnect"); return; } auto err = esp_ble_gattc_close(this->gattc_if_, this->conn_id_); @@ -234,6 +231,18 @@ void BLEClientBase::log_connection_params_(const char *param_type) { ESP_LOGD(TAG, "[%d] [%s] %s conn params", this->connection_index_, this->address_str_.c_str(), param_type); } +void BLEClientBase::log_error_(const char *message) { + ESP_LOGE(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_.c_str(), message); +} + +void BLEClientBase::log_error_(const char *message, int code) { + ESP_LOGE(TAG, "[%d] [%s] %s=%d", this->connection_index_, this->address_str_.c_str(), message, code); +} + +void BLEClientBase::log_warning_(const char *message) { + ESP_LOGW(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_.c_str(), message); +} + void BLEClientBase::restore_medium_conn_params_() { // Restore to medium connection parameters after initial connection phase // This balances performance with bandwidth usage for normal operation @@ -264,8 +273,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ this->app_id); this->gattc_if_ = esp_gattc_if; } else { - ESP_LOGE(TAG, "[%d] [%s] gattc app registration failed id=%d code=%d", this->connection_index_, - this->address_str_.c_str(), param->reg.app_id, param->reg.status); + this->log_error_("gattc app registration failed status", param->reg.status); this->status_ = param->reg.status; this->mark_failed(); } @@ -281,8 +289,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ // This should not happen but lets log it in case it does // because it means we have a bad assumption about how the // ESP BT stack works. - ESP_LOGE(TAG, "[%d] [%s] Got ESP_GATTC_OPEN_EVT while in %s state, status=%d", this->connection_index_, - this->address_str_.c_str(), espbt::client_state_to_string(this->state_), param->open.status); + this->log_error_("ESP_GATTC_OPEN_EVT wrong state status", param->open.status); } if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) { this->log_gattc_warning_("Connection open", param->open.status); @@ -307,7 +314,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ this->state_ = espbt::ClientState::ESTABLISHED; break; } - ESP_LOGD(TAG, "[%d] [%s] Searching for services", this->connection_index_, this->address_str_.c_str()); + this->log_event_("Searching for services"); esp_ble_gattc_search_service(esp_gattc_if, param->cfg_mtu.conn_id, nullptr); break; } @@ -332,8 +339,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ // Check if we were disconnected while waiting for service discovery if (param->disconnect.reason == ESP_GATT_CONN_TERMINATE_PEER_USER && this->state_ == espbt::ClientState::CONNECTED) { - ESP_LOGW(TAG, "[%d] [%s] Disconnected by remote during service discovery", this->connection_index_, - this->address_str_.c_str()); + this->log_warning_("Remote closed during discovery"); } else { ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_DISCONNECT_EVT, reason 0x%02x", this->connection_index_, this->address_str_.c_str(), param->disconnect.reason); @@ -506,16 +512,14 @@ void BLEClientBase::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_ return; esp_bd_addr_t bd_addr; memcpy(bd_addr, param->ble_security.auth_cmpl.bd_addr, sizeof(esp_bd_addr_t)); - ESP_LOGI(TAG, "[%d] [%s] auth complete. remote BD_ADDR: %s", this->connection_index_, this->address_str_.c_str(), + ESP_LOGI(TAG, "[%d] [%s] auth complete addr: %s", this->connection_index_, this->address_str_.c_str(), format_hex(bd_addr, 6).c_str()); if (!param->ble_security.auth_cmpl.success) { - ESP_LOGE(TAG, "[%d] [%s] auth fail reason = 0x%x", this->connection_index_, this->address_str_.c_str(), - param->ble_security.auth_cmpl.fail_reason); + this->log_error_("auth fail reason", param->ble_security.auth_cmpl.fail_reason); } else { this->paired_ = true; - ESP_LOGD(TAG, "[%d] [%s] auth success. address type = %d auth mode = %d", this->connection_index_, - this->address_str_.c_str(), param->ble_security.auth_cmpl.addr_type, - param->ble_security.auth_cmpl.auth_mode); + ESP_LOGD(TAG, "[%d] [%s] auth success type = %d mode = %d", this->connection_index_, this->address_str_.c_str(), + param->ble_security.auth_cmpl.addr_type, param->ble_security.auth_cmpl.auth_mode); } break; diff --git a/esphome/components/esp32_ble_client/ble_client_base.h b/esphome/components/esp32_ble_client/ble_client_base.h index 2d00688dbd..1850b2c5b3 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.h +++ b/esphome/components/esp32_ble_client/ble_client_base.h @@ -137,6 +137,10 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { void log_gattc_warning_(const char *operation, esp_gatt_status_t status); void log_gattc_warning_(const char *operation, esp_err_t err); void log_connection_params_(const char *param_type); + // Compact error logging helpers to reduce flash usage + void log_error_(const char *message); + void log_error_(const char *message, int code); + void log_warning_(const char *message); }; } // namespace esphome::esp32_ble_client From 5a6db28f1deb694e230013260768a9044de9f456 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 20 Aug 2025 14:39:29 +1200 Subject: [PATCH 048/208] [CI] Base ``too-big`` label on new additions only (#10307) --- .github/workflows/auto-label-pr.yml | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/.github/workflows/auto-label-pr.yml b/.github/workflows/auto-label-pr.yml index 63f059eb6d..748235df30 100644 --- a/.github/workflows/auto-label-pr.yml +++ b/.github/workflows/auto-label-pr.yml @@ -105,7 +105,9 @@ jobs: // Calculate data from PR files const changedFiles = prFiles.map(file => file.filename); - const totalChanges = prFiles.reduce((sum, file) => sum + (file.additions || 0) + (file.deletions || 0), 0); + const totalAdditions = prFiles.reduce((sum, file) => sum + (file.additions || 0), 0); + const totalDeletions = prFiles.reduce((sum, file) => sum + (file.deletions || 0), 0); + const totalChanges = totalAdditions + totalDeletions; console.log('Current labels:', currentLabels.join(', ')); console.log('Changed files:', changedFiles.length); @@ -231,16 +233,21 @@ jobs: // Strategy: PR size detection async function detectPRSize() { const labels = new Set(); - const testChanges = prFiles - .filter(file => file.filename.startsWith('tests/')) - .reduce((sum, file) => sum + (file.additions || 0) + (file.deletions || 0), 0); - - const nonTestChanges = totalChanges - testChanges; if (totalChanges <= SMALL_PR_THRESHOLD) { labels.add('small-pr'); + return labels; } + const testAdditions = prFiles + .filter(file => file.filename.startsWith('tests/')) + .reduce((sum, file) => sum + (file.additions || 0), 0); + const testDeletions = prFiles + .filter(file => file.filename.startsWith('tests/')) + .reduce((sum, file) => sum + (file.deletions || 0), 0); + + const nonTestChanges = (totalAdditions - testAdditions) - (totalDeletions - testDeletions); + // Don't add too-big if mega-pr label is already present if (nonTestChanges > TOO_BIG_THRESHOLD && !isMegaPR) { labels.add('too-big'); @@ -412,10 +419,13 @@ jobs: // Too big message if (finalLabels.includes('too-big')) { - const testChanges = prFiles + const testAdditions = prFiles .filter(file => file.filename.startsWith('tests/')) - .reduce((sum, file) => sum + (file.additions || 0) + (file.deletions || 0), 0); - const nonTestChanges = totalChanges - testChanges; + .reduce((sum, file) => sum + (file.additions || 0), 0); + const testDeletions = prFiles + .filter(file => file.filename.startsWith('tests/')) + .reduce((sum, file) => sum + (file.deletions || 0), 0); + const nonTestChanges = (totalAdditions - testAdditions) - (totalDeletions - testDeletions); const tooManyLabels = finalLabels.length > MAX_LABELS; const tooManyChanges = nonTestChanges > TOO_BIG_THRESHOLD; From 0089619518a9aa7cd132cd30a0bafbb071e15a22 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 19 Aug 2025 21:41:34 -0500 Subject: [PATCH 049/208] [web_server] Reduce flash usage by consolidating defer calls in switch and lock handlers (#10297) --- esphome/components/web_server/web_server.cpp | 70 ++++++++++++++++---- 1 file changed, 58 insertions(+), 12 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 399b8785ae..290992b096 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -507,14 +507,37 @@ void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlM auto detail = get_request_detail(request); std::string data = this->switch_json(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); - } else if (match.method_equals("toggle")) { - this->defer([obj]() { obj->toggle(); }); - request->send(200); + return; + } + + // Handle action methods with single defer and response + enum SwitchAction { NONE, TOGGLE, TURN_ON, TURN_OFF }; + SwitchAction action = NONE; + + if (match.method_equals("toggle")) { + action = TOGGLE; } else if (match.method_equals("turn_on")) { - this->defer([obj]() { obj->turn_on(); }); - request->send(200); + action = TURN_ON; } else if (match.method_equals("turn_off")) { - this->defer([obj]() { obj->turn_off(); }); + action = TURN_OFF; + } + + if (action != NONE) { + this->defer([obj, action]() { + switch (action) { + case TOGGLE: + obj->toggle(); + break; + case TURN_ON: + obj->turn_on(); + break; + case TURN_OFF: + obj->turn_off(); + break; + default: + break; + } + }); request->send(200); } else { request->send(404); @@ -1332,14 +1355,37 @@ void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMat auto detail = get_request_detail(request); std::string data = this->lock_json(obj, obj->state, detail); request->send(200, "application/json", data.c_str()); - } else if (match.method_equals("lock")) { - this->defer([obj]() { obj->lock(); }); - request->send(200); + return; + } + + // Handle action methods with single defer and response + enum LockAction { NONE, LOCK, UNLOCK, OPEN }; + LockAction action = NONE; + + if (match.method_equals("lock")) { + action = LOCK; } else if (match.method_equals("unlock")) { - this->defer([obj]() { obj->unlock(); }); - request->send(200); + action = UNLOCK; } else if (match.method_equals("open")) { - this->defer([obj]() { obj->open(); }); + action = OPEN; + } + + if (action != NONE) { + this->defer([obj, action]() { + switch (action) { + case LOCK: + obj->lock(); + break; + case UNLOCK: + obj->unlock(); + break; + case OPEN: + obj->open(); + break; + default: + break; + } + }); request->send(200); } else { request->send(404); From fc1b49e87d2066c0f3d08e58c7c8f29b321dcdfa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 20 Aug 2025 14:42:33 +1200 Subject: [PATCH 050/208] Bump ruamel-yaml from 0.18.14 to 0.18.15 (#10310) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 0675115c02..2665211381 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,7 +15,7 @@ esphome-dashboard==20250814.0 aioesphomeapi==39.0.0 zeroconf==0.147.0 puremagic==1.30 -ruamel.yaml==0.18.14 # dashboard_import +ruamel.yaml==0.18.15 # dashboard_import esphome-glyphsets==0.2.0 pillow==10.4.0 cairosvg==2.8.2 From 56b6dd31f13fccf83f7bd04f819257019ffdc66b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 19 Aug 2025 21:45:13 -0500 Subject: [PATCH 051/208] [core] Eliminate heap allocation in teardown_components by using StaticVector (#10256) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/core/application.cpp | 79 +++++++++++++++++++++++++++++------- 1 file changed, 64 insertions(+), 15 deletions(-) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index d2d47fe171..dc745a2a46 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -256,30 +256,79 @@ void Application::run_powerdown_hooks() { void Application::teardown_components(uint32_t timeout_ms) { uint32_t start_time = millis(); - // Copy all components in reverse order using reverse iterators + // Use a StaticVector instead of std::vector to avoid heap allocation + // since we know the actual size at compile time + StaticVector pending_components; + + // Copy all components in reverse order // Reverse order matches the behavior of run_safe_shutdown_hooks() above and ensures // components are torn down in the opposite order of their setup_priority (which is // used to sort components during Application::setup()) - std::vector pending_components(this->components_.rbegin(), this->components_.rend()); + size_t num_components = this->components_.size(); + for (size_t i = 0; i < num_components; ++i) { + pending_components[i] = this->components_[num_components - 1 - i]; + } uint32_t now = start_time; - while (!pending_components.empty() && (now - start_time) < timeout_ms) { + size_t pending_count = num_components; + + // Teardown Algorithm + // ================== + // We iterate through pending components, calling teardown() on each. + // Components that return false (need more time) are copied forward + // in the array. Components that return true (finished) are skipped. + // + // The compaction happens in-place during iteration: + // - still_pending tracks the write position (where to put next pending component) + // - i tracks the read position (which component we're testing) + // - When teardown() returns false, we copy component[i] to component[still_pending] + // - When teardown() returns true, we just skip it (don't increment still_pending) + // + // Example with 4 components where B can teardown immediately: + // + // Start: + // pending_components: [A, B, C, D] + // pending_count: 4 ^----------^ + // + // Iteration 1: + // i=0: A needs more time → keep at pos 0 (no copy needed) + // i=1: B finished → skip + // i=2: C needs more time → copy to pos 1 + // i=3: D needs more time → copy to pos 2 + // + // After iteration 1: + // pending_components: [A, C, D | D] + // pending_count: 3 ^--------^ + // + // Iteration 2: + // i=0: A finished → skip + // i=1: C needs more time → copy to pos 0 + // i=2: D finished → skip + // + // After iteration 2: + // pending_components: [C | C, D, D] (positions 1-3 have old values) + // pending_count: 1 ^--^ + + while (pending_count > 0 && (now - start_time) < timeout_ms) { // Feed watchdog during teardown to prevent triggering this->feed_wdt(now); - // Use iterator to safely erase elements - for (auto it = pending_components.begin(); it != pending_components.end();) { - if ((*it)->teardown()) { - // Component finished teardown, erase it - it = pending_components.erase(it); - } else { - // Component still needs time - ++it; + // Process components and compact the array, keeping only those still pending + size_t still_pending = 0; + for (size_t i = 0; i < pending_count; ++i) { + if (!pending_components[i]->teardown()) { + // Component still needs time, copy it forward + if (still_pending != i) { + pending_components[still_pending] = pending_components[i]; + } + ++still_pending; } + // Component finished teardown, skip it (don't increment still_pending) } + pending_count = still_pending; // Give some time for I/O operations if components are still pending - if (!pending_components.empty()) { + if (pending_count > 0) { this->yield_with_select_(1); } @@ -287,11 +336,11 @@ void Application::teardown_components(uint32_t timeout_ms) { now = millis(); } - if (!pending_components.empty()) { + if (pending_count > 0) { // Note: At this point, connections are either disconnected or in a bad state, // so this warning will only appear via serial rather than being transmitted to clients - for (auto *component : pending_components) { - ESP_LOGW(TAG, "%s did not complete teardown within %" PRIu32 " ms", component->get_component_source(), + for (size_t i = 0; i < pending_count; ++i) { + ESP_LOGW(TAG, "%s did not complete teardown within %" PRIu32 " ms", pending_components[i]->get_component_source(), timeout_ms); } } From 2cbf4f30f97e676613da0c1cba0a9f9236268ade Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 19 Aug 2025 21:48:04 -0500 Subject: [PATCH 052/208] [libretiny] Optimize preferences is_changed() by replacing temporary vector with unique_ptr (#10272) --- esphome/components/libretiny/preferences.cpp | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/esphome/components/libretiny/preferences.cpp b/esphome/components/libretiny/preferences.cpp index ce4ed915c0..fc535c99b4 100644 --- a/esphome/components/libretiny/preferences.cpp +++ b/esphome/components/libretiny/preferences.cpp @@ -5,7 +5,7 @@ #include "esphome/core/preferences.h" #include #include -#include +#include #include namespace esphome { @@ -139,21 +139,29 @@ class LibreTinyPreferences : public ESPPreferences { } bool is_changed(const fdb_kvdb_t db, const NVSData &to_save) { - NVSData stored_data{}; struct fdb_kv kv; fdb_kv_t kvp = fdb_kv_get_obj(db, to_save.key.c_str(), &kv); if (kvp == nullptr) { ESP_LOGV(TAG, "fdb_kv_get_obj('%s'): nullptr - the key might not be set yet", to_save.key.c_str()); return true; } - stored_data.data.resize(kv.value_len); - fdb_blob_make(&blob, stored_data.data.data(), kv.value_len); + + // Check size first - if different, data has changed + if (kv.value_len != to_save.data.size()) { + return true; + } + + // Allocate buffer on heap to avoid stack allocation for large data + auto stored_data = std::make_unique(kv.value_len); + fdb_blob_make(&blob, stored_data.get(), kv.value_len); size_t actual_len = fdb_kv_get_blob(db, to_save.key.c_str(), &blob); if (actual_len != kv.value_len) { ESP_LOGV(TAG, "fdb_kv_get_blob('%s') len mismatch: %u != %u", to_save.key.c_str(), actual_len, kv.value_len); return true; } - return to_save.data != stored_data.data; + + // Compare the actual data + return memcmp(to_save.data.data(), stored_data.get(), kv.value_len) != 0; } bool reset() override { From 3ff5b4773b7783065e5bcb64059f47700d56f55f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 19 Aug 2025 21:48:40 -0500 Subject: [PATCH 053/208] [bluetooth_proxy] Mark BluetoothConnection and BluetoothProxy as final for compiler optimizations (#10280) --- esphome/components/bluetooth_proxy/bluetooth_connection.h | 2 +- esphome/components/bluetooth_proxy/bluetooth_proxy.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/bluetooth_proxy/bluetooth_connection.h b/esphome/components/bluetooth_proxy/bluetooth_connection.h index a975d25d91..e5d5ff2dd6 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_connection.h +++ b/esphome/components/bluetooth_proxy/bluetooth_connection.h @@ -8,7 +8,7 @@ namespace esphome::bluetooth_proxy { class BluetoothProxy; -class BluetoothConnection : public esp32_ble_client::BLEClientBase { +class BluetoothConnection final : public esp32_ble_client::BLEClientBase { public: void dump_config() override; void loop() override; diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.h b/esphome/components/bluetooth_proxy/bluetooth_proxy.h index bc8d3ed762..c81c8c9532 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.h +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.h @@ -50,7 +50,7 @@ enum BluetoothProxySubscriptionFlag : uint32_t { SUBSCRIPTION_RAW_ADVERTISEMENTS = 1 << 0, }; -class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Component { +class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener, public Component { friend class BluetoothConnection; // Allow connection to update connections_free_response_ public: BluetoothProxy(); From 86f306ba9e32e020b5ce77f5d1a74f4bf7060daa Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 20 Aug 2025 22:02:14 +1200 Subject: [PATCH 054/208] [CI] Also require tests for ``new-features`` (#10311) --- .github/workflows/auto-label-pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/auto-label-pr.yml b/.github/workflows/auto-label-pr.yml index 748235df30..c42b5330d2 100644 --- a/.github/workflows/auto-label-pr.yml +++ b/.github/workflows/auto-label-pr.yml @@ -382,7 +382,7 @@ jobs: const labels = new Set(); // Check for missing tests - if ((allLabels.has('new-component') || allLabels.has('new-platform')) && !allLabels.has('has-tests')) { + if ((allLabels.has('new-component') || allLabels.has('new-platform') || allLabels.has('new-feature')) && !allLabels.has('has-tests')) { labels.add('needs-tests'); } From d45944a9e26ab1aa1340e3bb011aca4e3e56412a Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 20 Aug 2025 23:47:20 +1200 Subject: [PATCH 055/208] [api] Add ``USE_API_HOMEASSISTANT_SERVICES`` if using ``tag_scanned`` action (#10316) --- esphome/components/api/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index ba5f994e9a..2672ea1edb 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -321,6 +321,7 @@ HOMEASSISTANT_TAG_SCANNED_ACTION_SCHEMA = cv.maybe_simple_value( HOMEASSISTANT_TAG_SCANNED_ACTION_SCHEMA, ) async def homeassistant_tag_scanned_to_code(config, action_id, template_arg, args): + cg.add_define("USE_API_HOMEASSISTANT_SERVICES") serv = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, serv, True) cg.add(var.set_service("esphome.tag_scanned")) From 35411d199f79ca92fce2dc18ab91f050d088ba8f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 20 Aug 2025 09:10:20 -0500 Subject: [PATCH 056/208] [homeassistant] Add compilation test for homeassistant.tag_scanned action (#10319) --- .../homeassistant/test-tag-scanned.esp32-idf.yaml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 tests/components/homeassistant/test-tag-scanned.esp32-idf.yaml diff --git a/tests/components/homeassistant/test-tag-scanned.esp32-idf.yaml b/tests/components/homeassistant/test-tag-scanned.esp32-idf.yaml new file mode 100644 index 0000000000..ef148174d7 --- /dev/null +++ b/tests/components/homeassistant/test-tag-scanned.esp32-idf.yaml @@ -0,0 +1,14 @@ +wifi: + ssid: MySSID + password: password1 + +api: + +esphome: + on_boot: + then: + - homeassistant.tag_scanned: 'test_tag_123' + - homeassistant.tag_scanned: + tag: 'another_tag' + - homeassistant.tag_scanned: + tag: !lambda 'return "dynamic_tag";' From 72c58ae36d85490256bb1bce15b47ccefa6693a6 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 21 Aug 2025 02:13:50 +1200 Subject: [PATCH 057/208] [core] Add idf-tidy env for esp32-c6 (#10270) --- .clang-tidy.hash | 2 +- platformio.ini | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/.clang-tidy.hash b/.clang-tidy.hash index 30cf982649..a6de2366bc 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -6af8b429b94191fe8e239fcb3b73f7982d0266cb5b05ffbc81edaeac1bc8c273 +0440e35cf89a49e8a35fd3690ed453a72b7b6f61b9d346ced6140e1c0d39dff6 diff --git a/platformio.ini b/platformio.ini index d9f2f879ec..47fc5205bc 100644 --- a/platformio.ini +++ b/platformio.ini @@ -357,6 +357,19 @@ build_flags = ${common:esp32-idf.build_flags} ${flags:runtime.build_flags} -DUSE_ESP32_VARIANT_ESP32C6 +build_unflags = + ${common.build_unflags} + +[env:esp32c6-idf-tidy] +extends = common:esp32-idf +board = esp32-c6-devkitc-1 +board_build.esp-idf.sdkconfig_path = .temp/sdkconfig-esp32c6-idf-tidy +build_flags = + ${common:esp32-idf.build_flags} + ${flags:clangtidy.build_flags} + -DUSE_ESP32_VARIANT_ESP32C6 +build_unflags = + ${common.build_unflags} ;;;;;;;; ESP32-S2 ;;;;;;;; From 33eddb60352f63e6069f7908c0c57dc0bf1e9796 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 20 Aug 2025 16:46:04 -0500 Subject: [PATCH 058/208] Bump codecov/codecov-action from 5.4.3 to 5.5.0 (#10336) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e50f2aef40..8f429f7b40 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -156,7 +156,7 @@ jobs: . venv/bin/activate pytest -vv --cov-report=xml --tb=native -n auto tests --ignore=tests/integration/ - name: Upload coverage to Codecov - uses: codecov/codecov-action@v5.4.3 + uses: codecov/codecov-action@v5.5.0 with: token: ${{ secrets.CODECOV_TOKEN }} - name: Save Python virtual environment cache From 3ca00152847d37e205ac2464eded443fd3d87b31 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 21 Aug 2025 18:48:48 +1200 Subject: [PATCH 059/208] [opentherm] Rename c++ files for predictable doxygen generation (#10314) --- esphome/components/opentherm/hub.h | 12 ++++++------ .../number/{number.cpp => opentherm_number.cpp} | 2 +- .../number/{number.h => opentherm_number.h} | 0 .../output/{output.cpp => opentherm_output.cpp} | 2 +- .../output/{output.h => opentherm_output.h} | 0 .../switch/{switch.cpp => opentherm_switch.cpp} | 2 +- .../switch/{switch.h => opentherm_switch.h} | 0 7 files changed, 9 insertions(+), 9 deletions(-) rename esphome/components/opentherm/number/{number.cpp => opentherm_number.cpp} (97%) rename esphome/components/opentherm/number/{number.h => opentherm_number.h} (100%) rename esphome/components/opentherm/output/{output.cpp => opentherm_output.cpp} (95%) rename esphome/components/opentherm/output/{output.h => opentherm_output.h} (100%) rename esphome/components/opentherm/switch/{switch.cpp => opentherm_switch.cpp} (96%) rename esphome/components/opentherm/switch/{switch.h => opentherm_switch.h} (100%) diff --git a/esphome/components/opentherm/hub.h b/esphome/components/opentherm/hub.h index 80fd268820..ee0cfd104d 100644 --- a/esphome/components/opentherm/hub.h +++ b/esphome/components/opentherm/hub.h @@ -1,10 +1,10 @@ #pragma once +#include +#include "esphome/core/component.h" #include "esphome/core/defines.h" #include "esphome/core/hal.h" -#include "esphome/core/component.h" #include "esphome/core/log.h" -#include #include "opentherm.h" @@ -17,21 +17,21 @@ #endif #ifdef OPENTHERM_USE_SWITCH -#include "esphome/components/opentherm/switch/switch.h" +#include "esphome/components/opentherm/switch/opentherm_switch.h" #endif #ifdef OPENTHERM_USE_OUTPUT -#include "esphome/components/opentherm/output/output.h" +#include "esphome/components/opentherm/output/opentherm_output.h" #endif #ifdef OPENTHERM_USE_NUMBER -#include "esphome/components/opentherm/number/number.h" +#include "esphome/components/opentherm/number/opentherm_number.h" #endif +#include #include #include #include -#include #include "opentherm_macros.h" diff --git a/esphome/components/opentherm/number/number.cpp b/esphome/components/opentherm/number/opentherm_number.cpp similarity index 97% rename from esphome/components/opentherm/number/number.cpp rename to esphome/components/opentherm/number/opentherm_number.cpp index 90ab5d6490..c5d094aa36 100644 --- a/esphome/components/opentherm/number/number.cpp +++ b/esphome/components/opentherm/number/opentherm_number.cpp @@ -1,4 +1,4 @@ -#include "number.h" +#include "opentherm_number.h" namespace esphome { namespace opentherm { diff --git a/esphome/components/opentherm/number/number.h b/esphome/components/opentherm/number/opentherm_number.h similarity index 100% rename from esphome/components/opentherm/number/number.h rename to esphome/components/opentherm/number/opentherm_number.h diff --git a/esphome/components/opentherm/output/output.cpp b/esphome/components/opentherm/output/opentherm_output.cpp similarity index 95% rename from esphome/components/opentherm/output/output.cpp rename to esphome/components/opentherm/output/opentherm_output.cpp index 486aa0d4e7..ff82ddd72c 100644 --- a/esphome/components/opentherm/output/output.cpp +++ b/esphome/components/opentherm/output/opentherm_output.cpp @@ -1,5 +1,5 @@ #include "esphome/core/helpers.h" // for clamp() and lerp() -#include "output.h" +#include "opentherm_output.h" namespace esphome { namespace opentherm { diff --git a/esphome/components/opentherm/output/output.h b/esphome/components/opentherm/output/opentherm_output.h similarity index 100% rename from esphome/components/opentherm/output/output.h rename to esphome/components/opentherm/output/opentherm_output.h diff --git a/esphome/components/opentherm/switch/switch.cpp b/esphome/components/opentherm/switch/opentherm_switch.cpp similarity index 96% rename from esphome/components/opentherm/switch/switch.cpp rename to esphome/components/opentherm/switch/opentherm_switch.cpp index 228d9ac8f3..5c5d62e68e 100644 --- a/esphome/components/opentherm/switch/switch.cpp +++ b/esphome/components/opentherm/switch/opentherm_switch.cpp @@ -1,4 +1,4 @@ -#include "switch.h" +#include "opentherm_switch.h" namespace esphome { namespace opentherm { diff --git a/esphome/components/opentherm/switch/switch.h b/esphome/components/opentherm/switch/opentherm_switch.h similarity index 100% rename from esphome/components/opentherm/switch/switch.h rename to esphome/components/opentherm/switch/opentherm_switch.h From 94accd5abe8d70713f3b4db7809e4b47824e7ce4 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 21 Aug 2025 18:49:26 +1200 Subject: [PATCH 060/208] [ld2420] Rename c++ files for predictable doxygen generation (#10315) --- .../text_sensor/{text_sensor.cpp => ld2420_text_sensor.cpp} | 2 +- .../ld2420/text_sensor/{text_sensor.h => ld2420_text_sensor.h} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename esphome/components/ld2420/text_sensor/{text_sensor.cpp => ld2420_text_sensor.cpp} (91%) rename esphome/components/ld2420/text_sensor/{text_sensor.h => ld2420_text_sensor.h} (100%) diff --git a/esphome/components/ld2420/text_sensor/text_sensor.cpp b/esphome/components/ld2420/text_sensor/ld2420_text_sensor.cpp similarity index 91% rename from esphome/components/ld2420/text_sensor/text_sensor.cpp rename to esphome/components/ld2420/text_sensor/ld2420_text_sensor.cpp index 73af3b3660..f647a36936 100644 --- a/esphome/components/ld2420/text_sensor/text_sensor.cpp +++ b/esphome/components/ld2420/text_sensor/ld2420_text_sensor.cpp @@ -1,4 +1,4 @@ -#include "text_sensor.h" +#include "ld2420_text_sensor.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" diff --git a/esphome/components/ld2420/text_sensor/text_sensor.h b/esphome/components/ld2420/text_sensor/ld2420_text_sensor.h similarity index 100% rename from esphome/components/ld2420/text_sensor/text_sensor.h rename to esphome/components/ld2420/text_sensor/ld2420_text_sensor.h From 83fe4b4ff334d04a9f0cc70e296e5b375cea352f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 21 Aug 2025 15:36:06 -0500 Subject: [PATCH 061/208] Bump ruff from 0.12.9 to 0.12.10 (#10362) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston --- .pre-commit-config.yaml | 2 +- requirements_test.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5540733131..8b76523b2e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ ci: repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.12.9 + rev: v0.12.10 hooks: # Run the linter. - id: ruff diff --git a/requirements_test.txt b/requirements_test.txt index f0a16fd7f3..f55618c0f8 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==3.3.8 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating -ruff==0.12.9 # also change in .pre-commit-config.yaml when updating +ruff==0.12.10 # also change in .pre-commit-config.yaml when updating pyupgrade==3.20.0 # also change in .pre-commit-config.yaml when updating pre-commit From 5cd9a86dcba809120cb3ccd62e6ea2af088c55ad Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Sat, 23 Aug 2025 08:20:16 +0200 Subject: [PATCH 062/208] [nrf52] update toolchain to v0.17.4, support mac (#10391) --- .clang-tidy.hash | 2 +- esphome/components/nrf52/__init__.py | 2 +- platformio.ini | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.clang-tidy.hash b/.clang-tidy.hash index a6de2366bc..07d9b3931e 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -0440e35cf89a49e8a35fd3690ed453a72b7b6f61b9d346ced6140e1c0d39dff6 +8158e5b8ed99b8c2d1d8953db0561393e07cc2555e2230ba685b591db9af156e diff --git a/esphome/components/nrf52/__init__.py b/esphome/components/nrf52/__init__.py index 908a855f70..e0ccd60c1a 100644 --- a/esphome/components/nrf52/__init__.py +++ b/esphome/components/nrf52/__init__.py @@ -120,7 +120,7 @@ async def to_code(config: ConfigType) -> None: "platform_packages", [ "platformio/framework-zephyr@https://github.com/tomaszduda23/framework-sdk-nrf/archive/refs/tags/v2.6.1-4.zip", - "platformio/toolchain-gccarmnoneeabi@https://github.com/tomaszduda23/toolchain-sdk-ng/archive/refs/tags/v0.16.1-1.zip", + "platformio/toolchain-gccarmnoneeabi@https://github.com/tomaszduda23/toolchain-sdk-ng/archive/refs/tags/v0.17.4-0.zip", ], ) diff --git a/platformio.ini b/platformio.ini index 47fc5205bc..7b9cbbc9b7 100644 --- a/platformio.ini +++ b/platformio.ini @@ -222,7 +222,7 @@ platform = https://github.com/tomaszduda23/platform-nordicnrf52/archive/refs/tag framework = zephyr platform_packages = platformio/framework-zephyr @ https://github.com/tomaszduda23/framework-sdk-nrf/archive/refs/tags/v2.6.1-4.zip - platformio/toolchain-gccarmnoneeabi@https://github.com/tomaszduda23/toolchain-sdk-ng/archive/refs/tags/v0.16.1-1.zip + platformio/toolchain-gccarmnoneeabi@https://github.com/tomaszduda23/toolchain-sdk-ng/archive/refs/tags/v0.17.4-0.zip build_flags = ${common.build_flags} -DUSE_ZEPHYR From 17f787fc36a04617f033efaddd704c7c79cb4e14 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Sat, 23 Aug 2025 14:17:42 +0200 Subject: [PATCH 063/208] [nrf52] fix build in dashboard (#10323) --- .clang-tidy.hash | 2 +- esphome/components/nrf52/__init__.py | 2 +- platformio.ini | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.clang-tidy.hash b/.clang-tidy.hash index 07d9b3931e..f61b79de4d 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -8158e5b8ed99b8c2d1d8953db0561393e07cc2555e2230ba685b591db9af156e +4368db58e8f884aff245996b1e8b644cc0796c0bb2fa706d5740d40b823d3ac9 diff --git a/esphome/components/nrf52/__init__.py b/esphome/components/nrf52/__init__.py index e0ccd60c1a..e75bf8192c 100644 --- a/esphome/components/nrf52/__init__.py +++ b/esphome/components/nrf52/__init__.py @@ -119,7 +119,7 @@ async def to_code(config: ConfigType) -> None: cg.add_platformio_option( "platform_packages", [ - "platformio/framework-zephyr@https://github.com/tomaszduda23/framework-sdk-nrf/archive/refs/tags/v2.6.1-4.zip", + "platformio/framework-zephyr@https://github.com/tomaszduda23/framework-sdk-nrf/archive/refs/tags/v2.6.1-7.zip", "platformio/toolchain-gccarmnoneeabi@https://github.com/tomaszduda23/toolchain-sdk-ng/archive/refs/tags/v0.17.4-0.zip", ], ) diff --git a/platformio.ini b/platformio.ini index 7b9cbbc9b7..d97607fac5 100644 --- a/platformio.ini +++ b/platformio.ini @@ -221,7 +221,7 @@ extends = common platform = https://github.com/tomaszduda23/platform-nordicnrf52/archive/refs/tags/v10.3.0-1.zip framework = zephyr platform_packages = - platformio/framework-zephyr @ https://github.com/tomaszduda23/framework-sdk-nrf/archive/refs/tags/v2.6.1-4.zip + platformio/framework-zephyr @ https://github.com/tomaszduda23/framework-sdk-nrf/archive/refs/tags/v2.6.1-7.zip platformio/toolchain-gccarmnoneeabi@https://github.com/tomaszduda23/toolchain-sdk-ng/archive/refs/tags/v0.17.4-0.zip build_flags = ${common.build_flags} From c096c6934d6e610c9192eaa09a23b01f41bbaba3 Mon Sep 17 00:00:00 2001 From: Thomas Rupprecht Date: Sun, 24 Aug 2025 10:56:06 +0200 Subject: [PATCH 064/208] fix temperature config validation regex (#9575) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/config_validation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/config_validation.py b/esphome/config_validation.py index f811fbf7c2..866ed4f8aa 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -1112,8 +1112,8 @@ voltage = float_with_unit("voltage", "(v|V|volt|Volts)?") distance = float_with_unit("distance", "(m)") framerate = float_with_unit("framerate", "(FPS|fps|Fps|FpS|Hz)") angle = float_with_unit("angle", "(°|deg)", optional_unit=True) -_temperature_c = float_with_unit("temperature", "(°C|° C|°|C)?") -_temperature_k = float_with_unit("temperature", "(° K|° K|K)?") +_temperature_c = float_with_unit("temperature", "(°C|° C|C|°)?") +_temperature_k = float_with_unit("temperature", "(°K|° K|K)?") _temperature_f = float_with_unit("temperature", "(°F|° F|F)?") decibel = float_with_unit("decibel", "(dB|dBm|db|dbm)", optional_unit=True) pressure = float_with_unit("pressure", "(bar|Bar)", optional_unit=True) From 12ba4b142e8dd2950e9406de2f8d378631772688 Mon Sep 17 00:00:00 2001 From: Thomas Rupprecht Date: Sun, 24 Aug 2025 11:03:14 +0200 Subject: [PATCH 065/208] Update Python to 3.11 in AI instructions (#10407) --- .ai/instructions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ai/instructions.md b/.ai/instructions.md index 6c002f9617..4d6cd71f85 100644 --- a/.ai/instructions.md +++ b/.ai/instructions.md @@ -9,7 +9,7 @@ This document provides essential context for AI models interacting with this pro ## 2. Core Technologies & Stack -* **Languages:** Python (>=3.10), C++ (gnu++20) +* **Languages:** Python (>=3.11), C++ (gnu++20) * **Frameworks & Runtimes:** PlatformIO, Arduino, ESP-IDF. * **Build Systems:** PlatformIO is the primary build system. CMake is used as an alternative. * **Configuration:** YAML. From be9c20c35758c64eae2289a8e8be4d04f5472212 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 25 Aug 2025 05:52:52 +1000 Subject: [PATCH 066/208] [mipi_spi] Add model (#10392) --- esphome/components/mipi_spi/models/jc.py | 229 +++++++++++++++++++++++ 1 file changed, 229 insertions(+) diff --git a/esphome/components/mipi_spi/models/jc.py b/esphome/components/mipi_spi/models/jc.py index f1f046a427..5dbf049ded 100644 --- a/esphome/components/mipi_spi/models/jc.py +++ b/esphome/components/mipi_spi/models/jc.py @@ -255,4 +255,233 @@ DriverChip( ), ) +DriverChip( + "JC3636W518V2", + height=360, + width=360, + offset_height=1, + draw_rounding=1, + cs_pin=10, + reset_pin=47, + invert_colors=True, + color_order=MODE_RGB, + bus_mode=TYPE_QUAD, + data_rate="40MHz", + initsequence=( + (0xF0, 0x28), + (0xF2, 0x28), + (0x73, 0xF0), + (0x7C, 0xD1), + (0x83, 0xE0), + (0x84, 0x61), + (0xF2, 0x82), + (0xF0, 0x00), + (0xF0, 0x01), + (0xF1, 0x01), + (0xB0, 0x56), + (0xB1, 0x4D), + (0xB2, 0x24), + (0xB4, 0x87), + (0xB5, 0x44), + (0xB6, 0x8B), + (0xB7, 0x40), + (0xB8, 0x86), + (0xBA, 0x00), + (0xBB, 0x08), + (0xBC, 0x08), + (0xBD, 0x00), + (0xC0, 0x80), + (0xC1, 0x10), + (0xC2, 0x37), + (0xC3, 0x80), + (0xC4, 0x10), + (0xC5, 0x37), + (0xC6, 0xA9), + (0xC7, 0x41), + (0xC8, 0x01), + (0xC9, 0xA9), + (0xCA, 0x41), + (0xCB, 0x01), + (0xD0, 0x91), + (0xD1, 0x68), + (0xD2, 0x68), + (0xF5, 0x00, 0xA5), + (0xDD, 0x4F), + (0xDE, 0x4F), + (0xF1, 0x10), + (0xF0, 0x00), + (0xF0, 0x02), + ( + 0xE0, + 0xF0, + 0x0A, + 0x10, + 0x09, + 0x09, + 0x36, + 0x35, + 0x33, + 0x4A, + 0x29, + 0x15, + 0x15, + 0x2E, + 0x34, + ), + ( + 0xE1, + 0xF0, + 0x0A, + 0x0F, + 0x08, + 0x08, + 0x05, + 0x34, + 0x33, + 0x4A, + 0x39, + 0x15, + 0x15, + 0x2D, + 0x33, + ), + (0xF0, 0x10), + (0xF3, 0x10), + (0xE0, 0x07), + (0xE1, 0x00), + (0xE2, 0x00), + (0xE3, 0x00), + (0xE4, 0xE0), + (0xE5, 0x06), + (0xE6, 0x21), + (0xE7, 0x01), + (0xE8, 0x05), + (0xE9, 0x02), + (0xEA, 0xDA), + (0xEB, 0x00), + (0xEC, 0x00), + (0xED, 0x0F), + (0xEE, 0x00), + (0xEF, 0x00), + (0xF8, 0x00), + (0xF9, 0x00), + (0xFA, 0x00), + (0xFB, 0x00), + (0xFC, 0x00), + (0xFD, 0x00), + (0xFE, 0x00), + (0xFF, 0x00), + (0x60, 0x40), + (0x61, 0x04), + (0x62, 0x00), + (0x63, 0x42), + (0x64, 0xD9), + (0x65, 0x00), + (0x66, 0x00), + (0x67, 0x00), + (0x68, 0x00), + (0x69, 0x00), + (0x6A, 0x00), + (0x6B, 0x00), + (0x70, 0x40), + (0x71, 0x03), + (0x72, 0x00), + (0x73, 0x42), + (0x74, 0xD8), + (0x75, 0x00), + (0x76, 0x00), + (0x77, 0x00), + (0x78, 0x00), + (0x79, 0x00), + (0x7A, 0x00), + (0x7B, 0x00), + (0x80, 0x48), + (0x81, 0x00), + (0x82, 0x06), + (0x83, 0x02), + (0x84, 0xD6), + (0x85, 0x04), + (0x86, 0x00), + (0x87, 0x00), + (0x88, 0x48), + (0x89, 0x00), + (0x8A, 0x08), + (0x8B, 0x02), + (0x8C, 0xD8), + (0x8D, 0x04), + (0x8E, 0x00), + (0x8F, 0x00), + (0x90, 0x48), + (0x91, 0x00), + (0x92, 0x0A), + (0x93, 0x02), + (0x94, 0xDA), + (0x95, 0x04), + (0x96, 0x00), + (0x97, 0x00), + (0x98, 0x48), + (0x99, 0x00), + (0x9A, 0x0C), + (0x9B, 0x02), + (0x9C, 0xDC), + (0x9D, 0x04), + (0x9E, 0x00), + (0x9F, 0x00), + (0xA0, 0x48), + (0xA1, 0x00), + (0xA2, 0x05), + (0xA3, 0x02), + (0xA4, 0xD5), + (0xA5, 0x04), + (0xA6, 0x00), + (0xA7, 0x00), + (0xA8, 0x48), + (0xA9, 0x00), + (0xAA, 0x07), + (0xAB, 0x02), + (0xAC, 0xD7), + (0xAD, 0x04), + (0xAE, 0x00), + (0xAF, 0x00), + (0xB0, 0x48), + (0xB1, 0x00), + (0xB2, 0x09), + (0xB3, 0x02), + (0xB4, 0xD9), + (0xB5, 0x04), + (0xB6, 0x00), + (0xB7, 0x00), + (0xB8, 0x48), + (0xB9, 0x00), + (0xBA, 0x0B), + (0xBB, 0x02), + (0xBC, 0xDB), + (0xBD, 0x04), + (0xBE, 0x00), + (0xBF, 0x00), + (0xC0, 0x10), + (0xC1, 0x47), + (0xC2, 0x56), + (0xC3, 0x65), + (0xC4, 0x74), + (0xC5, 0x88), + (0xC6, 0x99), + (0xC7, 0x01), + (0xC8, 0xBB), + (0xC9, 0xAA), + (0xD0, 0x10), + (0xD1, 0x47), + (0xD2, 0x56), + (0xD3, 0x65), + (0xD4, 0x74), + (0xD5, 0x88), + (0xD6, 0x99), + (0xD7, 0x01), + (0xD8, 0xBB), + (0xD9, 0xAA), + (0xF3, 0x01), + (0xF0, 0x00), + ), +) + models = {} From 9737b355791baf7348ae5eb5eaee85a88901fca3 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 25 Aug 2025 05:55:44 +1000 Subject: [PATCH 067/208] [http_request] Fix for host after ArduinoJson library bump (#10348) --- esphome/components/http_request/http_request_host.cpp | 7 +++++-- esphome/components/http_request/http_request_host.h | 6 +----- esphome/components/http_request/httplib.h | 10 +++++++--- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/esphome/components/http_request/http_request_host.cpp b/esphome/components/http_request/http_request_host.cpp index 192032c1ac..0b4c998a40 100644 --- a/esphome/components/http_request/http_request_host.cpp +++ b/esphome/components/http_request/http_request_host.cpp @@ -1,7 +1,10 @@ -#include "http_request_host.h" - #ifdef USE_HOST +#define USE_HTTP_REQUEST_HOST_H +#define CPPHTTPLIB_NO_EXCEPTIONS +#include "httplib.h" +#include "http_request_host.h" + #include #include "esphome/components/network/util.h" #include "esphome/components/watchdog/watchdog.h" diff --git a/esphome/components/http_request/http_request_host.h b/esphome/components/http_request/http_request_host.h index 49fd3b43fe..bbeed87f70 100644 --- a/esphome/components/http_request/http_request_host.h +++ b/esphome/components/http_request/http_request_host.h @@ -1,11 +1,7 @@ #pragma once -#include "http_request.h" - #ifdef USE_HOST - -#define CPPHTTPLIB_NO_EXCEPTIONS -#include "httplib.h" +#include "http_request.h" namespace esphome { namespace http_request { diff --git a/esphome/components/http_request/httplib.h b/esphome/components/http_request/httplib.h index a2f4436ec7..8b08699702 100644 --- a/esphome/components/http_request/httplib.h +++ b/esphome/components/http_request/httplib.h @@ -3,12 +3,10 @@ /** * NOTE: This is a copy of httplib.h from https://github.com/yhirose/cpp-httplib * - * It has been modified only to add ifdefs for USE_HOST. While it contains many functions unused in ESPHome, + * It has been modified to add ifdefs for USE_HOST. While it contains many functions unused in ESPHome, * it was considered preferable to use it with as few changes as possible, to facilitate future updates. */ -#include "esphome/core/defines.h" - // // httplib.h // @@ -17,6 +15,11 @@ // #ifdef USE_HOST +// Prevent this code being included in main.cpp +#ifdef USE_HTTP_REQUEST_HOST_H + +#include "esphome/core/defines.h" + #ifndef CPPHTTPLIB_HTTPLIB_H #define CPPHTTPLIB_HTTPLIB_H @@ -9687,5 +9690,6 @@ inline SSL_CTX *Client::ssl_context() const { #endif #endif // CPPHTTPLIB_HTTPLIB_H +#endif // USE_HTTP_REQUEST_HOST_H #endif From ca19959d7c991eb1ab6357c1578effe12e4a29b4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 24 Aug 2025 22:11:54 +0200 Subject: [PATCH 068/208] [core] Improve error reporting for entity name conflicts with non-ASCII characters (#10329) --- esphome/core/entity_helpers.py | 13 +++++- tests/unit_tests/core/test_entity_helpers.py | 45 ++++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/esphome/core/entity_helpers.py b/esphome/core/entity_helpers.py index 1ccc3e2683..e1b2a8264b 100644 --- a/esphome/core/entity_helpers.py +++ b/esphome/core/entity_helpers.py @@ -236,10 +236,21 @@ def entity_duplicate_validator(platform: str) -> Callable[[ConfigType], ConfigTy if existing_component != "unknown": conflict_msg += f" from component '{existing_component}'" + # Show both original names and their ASCII-only versions if they differ + sanitized_msg = "" + if entity_name != existing_name: + sanitized_msg = ( + f"\n Original names: '{entity_name}' and '{existing_name}'" + f"\n Both convert to ASCII ID: '{name_key}'" + "\n To fix: Add unique ASCII characters (e.g., '1', '2', or 'A', 'B')" + "\n to distinguish them" + ) + raise cv.Invalid( f"Duplicate {platform} entity with name '{entity_name}' found{device_prefix}. " f"{conflict_msg}. " - f"Each entity on a device must have a unique name within its platform." + "Each entity on a device must have a unique name within its platform." + f"{sanitized_msg}" ) # Store metadata about this entity diff --git a/tests/unit_tests/core/test_entity_helpers.py b/tests/unit_tests/core/test_entity_helpers.py index db99243a1a..9ba5367413 100644 --- a/tests/unit_tests/core/test_entity_helpers.py +++ b/tests/unit_tests/core/test_entity_helpers.py @@ -705,3 +705,48 @@ def test_empty_or_null_device_id_on_entity() -> None: config2 = {CONF_NAME: "Temperature", CONF_DEVICE_ID: None} validated2 = validator(config2) assert validated2 == config2 + + +def test_entity_duplicate_validator_non_ascii_names() -> None: + """Test that non-ASCII names show helpful error messages.""" + # Create validator for binary_sensor platform + validator = entity_duplicate_validator("binary_sensor") + + # First Russian sensor should pass + config1 = {CONF_NAME: "Датчик открытия основного крана"} + validated1 = validator(config1) + assert validated1 == config1 + + # Second Russian sensor with different text but same ASCII conversion should fail + config2 = {CONF_NAME: "Датчик закрытия основного крана"} + with pytest.raises( + Invalid, + match=re.compile( + r"Duplicate binary_sensor entity with name 'Датчик закрытия основного крана' found.*" + r"Original names: 'Датчик закрытия основного крана' and 'Датчик открытия основного крана'.*" + r"Both convert to ASCII ID: '_______________________________'.*" + r"To fix: Add unique ASCII characters \(e\.g\., '1', '2', or 'A', 'B'\)", + re.DOTALL, + ), + ): + validator(config2) + + +def test_entity_duplicate_validator_same_name_no_enhanced_message() -> None: + """Test that identical names don't show the enhanced message.""" + # Create validator for sensor platform + validator = entity_duplicate_validator("sensor") + + # First entity should pass + config1 = {CONF_NAME: "Temperature"} + validated1 = validator(config1) + assert validated1 == config1 + + # Second entity with exact same name should fail without enhanced message + config2 = {CONF_NAME: "Temperature"} + with pytest.raises( + Invalid, + match=r"Duplicate sensor entity with name 'Temperature' found.*" + r"Each entity on a device must have a unique name within its platform\.$", + ): + validator(config2) From 88303f39fa8174b17c9854ef5b8adafbb1ccb0a5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 24 Aug 2025 22:16:12 +0200 Subject: [PATCH 069/208] [pvvx_mithermometer] Fix race condition with BLE authentication (#10327) --- .../display/pvvx_display.cpp | 37 +++++++++++++++++-- .../pvvx_mithermometer/display/pvvx_display.h | 2 + 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/esphome/components/pvvx_mithermometer/display/pvvx_display.cpp b/esphome/components/pvvx_mithermometer/display/pvvx_display.cpp index 4b6c11b332..b6916ad68f 100644 --- a/esphome/components/pvvx_mithermometer/display/pvvx_display.cpp +++ b/esphome/components/pvvx_mithermometer/display/pvvx_display.cpp @@ -46,10 +46,32 @@ void PVVXDisplay::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t } this->connection_established_ = true; this->char_handle_ = chr->handle; -#ifdef USE_TIME - this->sync_time_(); -#endif - this->display(); + + // Attempt to write immediately + // For devices without security, this will work + // For devices with security that are already paired, this will work + // For devices that need pairing, the write will be retried after auth completes + this->sync_time_and_display_(); + break; + } + default: + break; + } +} + +void PVVXDisplay::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { + switch (event) { + case ESP_GAP_BLE_AUTH_CMPL_EVT: { + if (!this->parent_->check_addr(param->ble_security.auth_cmpl.bd_addr)) + return; + + if (param->ble_security.auth_cmpl.success) { + ESP_LOGD(TAG, "[%s] Authentication successful, performing writes.", this->parent_->address_str().c_str()); + // Now that pairing is complete, perform the pending writes + this->sync_time_and_display_(); + } else { + ESP_LOGW(TAG, "[%s] Authentication failed.", this->parent_->address_str().c_str()); + } break; } default: @@ -127,6 +149,13 @@ void PVVXDisplay::delayed_disconnect_() { this->set_timeout("disconnect", this->disconnect_delay_ms_, [this]() { this->parent_->set_enabled(false); }); } +void PVVXDisplay::sync_time_and_display_() { +#ifdef USE_TIME + this->sync_time_(); +#endif + this->display(); +} + #ifdef USE_TIME void PVVXDisplay::sync_time_() { if (this->time_ == nullptr) diff --git a/esphome/components/pvvx_mithermometer/display/pvvx_display.h b/esphome/components/pvvx_mithermometer/display/pvvx_display.h index 9739362024..c7fc523420 100644 --- a/esphome/components/pvvx_mithermometer/display/pvvx_display.h +++ b/esphome/components/pvvx_mithermometer/display/pvvx_display.h @@ -43,6 +43,7 @@ class PVVXDisplay : public ble_client::BLEClientNode, public PollingComponent { void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override; + void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override; /// Set validity period of the display information in seconds (1..65535) void set_validity_period(uint16_t validity_period) { this->validity_period_ = validity_period; } @@ -112,6 +113,7 @@ class PVVXDisplay : public ble_client::BLEClientNode, public PollingComponent { void setcfgbit_(uint8_t bit, bool value); void send_to_setup_char_(uint8_t *blk, size_t size); void delayed_disconnect_(); + void sync_time_and_display_(); #ifdef USE_TIME void sync_time_(); time::RealTimeClock *time_{nullptr}; From acfce581fa714eda03d4742927fc6d7d3c59275c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 24 Aug 2025 22:17:26 +0200 Subject: [PATCH 070/208] [esp32_ble_client] Optimize BLE connection parameters for different connection types (#10356) --- .../esp32_ble_client/ble_client_base.cpp | 99 +++++++++---------- .../esp32_ble_client/ble_client_base.h | 6 +- 2 files changed, 54 insertions(+), 51 deletions(-) diff --git a/esphome/components/esp32_ble_client/ble_client_base.cpp b/esphome/components/esp32_ble_client/ble_client_base.cpp index e23be2e0c1..b9b7c75e0a 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.cpp +++ b/esphome/components/esp32_ble_client/ble_client_base.cpp @@ -7,6 +7,7 @@ #include #include +#include namespace esphome::esp32_ble_client { @@ -111,43 +112,19 @@ void BLEClientBase::connect() { this->remote_addr_type_); this->paired_ = false; - // Set preferred connection parameters before connecting - // Use FAST for all V3 connections (better latency and reliability) - // Use MEDIUM for V1/legacy connections (balanced performance) - uint16_t min_interval, max_interval, timeout; - const char *param_type; - - if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE || - this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) { - min_interval = FAST_MIN_CONN_INTERVAL; - max_interval = FAST_MAX_CONN_INTERVAL; - timeout = FAST_CONN_TIMEOUT; - param_type = "fast"; - } else { - min_interval = MEDIUM_MIN_CONN_INTERVAL; - max_interval = MEDIUM_MAX_CONN_INTERVAL; - timeout = MEDIUM_CONN_TIMEOUT; - param_type = "medium"; + // Determine connection parameters based on connection type + if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) { + // V3 without cache needs fast params for service discovery + this->set_conn_params_(FAST_MIN_CONN_INTERVAL, FAST_MAX_CONN_INTERVAL, 0, FAST_CONN_TIMEOUT, "fast"); + } else if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) { + // V3 with cache can use medium params + this->set_conn_params_(MEDIUM_MIN_CONN_INTERVAL, MEDIUM_MAX_CONN_INTERVAL, 0, MEDIUM_CONN_TIMEOUT, "medium"); } + // For V1/Legacy, don't set params - use ESP-IDF defaults - auto param_ret = esp_ble_gap_set_prefer_conn_params(this->remote_bda_, min_interval, max_interval, - 0, // latency: 0 - timeout); - if (param_ret != ESP_OK) { - ESP_LOGW(TAG, "[%d] [%s] esp_ble_gap_set_prefer_conn_params failed: %d", this->connection_index_, - this->address_str_.c_str(), param_ret); - } else { - this->log_connection_params_(param_type); - } - - // Now open the connection + // Open the connection auto ret = esp_ble_gattc_open(this->gattc_if_, this->remote_bda_, this->remote_addr_type_, true); - if (ret) { - this->log_gattc_warning_("esp_ble_gattc_open", ret); - this->set_state(espbt::ClientState::IDLE); - } else { - this->set_state(espbt::ClientState::CONNECTING); - } + this->handle_connection_result_(ret); } esp_err_t BLEClientBase::pair() { return esp_ble_set_encryption(this->remote_bda_, ESP_BLE_SEC_ENCRYPT); } @@ -231,6 +208,15 @@ void BLEClientBase::log_connection_params_(const char *param_type) { ESP_LOGD(TAG, "[%d] [%s] %s conn params", this->connection_index_, this->address_str_.c_str(), param_type); } +void BLEClientBase::handle_connection_result_(esp_err_t ret) { + if (ret) { + this->log_gattc_warning_("esp_ble_gattc_open", ret); + this->set_state(espbt::ClientState::IDLE); + } else { + this->set_state(espbt::ClientState::CONNECTING); + } +} + void BLEClientBase::log_error_(const char *message) { ESP_LOGE(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_.c_str(), message); } @@ -243,17 +229,30 @@ void BLEClientBase::log_warning_(const char *message) { ESP_LOGW(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_.c_str(), message); } -void BLEClientBase::restore_medium_conn_params_() { - // Restore to medium connection parameters after initial connection phase - // This balances performance with bandwidth usage for normal operation +void BLEClientBase::update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, + uint16_t timeout, const char *param_type) { esp_ble_conn_update_params_t conn_params = {{0}}; memcpy(conn_params.bda, this->remote_bda_, sizeof(esp_bd_addr_t)); - conn_params.min_int = MEDIUM_MIN_CONN_INTERVAL; - conn_params.max_int = MEDIUM_MAX_CONN_INTERVAL; - conn_params.latency = 0; - conn_params.timeout = MEDIUM_CONN_TIMEOUT; - this->log_connection_params_("medium"); - esp_ble_gap_update_conn_params(&conn_params); + conn_params.min_int = min_interval; + conn_params.max_int = max_interval; + conn_params.latency = latency; + conn_params.timeout = timeout; + this->log_connection_params_(param_type); + esp_err_t err = esp_ble_gap_update_conn_params(&conn_params); + if (err != ESP_OK) { + this->log_gattc_warning_("esp_ble_gap_update_conn_params", err); + } +} + +void BLEClientBase::set_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout, + const char *param_type) { + // Set preferred connection parameters before connecting + // These will be used when establishing the connection + this->log_connection_params_(param_type); + esp_err_t err = esp_ble_gap_set_prefer_conn_params(this->remote_bda_, min_interval, max_interval, latency, timeout); + if (err != ESP_OK) { + this->log_gattc_warning_("esp_ble_gap_set_prefer_conn_params", err); + } } bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t esp_gattc_if, @@ -308,12 +307,13 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ this->set_state(espbt::ClientState::CONNECTED); ESP_LOGI(TAG, "[%d] [%s] Connection open", this->connection_index_, this->address_str_.c_str()); if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) { - // Restore to medium connection parameters for cached connections too - this->restore_medium_conn_params_(); + // Cached connections already connected with medium parameters, no update needed // only set our state, subclients might have more stuff to do yet. this->state_ = espbt::ClientState::ESTABLISHED; break; } + // For V3_WITHOUT_CACHE, we already set fast params before connecting + // No need to update them again here this->log_event_("Searching for services"); esp_ble_gattc_search_service(esp_gattc_if, param->cfg_mtu.conn_id, nullptr); break; @@ -395,12 +395,11 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ if (this->conn_id_ != param->search_cmpl.conn_id) return false; this->log_gattc_event_("SEARCH_CMPL"); - // For V3 connections, restore to medium connection parameters after service discovery + // For V3_WITHOUT_CACHE, switch back to medium connection parameters after service discovery // This balances performance with bandwidth usage after the critical discovery phase - if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE || - this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) { - this->restore_medium_conn_params_(); - } else { + if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) { + this->update_conn_params_(MEDIUM_MIN_CONN_INTERVAL, MEDIUM_MAX_CONN_INTERVAL, 0, MEDIUM_CONN_TIMEOUT, "medium"); + } else if (this->connection_type_ != espbt::ConnectionType::V3_WITH_CACHE) { #ifdef USE_ESP32_BLE_DEVICE for (auto &svc : this->services_) { ESP_LOGV(TAG, "[%d] [%s] Service UUID: %s", this->connection_index_, this->address_str_.c_str(), diff --git a/esphome/components/esp32_ble_client/ble_client_base.h b/esphome/components/esp32_ble_client/ble_client_base.h index 1850b2c5b3..acfad9e9b0 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.h +++ b/esphome/components/esp32_ble_client/ble_client_base.h @@ -133,10 +133,14 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { void log_event_(const char *name); void log_gattc_event_(const char *name); - void restore_medium_conn_params_(); + void update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout, + const char *param_type); + void set_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout, + const char *param_type); void log_gattc_warning_(const char *operation, esp_gatt_status_t status); void log_gattc_warning_(const char *operation, esp_err_t err); void log_connection_params_(const char *param_type); + void handle_connection_result_(esp_err_t ret); // Compact error logging helpers to reduce flash usage void log_error_(const char *message); void log_error_(const char *message, int code); From 4396bc0d1acbba2e6414c6f4ae16febe8b471307 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 24 Aug 2025 22:49:37 +0200 Subject: [PATCH 071/208] [esp32_ble] Increase GATT connection retry count to use full timeout window (#10376) --- esphome/components/esp32_ble/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index 2edd69c6c0..cc06058b65 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -280,6 +280,10 @@ async def to_code(config): add_idf_sdkconfig_option( "CONFIG_BT_BLE_ESTAB_LINK_CONN_TOUT", timeout_seconds ) + # Increase GATT client connection retry count for problematic devices + # Default in ESP-IDF is 3, we increase to 10 for better reliability with + # low-power/timing-sensitive devices + add_idf_sdkconfig_option("CONFIG_BT_GATTC_CONNECT_RETRY_COUNT", 10) # Set the maximum number of notification registrations # This controls how many BLE characteristics can have notifications enabled From 61a50238880937c0f7b13e18a6f06ad5aebcb38a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 24 Aug 2025 22:49:52 +0200 Subject: [PATCH 072/208] [script] Fix parallel mode scripts with delays cancelling each other (#10324) --- esphome/core/base_automation.h | 13 +++- esphome/core/scheduler.cpp | 19 +++-- esphome/core/scheduler.h | 9 ++- .../fixtures/parallel_script_delays.yaml | 45 ++++++++++++ tests/integration/test_automations.py | 70 +++++++++++++++++++ 5 files changed, 147 insertions(+), 9 deletions(-) create mode 100644 tests/integration/fixtures/parallel_script_delays.yaml diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index 740e10700b..ba942e5e43 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -5,6 +5,8 @@ #include "esphome/core/hal.h" #include "esphome/core/defines.h" #include "esphome/core/preferences.h" +#include "esphome/core/scheduler.h" +#include "esphome/core/application.h" #include @@ -158,7 +160,16 @@ template class DelayAction : public Action, public Compon void play_complex(Ts... x) override { auto f = std::bind(&DelayAction::play_next_, this, x...); this->num_running_++; - this->set_timeout("delay", this->delay_.value(x...), f); + + // If num_running_ > 1, we have multiple instances running in parallel + // In single/restart/queued modes, only one instance runs at a time + // Parallel mode uses skip_cancel=true to allow multiple delays to coexist + // WARNING: This can accumulate delays if scripts are triggered faster than they complete! + // Users should set max_runs on parallel scripts to limit concurrent executions. + // Issue #10264: This is a workaround for parallel script delays interfering with each other. + App.scheduler.set_timer_common_(this, Scheduler::SchedulerItem::TIMEOUT, + /* is_static_string= */ true, "delay", this->delay_.value(x...), std::move(f), + /* is_retry= */ false, /* skip_cancel= */ this->num_running_ > 1); } float get_setup_priority() const override { return setup_priority::HARDWARE; } diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index c3ade260ac..a907b89b02 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -65,14 +65,17 @@ static void validate_static_string(const char *name) { // Common implementation for both timeout and interval void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type type, bool is_static_string, - const void *name_ptr, uint32_t delay, std::function func, bool is_retry) { + const void *name_ptr, uint32_t delay, std::function func, bool is_retry, + bool skip_cancel) { // Get the name as const char* const char *name_cstr = this->get_name_cstr_(is_static_string, name_ptr); if (delay == SCHEDULER_DONT_RUN) { // Still need to cancel existing timer if name is not empty - LockGuard guard{this->lock_}; - this->cancel_item_locked_(component, name_cstr, type); + if (!skip_cancel) { + LockGuard guard{this->lock_}; + this->cancel_item_locked_(component, name_cstr, type); + } return; } @@ -97,7 +100,9 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type if (delay == 0 && type == SchedulerItem::TIMEOUT) { // Put in defer queue for guaranteed FIFO execution LockGuard guard{this->lock_}; - this->cancel_item_locked_(component, name_cstr, type); + if (!skip_cancel) { + this->cancel_item_locked_(component, name_cstr, type); + } this->defer_queue_.push_back(std::move(item)); return; } @@ -150,9 +155,11 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type return; } - // If name is provided, do atomic cancel-and-add + // If name is provided, do atomic cancel-and-add (unless skip_cancel is true) // Cancel existing items - this->cancel_item_locked_(component, name_cstr, type); + if (!skip_cancel) { + this->cancel_item_locked_(component, name_cstr, type); + } // Add new item directly to to_add_ // since we have the lock held this->to_add_.push_back(std::move(item)); diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index c73bd55d5d..f469a60d5c 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -21,8 +21,13 @@ struct RetryArgs; void retry_handler(const std::shared_ptr &args); class Scheduler { - // Allow retry_handler to access protected members + // Allow retry_handler to access protected members for internal retry mechanism friend void ::esphome::retry_handler(const std::shared_ptr &args); + // Allow DelayAction to call set_timer_common_ with skip_cancel=true for parallel script delays. + // This is needed to fix issue #10264 where parallel scripts with delays interfere with each other. + // We use friend instead of a public API because skip_cancel is dangerous - it can cause delays + // to accumulate and overload the scheduler if misused. + template friend class DelayAction; public: // Public API - accepts std::string for backward compatibility @@ -184,7 +189,7 @@ class Scheduler { // Common implementation for both timeout and interval void set_timer_common_(Component *component, SchedulerItem::Type type, bool is_static_string, const void *name_ptr, - uint32_t delay, std::function func, bool is_retry = false); + uint32_t delay, std::function func, bool is_retry = false, bool skip_cancel = false); // Common implementation for retry void set_retry_common_(Component *component, bool is_static_string, const void *name_ptr, uint32_t initial_wait_time, diff --git a/tests/integration/fixtures/parallel_script_delays.yaml b/tests/integration/fixtures/parallel_script_delays.yaml new file mode 100644 index 0000000000..6887045913 --- /dev/null +++ b/tests/integration/fixtures/parallel_script_delays.yaml @@ -0,0 +1,45 @@ +esphome: + name: test-parallel-delays + +host: + +logger: + level: DEBUG + +api: + actions: + - action: test_parallel_delays + then: + # Start three parallel script instances with small delays between starts + - globals.set: + id: instance_counter + value: '1' + - script.execute: parallel_delay_script + - delay: 10ms + - globals.set: + id: instance_counter + value: '2' + - script.execute: parallel_delay_script + - delay: 10ms + - globals.set: + id: instance_counter + value: '3' + - script.execute: parallel_delay_script + +globals: + - id: instance_counter + type: int + initial_value: '0' + +script: + - id: parallel_delay_script + mode: parallel + then: + - lambda: !lambda |- + int instance = id(instance_counter); + ESP_LOGI("TEST", "Parallel script instance %d started", instance); + - delay: 1s + - lambda: !lambda |- + static int completed_counter = 0; + completed_counter++; + ESP_LOGI("TEST", "Parallel script instance %d completed after delay", completed_counter); diff --git a/tests/integration/test_automations.py b/tests/integration/test_automations.py index bd2082e86b..83268c1eea 100644 --- a/tests/integration/test_automations.py +++ b/tests/integration/test_automations.py @@ -89,3 +89,73 @@ async def test_delay_action_cancellation( assert 0.4 < time_from_second_start < 0.6, ( f"Delay completed {time_from_second_start:.3f}s after second start, expected ~0.5s" ) + + +@pytest.mark.asyncio +async def test_parallel_script_delays( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that parallel scripts with delays don't interfere with each other.""" + loop = asyncio.get_running_loop() + + # Track script executions + script_starts: list[float] = [] + script_ends: list[float] = [] + + # Patterns to match + start_pattern = re.compile(r"Parallel script instance \d+ started") + end_pattern = re.compile(r"Parallel script instance \d+ completed after delay") + + # Future to track when all scripts have completed + all_scripts_completed = loop.create_future() + + def check_output(line: str) -> None: + """Check log output for parallel script messages.""" + current_time = loop.time() + + if start_pattern.search(line): + script_starts.append(current_time) + + if end_pattern.search(line): + script_ends.append(current_time) + # Check if we have all 3 completions + if len(script_ends) == 3 and not all_scripts_completed.done(): + all_scripts_completed.set_result(True) + + async with ( + run_compiled(yaml_config, line_callback=check_output), + api_client_connected() as client, + ): + # Get services + entities, services = await client.list_entities_services() + + # Find our test service + test_service = next( + (s for s in services if s.name == "test_parallel_delays"), None + ) + assert test_service is not None, "test_parallel_delays service not found" + + # Execute the test - this will start 3 parallel scripts with 1 second delays + client.execute_service(test_service, {}) + + # Wait for all scripts to complete (should take ~1 second, not 3) + await asyncio.wait_for(all_scripts_completed, timeout=2.0) + + # Verify we had 3 starts and 3 ends + assert len(script_starts) == 3, ( + f"Expected 3 script starts, got {len(script_starts)}" + ) + assert len(script_ends) == 3, f"Expected 3 script ends, got {len(script_ends)}" + + # Verify they ran in parallel - all should complete within ~1.5 seconds + first_start = min(script_starts) + last_end = max(script_ends) + total_time = last_end - first_start + + # If running in parallel, total time should be close to 1 second + # If they were interfering (running sequentially), it would take 3+ seconds + assert total_time < 1.5, ( + f"Parallel scripts took {total_time:.2f}s total, should be ~1s if running in parallel" + ) From b41a61c76e9fec271f8211be5c5cfdd3276728c1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 24 Aug 2025 22:51:23 +0200 Subject: [PATCH 073/208] [deep_sleep] Fix ESP32-C6 compilation error with gpio_deep_sleep_hold_en() (#10345) --- esphome/components/deep_sleep/deep_sleep_esp32.cpp | 9 +++++++++ tests/components/deep_sleep/test.esp32-c6-idf.yaml | 5 +++++ tests/components/deep_sleep/test.esp32-s2-idf.yaml | 5 +++++ tests/components/deep_sleep/test.esp32-s3-idf.yaml | 5 +++++ 4 files changed, 24 insertions(+) create mode 100644 tests/components/deep_sleep/test.esp32-c6-idf.yaml create mode 100644 tests/components/deep_sleep/test.esp32-s2-idf.yaml create mode 100644 tests/components/deep_sleep/test.esp32-s3-idf.yaml diff --git a/esphome/components/deep_sleep/deep_sleep_esp32.cpp b/esphome/components/deep_sleep/deep_sleep_esp32.cpp index c5c1fe0835..e9d0a4981f 100644 --- a/esphome/components/deep_sleep/deep_sleep_esp32.cpp +++ b/esphome/components/deep_sleep/deep_sleep_esp32.cpp @@ -1,4 +1,5 @@ #ifdef USE_ESP32 +#include "soc/soc_caps.h" #include "driver/gpio.h" #include "deep_sleep_component.h" #include "esphome/core/log.h" @@ -83,7 +84,11 @@ void DeepSleepComponent::deep_sleep_() { } gpio_sleep_set_direction(gpio_pin, GPIO_MODE_INPUT); gpio_hold_en(gpio_pin); +#if !SOC_GPIO_SUPPORT_HOLD_SINGLE_IO_IN_DSLP + // Some ESP32 variants support holding a single GPIO during deep sleep without this function + // For those variants, gpio_hold_en() is sufficient to hold the pin state during deep sleep gpio_deep_sleep_hold_en(); +#endif bool level = !this->wakeup_pin_->is_inverted(); if (this->wakeup_pin_mode_ == WAKEUP_PIN_MODE_INVERT_WAKEUP && this->wakeup_pin_->digital_read()) { level = !level; @@ -120,7 +125,11 @@ void DeepSleepComponent::deep_sleep_() { } gpio_sleep_set_direction(gpio_pin, GPIO_MODE_INPUT); gpio_hold_en(gpio_pin); +#if !SOC_GPIO_SUPPORT_HOLD_SINGLE_IO_IN_DSLP + // Some ESP32 variants support holding a single GPIO during deep sleep without this function + // For those variants, gpio_hold_en() is sufficient to hold the pin state during deep sleep gpio_deep_sleep_hold_en(); +#endif bool level = !this->wakeup_pin_->is_inverted(); if (this->wakeup_pin_mode_ == WAKEUP_PIN_MODE_INVERT_WAKEUP && this->wakeup_pin_->digital_read()) { level = !level; diff --git a/tests/components/deep_sleep/test.esp32-c6-idf.yaml b/tests/components/deep_sleep/test.esp32-c6-idf.yaml new file mode 100644 index 0000000000..10c17af0f5 --- /dev/null +++ b/tests/components/deep_sleep/test.esp32-c6-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + wakeup_pin: GPIO4 + +<<: !include common.yaml +<<: !include common-esp32.yaml diff --git a/tests/components/deep_sleep/test.esp32-s2-idf.yaml b/tests/components/deep_sleep/test.esp32-s2-idf.yaml new file mode 100644 index 0000000000..10c17af0f5 --- /dev/null +++ b/tests/components/deep_sleep/test.esp32-s2-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + wakeup_pin: GPIO4 + +<<: !include common.yaml +<<: !include common-esp32.yaml diff --git a/tests/components/deep_sleep/test.esp32-s3-idf.yaml b/tests/components/deep_sleep/test.esp32-s3-idf.yaml new file mode 100644 index 0000000000..10c17af0f5 --- /dev/null +++ b/tests/components/deep_sleep/test.esp32-s3-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + wakeup_pin: GPIO4 + +<<: !include common.yaml +<<: !include common-esp32.yaml From 8fe582309e18b019136c1f5d2347935d2fe53de9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 24 Aug 2025 22:51:54 +0200 Subject: [PATCH 074/208] [esp32_ble_client] Reduce log level for harmless BLE timeout race conditions (#10339) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../esp32_ble_client/ble_client_base.cpp | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/esphome/components/esp32_ble_client/ble_client_base.cpp b/esphome/components/esp32_ble_client/ble_client_base.cpp index b9b7c75e0a..351658279f 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.cpp +++ b/esphome/components/esp32_ble_client/ble_client_base.cpp @@ -136,7 +136,8 @@ void BLEClientBase::disconnect() { return; } if (this->state_ == espbt::ClientState::CONNECTING || this->conn_id_ == UNSET_CONN_ID) { - this->log_warning_("Disconnect before connected, disconnect scheduled."); + ESP_LOGD(TAG, "[%d] [%s] Disconnect before connected, disconnect scheduled", this->connection_index_, + this->address_str_.c_str()); this->want_disconnect_ = true; return; } @@ -284,11 +285,22 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ this->log_gattc_event_("OPEN"); // conn_id was already set in ESP_GATTC_CONNECT_EVT this->service_count_ = 0; + + // ESP-IDF's BLE stack may send ESP_GATTC_OPEN_EVT after esp_ble_gattc_open() returns an + // error, if the error occurred at the BTA/GATT layer. This can result in the event + // arriving after we've already transitioned to IDLE state. + if (this->state_ == espbt::ClientState::IDLE) { + ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_OPEN_EVT in IDLE state (status=%d), ignoring", this->connection_index_, + this->address_str_.c_str(), param->open.status); + break; + } + if (this->state_ != espbt::ClientState::CONNECTING) { // This should not happen but lets log it in case it does // because it means we have a bad assumption about how the // ESP BT stack works. - this->log_error_("ESP_GATTC_OPEN_EVT wrong state status", param->open.status); + ESP_LOGE(TAG, "[%d] [%s] ESP_GATTC_OPEN_EVT in %s state (status=%d)", this->connection_index_, + this->address_str_.c_str(), espbt::client_state_to_string(this->state_), param->open.status); } if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) { this->log_gattc_warning_("Connection open", param->open.status); From a58c3950bc7f055eb91f61655087fc6eb5f19ad0 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 25 Aug 2025 06:52:37 +1000 Subject: [PATCH 075/208] [lvgl] Fix meter rotation (#10342) --- esphome/components/lvgl/widgets/canvas.py | 10 +++++----- esphome/components/lvgl/widgets/meter.py | 9 +++------ 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/esphome/components/lvgl/widgets/canvas.py b/esphome/components/lvgl/widgets/canvas.py index 4fd81b6e4a..217e8935f1 100644 --- a/esphome/components/lvgl/widgets/canvas.py +++ b/esphome/components/lvgl/widgets/canvas.py @@ -24,7 +24,7 @@ from ..defines import ( literal, ) from ..lv_validation import ( - lv_angle, + lv_angle_degrees, lv_bool, lv_color, lv_image, @@ -395,15 +395,15 @@ ARC_PROPS = { DRAW_OPA_SCHEMA.extend( { cv.Required(CONF_RADIUS): pixels, - cv.Required(CONF_START_ANGLE): lv_angle, - cv.Required(CONF_END_ANGLE): lv_angle, + cv.Required(CONF_START_ANGLE): lv_angle_degrees, + cv.Required(CONF_END_ANGLE): lv_angle_degrees, } ).extend({cv.Optional(prop): validator for prop, validator in ARC_PROPS.items()}), ) async def canvas_draw_arc(config, action_id, template_arg, args): radius = await size.process(config[CONF_RADIUS]) - start_angle = await lv_angle.process(config[CONF_START_ANGLE]) - end_angle = await lv_angle.process(config[CONF_END_ANGLE]) + start_angle = await lv_angle_degrees.process(config[CONF_START_ANGLE]) + end_angle = await lv_angle_degrees.process(config[CONF_END_ANGLE]) async def do_draw_arc(w: Widget, x, y, dsc_addr): lv.canvas_draw_arc(w.obj, x, y, radius, start_angle, end_angle, dsc_addr) diff --git a/esphome/components/lvgl/widgets/meter.py b/esphome/components/lvgl/widgets/meter.py index acec986f99..aefda0e71a 100644 --- a/esphome/components/lvgl/widgets/meter.py +++ b/esphome/components/lvgl/widgets/meter.py @@ -14,7 +14,6 @@ from esphome.const import ( CONF_VALUE, CONF_WIDTH, ) -from esphome.cpp_generator import IntLiteral from ..automation import action_to_code from ..defines import ( @@ -32,7 +31,7 @@ from ..helpers import add_lv_use, lvgl_components_required from ..lv_validation import ( get_end_value, get_start_value, - lv_angle, + lv_angle_degrees, lv_bool, lv_color, lv_float, @@ -163,7 +162,7 @@ SCALE_SCHEMA = cv.Schema( cv.Optional(CONF_RANGE_FROM, default=0.0): cv.float_, cv.Optional(CONF_RANGE_TO, default=100.0): cv.float_, cv.Optional(CONF_ANGLE_RANGE, default=270): cv.int_range(0, 360), - cv.Optional(CONF_ROTATION): lv_angle, + cv.Optional(CONF_ROTATION): lv_angle_degrees, cv.Optional(CONF_INDICATORS): cv.ensure_list(INDICATOR_SCHEMA), } ) @@ -188,9 +187,7 @@ class MeterType(WidgetType): for scale_conf in config.get(CONF_SCALES, ()): rotation = 90 + (360 - scale_conf[CONF_ANGLE_RANGE]) / 2 if CONF_ROTATION in scale_conf: - rotation = await lv_angle.process(scale_conf[CONF_ROTATION]) - if isinstance(rotation, IntLiteral): - rotation = int(str(rotation)) // 10 + rotation = await lv_angle_degrees.process(scale_conf[CONF_ROTATION]) with LocalVariable( "meter_var", "lv_meter_scale_t", lv_expr.meter_add_scale(var) ) as meter_var: From 07bca6103f24bf0e9b3715db21638f345bced996 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 24 Aug 2025 22:57:09 +0200 Subject: [PATCH 076/208] [esp32_ble_tracker] Fix on_scan_end trigger compilation without USE_ESP32_BLE_DEVICE (#10399) --- esphome/components/esp32_ble_tracker/automation.h | 5 ++++- .../esp32_ble_tracker/test-on-scan-end.esp32-idf.yaml | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 tests/components/esp32_ble_tracker/test-on-scan-end.esp32-idf.yaml diff --git a/esphome/components/esp32_ble_tracker/automation.h b/esphome/components/esp32_ble_tracker/automation.h index c0e6eee138..784f2eaaa2 100644 --- a/esphome/components/esp32_ble_tracker/automation.h +++ b/esphome/components/esp32_ble_tracker/automation.h @@ -80,14 +80,17 @@ class BLEManufacturerDataAdvertiseTrigger : public Trigger, ESPBTUUID uuid_; }; +#endif // USE_ESP32_BLE_DEVICE + class BLEEndOfScanTrigger : public Trigger<>, public ESPBTDeviceListener { public: explicit BLEEndOfScanTrigger(ESP32BLETracker *parent) { parent->register_listener(this); } +#ifdef USE_ESP32_BLE_DEVICE bool parse_device(const ESPBTDevice &device) override { return false; } +#endif void on_scan_end() override { this->trigger(); } }; -#endif // USE_ESP32_BLE_DEVICE template class ESP32BLEStartScanAction : public Action { public: diff --git a/tests/components/esp32_ble_tracker/test-on-scan-end.esp32-idf.yaml b/tests/components/esp32_ble_tracker/test-on-scan-end.esp32-idf.yaml new file mode 100644 index 0000000000..4e9849a540 --- /dev/null +++ b/tests/components/esp32_ble_tracker/test-on-scan-end.esp32-idf.yaml @@ -0,0 +1,3 @@ +esp32_ble_tracker: + on_scan_end: + - logger.log: "Scan ended!" From 9f0257528794ac60d3d9f3eb353f83b69845e30f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 24 Aug 2025 22:58:46 +0200 Subject: [PATCH 077/208] [test] Add integration test for light effect memory corruption fix (#10417) --- tests/integration/fixtures/light_calls.yaml | 32 ++++++++++++++- tests/integration/test_light_calls.py | 45 +++++++++++++++++++-- 2 files changed, 72 insertions(+), 5 deletions(-) diff --git a/tests/integration/fixtures/light_calls.yaml b/tests/integration/fixtures/light_calls.yaml index d692a11765..2b7650526f 100644 --- a/tests/integration/fixtures/light_calls.yaml +++ b/tests/integration/fixtures/light_calls.yaml @@ -56,10 +56,29 @@ light: warm_white_color_temperature: 2000 K constant_brightness: true effects: + # Use default parameters: - random: - name: "Random Effect" + # Customize parameters - use longer names to potentially trigger buffer issues + - random: + name: "My Very Slow Random Effect With Long Name" + transition_length: 30ms + update_interval: 30ms + - random: + name: "My Fast Random Effect That Changes Quickly" + transition_length: 4ms + update_interval: 5ms + - random: + name: "Random Effect With Medium Length Name Here" transition_length: 100ms update_interval: 200ms + - random: + name: "Another Random Effect With Different Parameters" + transition_length: 2ms + update_interval: 3ms + - random: + name: "Yet Another Random Effect To Test Memory" + transition_length: 15ms + update_interval: 20ms - strobe: name: "Strobe Effect" - pulse: @@ -73,6 +92,17 @@ light: red: test_red green: test_green blue: test_blue + effects: + # Same random effects to test for cross-contamination + - random: + - random: + name: "RGB Slow Random" + transition_length: 20ms + update_interval: 25ms + - random: + name: "RGB Fast Random" + transition_length: 2ms + update_interval: 3ms - platform: binary name: "Test Binary Light" diff --git a/tests/integration/test_light_calls.py b/tests/integration/test_light_calls.py index 1a0a9e553f..af90ddbe86 100644 --- a/tests/integration/test_light_calls.py +++ b/tests/integration/test_light_calls.py @@ -108,14 +108,51 @@ async def test_light_calls( # Wait for flash to end state = await wait_for_state_change(rgbcw_light.key) - # Test 13: effect only + # Test 13: effect only - test all random effects # First ensure light is on client.light_command(key=rgbcw_light.key, state=True) state = await wait_for_state_change(rgbcw_light.key) - # Now set effect - client.light_command(key=rgbcw_light.key, effect="Random Effect") + + # Test 13a: Default random effect (no name, gets default name "Random") + client.light_command(key=rgbcw_light.key, effect="Random") state = await wait_for_state_change(rgbcw_light.key) - assert state.effect == "Random Effect" + assert state.effect == "Random" + + # Test 13b: Slow random effect with long name + client.light_command( + key=rgbcw_light.key, effect="My Very Slow Random Effect With Long Name" + ) + state = await wait_for_state_change(rgbcw_light.key) + assert state.effect == "My Very Slow Random Effect With Long Name" + + # Test 13c: Fast random effect with long name + client.light_command( + key=rgbcw_light.key, effect="My Fast Random Effect That Changes Quickly" + ) + state = await wait_for_state_change(rgbcw_light.key) + assert state.effect == "My Fast Random Effect That Changes Quickly" + + # Test 13d: Random effect with medium length name + client.light_command( + key=rgbcw_light.key, effect="Random Effect With Medium Length Name Here" + ) + state = await wait_for_state_change(rgbcw_light.key) + assert state.effect == "Random Effect With Medium Length Name Here" + + # Test 13e: Another random effect + client.light_command( + key=rgbcw_light.key, + effect="Another Random Effect With Different Parameters", + ) + state = await wait_for_state_change(rgbcw_light.key) + assert state.effect == "Another Random Effect With Different Parameters" + + # Test 13f: Yet another random effect + client.light_command( + key=rgbcw_light.key, effect="Yet Another Random Effect To Test Memory" + ) + state = await wait_for_state_change(rgbcw_light.key) + assert state.effect == "Yet Another Random Effect To Test Memory" # Test 14: stop effect client.light_command(key=rgbcw_light.key, effect="None") From 456c31262d558c74100ca2481c70bb7f2e64b220 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 25 Aug 2025 07:04:32 +1000 Subject: [PATCH 078/208] [web_server] Use oi.esphome.io for css and js assets (#10296) --- esphome/components/web_server/__init__.py | 4 ++-- esphome/components/web_server/web_server.h | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index 695757e137..be193bbab8 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -52,9 +52,9 @@ def default_url(config: ConfigType) -> ConfigType: config = config.copy() if config[CONF_VERSION] == 1: if CONF_CSS_URL not in config: - config[CONF_CSS_URL] = "https://esphome.io/_static/webserver-v1.min.css" + config[CONF_CSS_URL] = "https://oi.esphome.io/v1/webserver-v1.min.css" if CONF_JS_URL not in config: - config[CONF_JS_URL] = "https://esphome.io/_static/webserver-v1.min.js" + config[CONF_JS_URL] = "https://oi.esphome.io/v1/webserver-v1.min.js" if config[CONF_VERSION] == 2: if CONF_CSS_URL not in config: config[CONF_CSS_URL] = "" diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 6bece732fc..e42c35b32d 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -173,14 +173,14 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { #if USE_WEBSERVER_VERSION == 1 /** Set the URL to the CSS that's sent to each client. Defaults to - * https://esphome.io/_static/webserver-v1.min.css + * https://oi.esphome.io/v1/webserver-v1.min.css * * @param css_url The url to the web server stylesheet. */ void set_css_url(const char *css_url); /** Set the URL to the script that's embedded in the index page. Defaults to - * https://esphome.io/_static/webserver-v1.min.js + * https://oi.esphome.io/v1/webserver-v1.min.js * * @param js_url The url to the web server script. */ From ecfeb8e4d320f1f63e222caa36e593dcfe5b4c0e Mon Sep 17 00:00:00 2001 From: Thomas Rupprecht Date: Mon, 25 Aug 2025 01:51:28 +0200 Subject: [PATCH 079/208] improve AI instructions (#10416) --- .ai/instructions.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.ai/instructions.md b/.ai/instructions.md index 4d6cd71f85..1cd77b9136 100644 --- a/.ai/instructions.md +++ b/.ai/instructions.md @@ -38,7 +38,7 @@ This document provides essential context for AI models interacting with this pro 5. **Dashboard** (`esphome/dashboard/`): A web-based interface for device configuration, management, and OTA updates. * **Platform Support:** - 1. **ESP32** (`components/esp32/`): Espressif ESP32 family. Supports multiple variants (S2, S3, C3, etc.) and both IDF and Arduino frameworks. + 1. **ESP32** (`components/esp32/`): Espressif ESP32 family. Supports multiple variants (Original, C2, C3, C5, C6, H2, P4, S2, S3) with ESP-IDF framework. Arduino framework supports only a subset of the variants (Original, C3, S2, S3). 2. **ESP8266** (`components/esp8266/`): Espressif ESP8266. Arduino framework only, with memory constraints. 3. **RP2040** (`components/rp2040/`): Raspberry Pi Pico/RP2040. Arduino framework with PIO (Programmable I/O) support. 4. **LibreTiny** (`components/libretiny/`): Realtek and Beken chips. Supports multiple chip families and auto-generated components. @@ -60,7 +60,7 @@ This document provides essential context for AI models interacting with this pro ├── __init__.py # Component configuration schema and code generation ├── [component].h # C++ header file (if needed) ├── [component].cpp # C++ implementation (if needed) - └── [platform]/ # Platform-specific implementations + └── [platform]/ # Platform-specific implementations ├── __init__.py # Platform-specific configuration ├── [platform].h # Platform C++ header └── [platform].cpp # Platform C++ implementation @@ -150,7 +150,8 @@ This document provides essential context for AI models interacting with this pro * **Configuration Validation:** * **Common Validators:** `cv.int_`, `cv.float_`, `cv.string`, `cv.boolean`, `cv.int_range(min=0, max=100)`, `cv.positive_int`, `cv.percentage`. * **Complex Validation:** `cv.All(cv.string, cv.Length(min=1, max=50))`, `cv.Any(cv.int_, cv.string)`. - * **Platform-Specific:** `cv.only_on(["esp32", "esp8266"])`, `cv.only_with_arduino`. + * **Platform-Specific:** `cv.only_on(["esp32", "esp8266"])`, `esp32.only_on_variant(...)`, `cv.only_on_esp32`, `cv.only_on_esp8266`, `cv.only_on_rp2040`. + * **Framework-Specific:** `cv.only_with_framework(...)`, `cv.only_with_arduino`, `cv.only_with_esp_idf`. * **Schema Extensions:** ```python CONFIG_SCHEMA = cv.Schema({ ... }) From 6004367ee29942a74164db5f1d7487d5d6198271 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 25 Aug 2025 02:07:04 +0200 Subject: [PATCH 080/208] [esp32_ble_client] Add missing ESP_GATTC_UNREG_FOR_NOTIFY_EVT logging (#10347) --- esphome/components/esp32_ble_client/ble_client_base.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/esphome/components/esp32_ble_client/ble_client_base.cpp b/esphome/components/esp32_ble_client/ble_client_base.cpp index 351658279f..4805855960 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.cpp +++ b/esphome/components/esp32_ble_client/ble_client_base.cpp @@ -495,6 +495,11 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_ break; } + case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: { + this->log_gattc_event_("UNREG_FOR_NOTIFY"); + break; + } + default: // ideally would check all other events for matching conn_id ESP_LOGD(TAG, "[%d] [%s] Event %d", this->connection_index_, this->address_str_.c_str(), event); From d2752b38c963f42336e0b48e104c4613e890fc99 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 25 Aug 2025 02:22:16 +0200 Subject: [PATCH 081/208] [core] Fix preference storage to account for device_id (#10333) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/climate/climate.cpp | 2 +- esphome/components/cover/cover.cpp | 2 +- .../components/duty_time/duty_time_sensor.cpp | 2 +- esphome/components/fan/fan.cpp | 3 +- esphome/components/haier/haier_base.cpp | 2 +- esphome/components/haier/hon_climate.cpp | 2 +- .../integration/integration_sensor.cpp | 2 +- esphome/components/ld2450/ld2450.cpp | 2 +- esphome/components/light/light_state.cpp | 4 +- esphome/components/lvgl/number/lvgl_number.h | 2 +- esphome/components/lvgl/select/lvgl_select.h | 2 +- esphome/components/number/automation.cpp | 2 +- .../opentherm/number/opentherm_number.cpp | 2 +- .../rotary_encoder/rotary_encoder.cpp | 2 +- esphome/components/sensor/automation.h | 2 +- .../media_player/speaker_media_player.cpp | 2 +- esphome/components/sprinkler/sprinkler.cpp | 2 +- esphome/components/switch/switch.cpp | 2 +- .../template_alarm_control_panel.cpp | 2 +- .../template/datetime/template_date.cpp | 2 +- .../template/datetime/template_datetime.cpp | 4 +- .../template/datetime/template_time.cpp | 2 +- .../template/number/template_number.cpp | 2 +- .../template/select/template_select.cpp | 2 +- .../template/text/template_text.cpp | 2 +- .../total_daily_energy/total_daily_energy.cpp | 2 +- .../components/tuya/number/tuya_number.cpp | 2 +- esphome/components/valve/valve.cpp | 2 +- esphome/core/entity_base.h | 29 +++ .../fixtures/multi_device_preferences.yaml | 165 ++++++++++++++++++ .../test_multi_device_preferences.py | 144 +++++++++++++++ 31 files changed, 369 insertions(+), 30 deletions(-) create mode 100644 tests/integration/fixtures/multi_device_preferences.yaml create mode 100644 tests/integration/test_multi_device_preferences.py diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index edebc0de69..be56310b35 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -327,7 +327,7 @@ void Climate::add_on_control_callback(std::function &&callb static const uint32_t RESTORE_STATE_VERSION = 0x848EA6ADUL; optional Climate::restore_state_() { - this->rtc_ = global_preferences->make_preference(this->get_object_id_hash() ^ + this->rtc_ = global_preferences->make_preference(this->get_preference_hash() ^ RESTORE_STATE_VERSION); ClimateDeviceRestoreState recovered{}; if (!this->rtc_.load(&recovered)) diff --git a/esphome/components/cover/cover.cpp b/esphome/components/cover/cover.cpp index 68dfab111b..700bceec01 100644 --- a/esphome/components/cover/cover.cpp +++ b/esphome/components/cover/cover.cpp @@ -194,7 +194,7 @@ void Cover::publish_state(bool save) { } } optional Cover::restore_state_() { - this->rtc_ = global_preferences->make_preference(this->get_object_id_hash()); + this->rtc_ = global_preferences->make_preference(this->get_preference_hash()); CoverRestoreState recovered{}; if (!this->rtc_.load(&recovered)) return {}; diff --git a/esphome/components/duty_time/duty_time_sensor.cpp b/esphome/components/duty_time/duty_time_sensor.cpp index c7319f7c33..f77f1fcf53 100644 --- a/esphome/components/duty_time/duty_time_sensor.cpp +++ b/esphome/components/duty_time/duty_time_sensor.cpp @@ -41,7 +41,7 @@ void DutyTimeSensor::setup() { uint32_t seconds = 0; if (this->restore_) { - this->pref_ = global_preferences->make_preference(this->get_object_id_hash()); + this->pref_ = global_preferences->make_preference(this->get_preference_hash()); this->pref_.load(&seconds); } diff --git a/esphome/components/fan/fan.cpp b/esphome/components/fan/fan.cpp index 82fc5319e0..26065ed644 100644 --- a/esphome/components/fan/fan.cpp +++ b/esphome/components/fan/fan.cpp @@ -148,7 +148,8 @@ void Fan::publish_state() { constexpr uint32_t RESTORE_STATE_VERSION = 0x71700ABA; optional Fan::restore_state_() { FanRestoreState recovered{}; - this->rtc_ = global_preferences->make_preference(this->get_object_id_hash() ^ RESTORE_STATE_VERSION); + this->rtc_ = + global_preferences->make_preference(this->get_preference_hash() ^ RESTORE_STATE_VERSION); bool restored = this->rtc_.load(&recovered); switch (this->restore_mode_) { diff --git a/esphome/components/haier/haier_base.cpp b/esphome/components/haier/haier_base.cpp index 4f933b08e3..55a2454fca 100644 --- a/esphome/components/haier/haier_base.cpp +++ b/esphome/components/haier/haier_base.cpp @@ -351,7 +351,7 @@ ClimateTraits HaierClimateBase::traits() { return traits_; } void HaierClimateBase::initialization() { constexpr uint32_t restore_settings_version = 0xA77D21EF; this->base_rtc_ = - global_preferences->make_preference(this->get_object_id_hash() ^ restore_settings_version); + global_preferences->make_preference(this->get_preference_hash() ^ restore_settings_version); HaierBaseSettings recovered; if (!this->base_rtc_.load(&recovered)) { recovered = {false, true}; diff --git a/esphome/components/haier/hon_climate.cpp b/esphome/components/haier/hon_climate.cpp index fd2d6a5800..9614bb1e47 100644 --- a/esphome/components/haier/hon_climate.cpp +++ b/esphome/components/haier/hon_climate.cpp @@ -516,7 +516,7 @@ void HonClimate::initialization() { HaierClimateBase::initialization(); constexpr uint32_t restore_settings_version = 0x57EB59DDUL; this->hon_rtc_ = - global_preferences->make_preference(this->get_object_id_hash() ^ restore_settings_version); + global_preferences->make_preference(this->get_preference_hash() ^ restore_settings_version); HonSettings recovered; if (this->hon_rtc_.load(&recovered)) { this->settings_ = recovered; diff --git a/esphome/components/integration/integration_sensor.cpp b/esphome/components/integration/integration_sensor.cpp index c09778e79e..80c718dc8d 100644 --- a/esphome/components/integration/integration_sensor.cpp +++ b/esphome/components/integration/integration_sensor.cpp @@ -10,7 +10,7 @@ static const char *const TAG = "integration"; void IntegrationSensor::setup() { if (this->restore_) { - this->pref_ = global_preferences->make_preference(this->get_object_id_hash()); + this->pref_ = global_preferences->make_preference(this->get_preference_hash()); float preference_value = 0; this->pref_.load(&preference_value); this->result_ = preference_value; diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index b123d541d9..f30752e5a2 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -184,7 +184,7 @@ static inline bool validate_header_footer(const uint8_t *header_footer, const ui void LD2450Component::setup() { #ifdef USE_NUMBER if (this->presence_timeout_number_ != nullptr) { - this->pref_ = global_preferences->make_preference(this->presence_timeout_number_->get_object_id_hash()); + this->pref_ = global_preferences->make_preference(this->presence_timeout_number_->get_preference_hash()); this->set_presence_timeout(); } #endif diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp index 9e42b2f1e2..f18d5ba1de 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -41,7 +41,7 @@ void LightState::setup() { case LIGHT_RESTORE_DEFAULT_ON: case LIGHT_RESTORE_INVERTED_DEFAULT_OFF: case LIGHT_RESTORE_INVERTED_DEFAULT_ON: - this->rtc_ = global_preferences->make_preference(this->get_object_id_hash()); + this->rtc_ = global_preferences->make_preference(this->get_preference_hash()); // Attempt to load from preferences, else fall back to default values if (!this->rtc_.load(&recovered)) { recovered.state = (this->restore_mode_ == LIGHT_RESTORE_DEFAULT_ON || @@ -54,7 +54,7 @@ void LightState::setup() { break; case LIGHT_RESTORE_AND_OFF: case LIGHT_RESTORE_AND_ON: - this->rtc_ = global_preferences->make_preference(this->get_object_id_hash()); + this->rtc_ = global_preferences->make_preference(this->get_preference_hash()); this->rtc_.load(&recovered); recovered.state = (this->restore_mode_ == LIGHT_RESTORE_AND_ON); break; diff --git a/esphome/components/lvgl/number/lvgl_number.h b/esphome/components/lvgl/number/lvgl_number.h index 277494673b..7bc44c9e20 100644 --- a/esphome/components/lvgl/number/lvgl_number.h +++ b/esphome/components/lvgl/number/lvgl_number.h @@ -21,7 +21,7 @@ class LVGLNumber : public number::Number, public Component { void setup() override { float value = this->value_lambda_(); if (this->restore_) { - this->pref_ = global_preferences->make_preference(this->get_object_id_hash()); + this->pref_ = global_preferences->make_preference(this->get_preference_hash()); if (this->pref_.load(&value)) { this->control_lambda_(value); } diff --git a/esphome/components/lvgl/select/lvgl_select.h b/esphome/components/lvgl/select/lvgl_select.h index 5b43209a5f..a0e60295a6 100644 --- a/esphome/components/lvgl/select/lvgl_select.h +++ b/esphome/components/lvgl/select/lvgl_select.h @@ -20,7 +20,7 @@ class LVGLSelect : public select::Select, public Component { this->set_options_(); if (this->restore_) { size_t index; - this->pref_ = global_preferences->make_preference(this->get_object_id_hash()); + this->pref_ = global_preferences->make_preference(this->get_preference_hash()); if (this->pref_.load(&index)) this->widget_->set_selected_index(index, LV_ANIM_OFF); } diff --git a/esphome/components/number/automation.cpp b/esphome/components/number/automation.cpp index cadc6f54f6..bfc59d0465 100644 --- a/esphome/components/number/automation.cpp +++ b/esphome/components/number/automation.cpp @@ -15,7 +15,7 @@ void ValueRangeTrigger::setup() { float local_min = this->min_.value(0.0); float local_max = this->max_.value(0.0); convert hash = {.from = (local_max - local_min)}; - uint32_t myhash = hash.to ^ this->parent_->get_object_id_hash(); + uint32_t myhash = hash.to ^ this->parent_->get_preference_hash(); this->rtc_ = global_preferences->make_preference(myhash); bool initial_state; if (this->rtc_.load(&initial_state)) { diff --git a/esphome/components/opentherm/number/opentherm_number.cpp b/esphome/components/opentherm/number/opentherm_number.cpp index c5d094aa36..f0c69144c8 100644 --- a/esphome/components/opentherm/number/opentherm_number.cpp +++ b/esphome/components/opentherm/number/opentherm_number.cpp @@ -17,7 +17,7 @@ void OpenthermNumber::setup() { if (!this->restore_value_) { value = this->initial_value_; } else { - this->pref_ = global_preferences->make_preference(this->get_object_id_hash()); + this->pref_ = global_preferences->make_preference(this->get_preference_hash()); if (!this->pref_.load(&value)) { if (!std::isnan(this->initial_value_)) { value = this->initial_value_; diff --git a/esphome/components/rotary_encoder/rotary_encoder.cpp b/esphome/components/rotary_encoder/rotary_encoder.cpp index 20ea8d0293..26e20664f2 100644 --- a/esphome/components/rotary_encoder/rotary_encoder.cpp +++ b/esphome/components/rotary_encoder/rotary_encoder.cpp @@ -132,7 +132,7 @@ void RotaryEncoderSensor::setup() { int32_t initial_value = 0; switch (this->restore_mode_) { case ROTARY_ENCODER_RESTORE_DEFAULT_ZERO: - this->rtc_ = global_preferences->make_preference(this->get_object_id_hash()); + this->rtc_ = global_preferences->make_preference(this->get_preference_hash()); if (!this->rtc_.load(&initial_value)) { initial_value = 0; } diff --git a/esphome/components/sensor/automation.h b/esphome/components/sensor/automation.h index 8cd0adbeb2..4f34c35023 100644 --- a/esphome/components/sensor/automation.h +++ b/esphome/components/sensor/automation.h @@ -40,7 +40,7 @@ class ValueRangeTrigger : public Trigger, public Component { template void set_max(V max) { this->max_ = max; } void setup() override { - this->rtc_ = global_preferences->make_preference(this->parent_->get_object_id_hash()); + this->rtc_ = global_preferences->make_preference(this->parent_->get_preference_hash()); bool initial_state; if (this->rtc_.load(&initial_state)) { this->previous_in_range_ = initial_state; diff --git a/esphome/components/speaker/media_player/speaker_media_player.cpp b/esphome/components/speaker/media_player/speaker_media_player.cpp index 2c30f17c78..b45a78010a 100644 --- a/esphome/components/speaker/media_player/speaker_media_player.cpp +++ b/esphome/components/speaker/media_player/speaker_media_player.cpp @@ -55,7 +55,7 @@ void SpeakerMediaPlayer::setup() { this->media_control_command_queue_ = xQueueCreate(MEDIA_CONTROLS_QUEUE_LENGTH, sizeof(MediaCallCommand)); - this->pref_ = global_preferences->make_preference(this->get_object_id_hash()); + this->pref_ = global_preferences->make_preference(this->get_preference_hash()); VolumeRestoreState volume_restore_state; if (this->pref_.load(&volume_restore_state)) { diff --git a/esphome/components/sprinkler/sprinkler.cpp b/esphome/components/sprinkler/sprinkler.cpp index e191498857..7676e17468 100644 --- a/esphome/components/sprinkler/sprinkler.cpp +++ b/esphome/components/sprinkler/sprinkler.cpp @@ -81,7 +81,7 @@ void SprinklerControllerNumber::setup() { if (!this->restore_value_) { value = this->initial_value_; } else { - this->pref_ = global_preferences->make_preference(this->get_object_id_hash()); + this->pref_ = global_preferences->make_preference(this->get_preference_hash()); if (!this->pref_.load(&value)) { if (!std::isnan(this->initial_value_)) { value = this->initial_value_; diff --git a/esphome/components/switch/switch.cpp b/esphome/components/switch/switch.cpp index 13c12c1213..49acd274b2 100644 --- a/esphome/components/switch/switch.cpp +++ b/esphome/components/switch/switch.cpp @@ -32,7 +32,7 @@ optional Switch::get_initial_state() { if (!(restore_mode & RESTORE_MODE_PERSISTENT_MASK)) return {}; - this->rtc_ = global_preferences->make_preference(this->get_object_id_hash()); + this->rtc_ = global_preferences->make_preference(this->get_preference_hash()); bool initial_state; if (!this->rtc_.load(&initial_state)) return {}; diff --git a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp index 11a148830d..eac0629480 100644 --- a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp +++ b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp @@ -86,7 +86,7 @@ void TemplateAlarmControlPanel::setup() { break; case ALARM_CONTROL_PANEL_RESTORE_DEFAULT_DISARMED: { uint8_t value; - this->pref_ = global_preferences->make_preference(this->get_object_id_hash()); + this->pref_ = global_preferences->make_preference(this->get_preference_hash()); if (this->pref_.load(&value)) { this->current_state_ = static_cast(value); } else { diff --git a/esphome/components/template/datetime/template_date.cpp b/esphome/components/template/datetime/template_date.cpp index 01e15e532e..2fa8016802 100644 --- a/esphome/components/template/datetime/template_date.cpp +++ b/esphome/components/template/datetime/template_date.cpp @@ -20,7 +20,7 @@ void TemplateDate::setup() { } else { datetime::DateEntityRestoreState temp; this->pref_ = - global_preferences->make_preference(194434030U ^ this->get_object_id_hash()); + global_preferences->make_preference(194434030U ^ this->get_preference_hash()); if (this->pref_.load(&temp)) { temp.apply(this); return; diff --git a/esphome/components/template/datetime/template_datetime.cpp b/esphome/components/template/datetime/template_datetime.cpp index 3ab74e197f..a4a4e47d65 100644 --- a/esphome/components/template/datetime/template_datetime.cpp +++ b/esphome/components/template/datetime/template_datetime.cpp @@ -19,8 +19,8 @@ void TemplateDateTime::setup() { state = this->initial_value_; } else { datetime::DateTimeEntityRestoreState temp; - this->pref_ = global_preferences->make_preference(194434090U ^ - this->get_object_id_hash()); + this->pref_ = global_preferences->make_preference( + 194434090U ^ this->get_preference_hash()); if (this->pref_.load(&temp)) { temp.apply(this); return; diff --git a/esphome/components/template/datetime/template_time.cpp b/esphome/components/template/datetime/template_time.cpp index 0e4d734d16..349700f187 100644 --- a/esphome/components/template/datetime/template_time.cpp +++ b/esphome/components/template/datetime/template_time.cpp @@ -20,7 +20,7 @@ void TemplateTime::setup() { } else { datetime::TimeEntityRestoreState temp; this->pref_ = - global_preferences->make_preference(194434060U ^ this->get_object_id_hash()); + global_preferences->make_preference(194434060U ^ this->get_preference_hash()); if (this->pref_.load(&temp)) { temp.apply(this); return; diff --git a/esphome/components/template/number/template_number.cpp b/esphome/components/template/number/template_number.cpp index aaf5b27a71..187f426273 100644 --- a/esphome/components/template/number/template_number.cpp +++ b/esphome/components/template/number/template_number.cpp @@ -14,7 +14,7 @@ void TemplateNumber::setup() { if (!this->restore_value_) { value = this->initial_value_; } else { - this->pref_ = global_preferences->make_preference(this->get_object_id_hash()); + this->pref_ = global_preferences->make_preference(this->get_preference_hash()); if (!this->pref_.load(&value)) { if (!std::isnan(this->initial_value_)) { value = this->initial_value_; diff --git a/esphome/components/template/select/template_select.cpp b/esphome/components/template/select/template_select.cpp index 6ec29c8ef0..95b0ee0d2b 100644 --- a/esphome/components/template/select/template_select.cpp +++ b/esphome/components/template/select/template_select.cpp @@ -16,7 +16,7 @@ void TemplateSelect::setup() { ESP_LOGD(TAG, "State from initial: %s", value.c_str()); } else { size_t index; - this->pref_ = global_preferences->make_preference(this->get_object_id_hash()); + this->pref_ = global_preferences->make_preference(this->get_preference_hash()); if (!this->pref_.load(&index)) { value = this->initial_option_; ESP_LOGD(TAG, "State from initial (could not load stored index): %s", value.c_str()); diff --git a/esphome/components/template/text/template_text.cpp b/esphome/components/template/text/template_text.cpp index f5df7287c5..d8e840ba7e 100644 --- a/esphome/components/template/text/template_text.cpp +++ b/esphome/components/template/text/template_text.cpp @@ -15,7 +15,7 @@ void TemplateText::setup() { if (!this->pref_) { ESP_LOGD(TAG, "State from initial: %s", value.c_str()); } else { - uint32_t key = this->get_object_id_hash(); + uint32_t key = this->get_preference_hash(); key += this->traits.get_min_length() << 2; key += this->traits.get_max_length() << 4; key += fnv1_hash(this->traits.get_pattern()) << 6; diff --git a/esphome/components/total_daily_energy/total_daily_energy.cpp b/esphome/components/total_daily_energy/total_daily_energy.cpp index 7c316c495d..818696f99b 100644 --- a/esphome/components/total_daily_energy/total_daily_energy.cpp +++ b/esphome/components/total_daily_energy/total_daily_energy.cpp @@ -10,7 +10,7 @@ void TotalDailyEnergy::setup() { float initial_value = 0; if (this->restore_) { - this->pref_ = global_preferences->make_preference(this->get_object_id_hash()); + this->pref_ = global_preferences->make_preference(this->get_preference_hash()); this->pref_.load(&initial_value); } this->publish_state_and_save(initial_value); diff --git a/esphome/components/tuya/number/tuya_number.cpp b/esphome/components/tuya/number/tuya_number.cpp index 68a7f8f2a7..44b22167de 100644 --- a/esphome/components/tuya/number/tuya_number.cpp +++ b/esphome/components/tuya/number/tuya_number.cpp @@ -8,7 +8,7 @@ static const char *const TAG = "tuya.number"; void TuyaNumber::setup() { if (this->restore_value_) { - this->pref_ = global_preferences->make_preference(this->get_object_id_hash()); + this->pref_ = global_preferences->make_preference(this->get_preference_hash()); } this->parent_->register_listener(this->number_id_, [this](const TuyaDatapoint &datapoint) { diff --git a/esphome/components/valve/valve.cpp b/esphome/components/valve/valve.cpp index d1ec17945a..0ee710fc02 100644 --- a/esphome/components/valve/valve.cpp +++ b/esphome/components/valve/valve.cpp @@ -155,7 +155,7 @@ void Valve::publish_state(bool save) { } } optional Valve::restore_state_() { - this->rtc_ = global_preferences->make_preference(this->get_object_id_hash()); + this->rtc_ = global_preferences->make_preference(this->get_preference_hash()); ValveRestoreState recovered{}; if (!this->rtc_.load(&recovered)) return {}; diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index 68163ce8c3..8a65a9627a 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -85,6 +85,35 @@ class EntityBase { // Set has_state - for components that need to manually set this void set_has_state(bool state) { this->flags_.has_state = state; } + /** + * @brief Get a unique hash for storing preferences/settings for this entity. + * + * This method returns a hash that uniquely identifies the entity for the purpose of + * storing preferences (such as calibration, state, etc.). Unlike get_object_id_hash(), + * this hash also incorporates the device_id (if devices are enabled), ensuring uniqueness + * across multiple devices that may have entities with the same object_id. + * + * Use this method when storing or retrieving preferences/settings that should be unique + * per device-entity pair. Use get_object_id_hash() when you need a hash that identifies + * the entity regardless of the device it belongs to. + * + * For backward compatibility, if device_id is 0 (the main device), the hash is unchanged + * from previous versions, so existing single-device configurations will continue to work. + * + * @return uint32_t The unique hash for preferences, including device_id if available. + */ + uint32_t get_preference_hash() { +#ifdef USE_DEVICES + // Combine object_id_hash with device_id to ensure uniqueness across devices + // Note: device_id is 0 for the main device, so XORing with 0 preserves the original hash + // This ensures backward compatibility for existing single-device configurations + return this->get_object_id_hash() ^ this->get_device_id(); +#else + // Without devices, just use object_id_hash as before + return this->get_object_id_hash(); +#endif + } + protected: friend class api::APIConnection; diff --git a/tests/integration/fixtures/multi_device_preferences.yaml b/tests/integration/fixtures/multi_device_preferences.yaml new file mode 100644 index 0000000000..634d7157b2 --- /dev/null +++ b/tests/integration/fixtures/multi_device_preferences.yaml @@ -0,0 +1,165 @@ +esphome: + name: multi-device-preferences-test + # Define multiple devices for testing preference storage + devices: + - id: device_a + name: Device A + - id: device_b + name: Device B + +host: +api: # Port will be automatically injected +logger: + level: DEBUG + +# Test entities with restore modes to verify preference storage + +# Switches with same name on different devices - test restore mode +switch: + - platform: template + name: Light + id: light_device_a + device_id: device_a + restore_mode: RESTORE_DEFAULT_OFF + turn_on_action: + - lambda: |- + ESP_LOGI("test", "Device A Light turned ON"); + turn_off_action: + - lambda: |- + ESP_LOGI("test", "Device A Light turned OFF"); + + - platform: template + name: Light + id: light_device_b + device_id: device_b + restore_mode: RESTORE_DEFAULT_ON # Different default to test uniqueness + turn_on_action: + - lambda: |- + ESP_LOGI("test", "Device B Light turned ON"); + turn_off_action: + - lambda: |- + ESP_LOGI("test", "Device B Light turned OFF"); + + - platform: template + name: Light + id: light_main + restore_mode: RESTORE_DEFAULT_OFF + turn_on_action: + - lambda: |- + ESP_LOGI("test", "Main Light turned ON"); + turn_off_action: + - lambda: |- + ESP_LOGI("test", "Main Light turned OFF"); + +# Numbers with restore to test preference storage +number: + - platform: template + name: Setpoint + id: setpoint_device_a + device_id: device_a + min_value: 10.0 + max_value: 30.0 + step: 0.5 + restore_value: true + initial_value: 20.0 + set_action: + - lambda: |- + ESP_LOGI("test", "Device A Setpoint set to %.1f", x); + id(setpoint_device_a).state = x; + + - platform: template + name: Setpoint + id: setpoint_device_b + device_id: device_b + min_value: 10.0 + max_value: 30.0 + step: 0.5 + restore_value: true + initial_value: 25.0 # Different initial to test uniqueness + set_action: + - lambda: |- + ESP_LOGI("test", "Device B Setpoint set to %.1f", x); + id(setpoint_device_b).state = x; + + - platform: template + name: Setpoint + id: setpoint_main + min_value: 10.0 + max_value: 30.0 + step: 0.5 + restore_value: true + initial_value: 22.0 + set_action: + - lambda: |- + ESP_LOGI("test", "Main Setpoint set to %.1f", x); + id(setpoint_main).state = x; + +# Selects with restore to test preference storage +select: + - platform: template + name: Mode + id: mode_device_a + device_id: device_a + options: + - "Auto" + - "Manual" + - "Off" + restore_value: true + initial_option: "Auto" + set_action: + - lambda: |- + ESP_LOGI("test", "Device A Mode set to %s", x.c_str()); + id(mode_device_a).state = x; + + - platform: template + name: Mode + id: mode_device_b + device_id: device_b + options: + - "Auto" + - "Manual" + - "Off" + restore_value: true + initial_option: "Manual" # Different initial to test uniqueness + set_action: + - lambda: |- + ESP_LOGI("test", "Device B Mode set to %s", x.c_str()); + id(mode_device_b).state = x; + + - platform: template + name: Mode + id: mode_main + options: + - "Auto" + - "Manual" + - "Off" + restore_value: true + initial_option: "Off" + set_action: + - lambda: |- + ESP_LOGI("test", "Main Mode set to %s", x.c_str()); + id(mode_main).state = x; + +# Button to trigger preference logging test +button: + - platform: template + name: Test Preferences + on_press: + - lambda: |- + ESP_LOGI("test", "Testing preference storage uniqueness:"); + ESP_LOGI("test", "Device A Light state: %s", id(light_device_a).state ? "ON" : "OFF"); + ESP_LOGI("test", "Device B Light state: %s", id(light_device_b).state ? "ON" : "OFF"); + ESP_LOGI("test", "Main Light state: %s", id(light_main).state ? "ON" : "OFF"); + ESP_LOGI("test", "Device A Setpoint: %.1f", id(setpoint_device_a).state); + ESP_LOGI("test", "Device B Setpoint: %.1f", id(setpoint_device_b).state); + ESP_LOGI("test", "Main Setpoint: %.1f", id(setpoint_main).state); + ESP_LOGI("test", "Device A Mode: %s", id(mode_device_a).state.c_str()); + ESP_LOGI("test", "Device B Mode: %s", id(mode_device_b).state.c_str()); + ESP_LOGI("test", "Main Mode: %s", id(mode_main).state.c_str()); + // Log preference hashes for entities that actually store preferences + ESP_LOGI("test", "Device A Switch Pref Hash: %u", id(light_device_a).get_preference_hash()); + ESP_LOGI("test", "Device B Switch Pref Hash: %u", id(light_device_b).get_preference_hash()); + ESP_LOGI("test", "Main Switch Pref Hash: %u", id(light_main).get_preference_hash()); + ESP_LOGI("test", "Device A Number Pref Hash: %u", id(setpoint_device_a).get_preference_hash()); + ESP_LOGI("test", "Device B Number Pref Hash: %u", id(setpoint_device_b).get_preference_hash()); + ESP_LOGI("test", "Main Number Pref Hash: %u", id(setpoint_main).get_preference_hash()); diff --git a/tests/integration/test_multi_device_preferences.py b/tests/integration/test_multi_device_preferences.py new file mode 100644 index 0000000000..625f83f16e --- /dev/null +++ b/tests/integration/test_multi_device_preferences.py @@ -0,0 +1,144 @@ +"""Test multi-device preference storage functionality.""" + +from __future__ import annotations + +import asyncio +import re + +from aioesphomeapi import ButtonInfo, NumberInfo, SelectInfo, SwitchInfo +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_multi_device_preferences( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that entities with same names on different devices have unique preference storage.""" + loop = asyncio.get_running_loop() + log_lines: list[str] = [] + preferences_logged = loop.create_future() + + # Patterns to match preference hash logs + switch_hash_pattern_device = re.compile(r"Device ([AB]) Switch Pref Hash: (\d+)") + switch_hash_pattern_main = re.compile(r"Main Switch Pref Hash: (\d+)") + number_hash_pattern_device = re.compile(r"Device ([AB]) Number Pref Hash: (\d+)") + number_hash_pattern_main = re.compile(r"Main Number Pref Hash: (\d+)") + switch_hashes: dict[str, int] = {} + number_hashes: dict[str, int] = {} + + def check_output(line: str) -> None: + """Check log output for preference hash information.""" + log_lines.append(line) + + # Look for device switch preference hash logs + match = switch_hash_pattern_device.search(line) + if match: + device = match.group(1) + hash_value = int(match.group(2)) + switch_hashes[device] = hash_value + + # Look for main switch preference hash + match = switch_hash_pattern_main.search(line) + if match: + hash_value = int(match.group(1)) + switch_hashes["Main"] = hash_value + + # Look for device number preference hash logs + match = number_hash_pattern_device.search(line) + if match: + device = match.group(1) + hash_value = int(match.group(2)) + number_hashes[device] = hash_value + + # Look for main number preference hash + match = number_hash_pattern_main.search(line) + if match: + hash_value = int(match.group(1)) + number_hashes["Main"] = hash_value + + # If we have all hashes, complete the future + if ( + len(switch_hashes) == 3 + and len(number_hashes) == 3 + and not preferences_logged.done() + ): + preferences_logged.set_result(True) + + async with ( + run_compiled(yaml_config, line_callback=check_output), + api_client_connected() as client, + ): + # Get entity list + entities, _ = await client.list_entities_services() + + # Verify we have the expected entities with duplicate names on different devices + + # Check switches (3 with name "Light") + switches = [ + e for e in entities if isinstance(e, SwitchInfo) and e.name == "Light" + ] + assert len(switches) == 3, f"Expected 3 'Light' switches, got {len(switches)}" + + # Check numbers (3 with name "Setpoint") + numbers = [ + e for e in entities if isinstance(e, NumberInfo) and e.name == "Setpoint" + ] + assert len(numbers) == 3, f"Expected 3 'Setpoint' numbers, got {len(numbers)}" + + # Check selects (3 with name "Mode") + selects = [ + e for e in entities if isinstance(e, SelectInfo) and e.name == "Mode" + ] + assert len(selects) == 3, f"Expected 3 'Mode' selects, got {len(selects)}" + + # Find the test button entity to trigger preference logging + buttons = [e for e in entities if isinstance(e, ButtonInfo)] + test_button = next((b for b in buttons if b.name == "Test Preferences"), None) + assert test_button is not None, "Test Preferences button not found" + + # Press the button to trigger logging + client.button_command(test_button.key) + + # Wait for preference hashes to be logged + try: + await asyncio.wait_for(preferences_logged, timeout=5.0) + except TimeoutError: + pytest.fail("Preference hashes not logged within timeout") + + # Verify all switch preference hashes are unique + assert len(switch_hashes) == 3, ( + f"Expected 3 devices with switches, got {switch_hashes}" + ) + switch_hash_values = list(switch_hashes.values()) + assert len(switch_hash_values) == len(set(switch_hash_values)), ( + f"Switch preference hashes are not unique: {switch_hashes}" + ) + + # Verify all number preference hashes are unique + assert len(number_hashes) == 3, ( + f"Expected 3 devices with numbers, got {number_hashes}" + ) + number_hash_values = list(number_hashes.values()) + assert len(number_hash_values) == len(set(number_hash_values)), ( + f"Number preference hashes are not unique: {number_hashes}" + ) + + # Verify Device A and Device B have different hashes (they have device_id set) + assert switch_hashes["A"] != switch_hashes["B"], ( + f"Device A and B switches should have different hashes: A={switch_hashes['A']}, B={switch_hashes['B']}" + ) + assert number_hashes["A"] != number_hashes["B"], ( + f"Device A and B numbers should have different hashes: A={number_hashes['A']}, B={number_hashes['B']}" + ) + + # Verify Main device hash is different from both A and B + assert switch_hashes["Main"] != switch_hashes["A"], ( + f"Main and Device A switches should have different hashes: Main={switch_hashes['Main']}, A={switch_hashes['A']}" + ) + assert switch_hashes["Main"] != switch_hashes["B"], ( + f"Main and Device B switches should have different hashes: Main={switch_hashes['Main']}, B={switch_hashes['B']}" + ) From 6da8ec8d555972ec6a3829dccfad96adaea75f32 Mon Sep 17 00:00:00 2001 From: Jonathan Rascher Date: Sun, 24 Aug 2025 22:40:19 -0500 Subject: [PATCH 082/208] Merge commit from fork Ensures auth check doesn't pass erroneously when the client-supplied digest is shorter than the correct digest, but happens to match a prefix of the correct value (e.g., same username + certain substrings of the password). --- esphome/components/web_server_idf/web_server_idf.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index 3226444b1b..55b07c0f5e 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -253,7 +253,7 @@ bool AsyncWebServerRequest::authenticate(const char *username, const char *passw esp_crypto_base64_encode(reinterpret_cast(digest.get()), n, &out, reinterpret_cast(user_info.c_str()), user_info.size()); - return strncmp(digest.get(), auth_str + auth_prefix_len, auth.value().size() - auth_prefix_len) == 0; + return strcmp(digest.get(), auth_str + auth_prefix_len) == 0; } void AsyncWebServerRequest::requestAuthentication(const char *realm) const { From 5e508f7461349948c63e4fa85c845f3086156b95 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 26 Aug 2025 07:46:54 +1200 Subject: [PATCH 083/208] [core] Dont copy platform source files if there are no entities of that type (#10436) --- esphome/config.py | 5 ++++- esphome/writer.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/esphome/config.py b/esphome/config.py index 90325cbf6e..ebf1989693 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -1,6 +1,7 @@ from __future__ import annotations import abc +from collections import defaultdict from contextlib import contextmanager import contextvars import functools @@ -37,11 +38,13 @@ from esphome.yaml_util import ESPForceValue, ESPHomeDataBase, is_secret _LOGGER = logging.getLogger(__name__) -def iter_components(config): +def iter_components(config: ConfigType, platform_counts: defaultdict[str, int]): for domain, conf in config.items(): component = get_component(domain) yield domain, component if component.is_platform_component: + if not platform_counts[domain]: + continue for p_config in conf: p_name = f"{domain}.{p_config[CONF_PLATFORM]}" platform = get_platform(domain, p_config[CONF_PLATFORM]) diff --git a/esphome/writer.py b/esphome/writer.py index 4b25a25f7e..45aa996295 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -181,7 +181,7 @@ the custom_components folder or the external_components feature. def copy_src_tree(): source_files: list[loader.FileResource] = [] - for _, component in iter_components(CORE.config): + for _, component in iter_components(CORE.config, CORE.platform_counts): source_files += component.resources source_files_map = { Path(x.package.replace(".", "/") + "/" + x.resource): x for x in source_files From c01a26607ee469853fdf1af90be7d5616c5665be Mon Sep 17 00:00:00 2001 From: Thomas Rupprecht Date: Mon, 25 Aug 2025 23:45:03 +0200 Subject: [PATCH 084/208] improve const imports of `esphome.const` (#10438) --- esphome/components/esp32_ble/__init__.py | 9 +++++++-- esphome/components/sdm_meter/sensor.py | 4 +++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/esphome/components/esp32_ble/__init__.py b/esphome/components/esp32_ble/__init__.py index cc06058b65..d2eaa3ce6f 100644 --- a/esphome/components/esp32_ble/__init__.py +++ b/esphome/components/esp32_ble/__init__.py @@ -5,9 +5,14 @@ from esphome import automation import esphome.codegen as cg from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant import esphome.config_validation as cv -from esphome.const import CONF_ENABLE_ON_BOOT, CONF_ESPHOME, CONF_ID, CONF_NAME +from esphome.const import ( + CONF_ENABLE_ON_BOOT, + CONF_ESPHOME, + CONF_ID, + CONF_NAME, + CONF_NAME_ADD_MAC_SUFFIX, +) from esphome.core import CORE, TimePeriod -from esphome.core.config import CONF_NAME_ADD_MAC_SUFFIX import esphome.final_validate as fv DEPENDENCIES = ["esp32"] diff --git a/esphome/components/sdm_meter/sensor.py b/esphome/components/sdm_meter/sensor.py index 24ae32c7bc..affbc0409e 100644 --- a/esphome/components/sdm_meter/sensor.py +++ b/esphome/components/sdm_meter/sensor.py @@ -1,6 +1,5 @@ import esphome.codegen as cg from esphome.components import modbus, sensor -from esphome.components.atm90e32.sensor import CONF_PHASE_A, CONF_PHASE_B, CONF_PHASE_C import esphome.config_validation as cv from esphome.const import ( CONF_ACTIVE_POWER, @@ -12,7 +11,10 @@ from esphome.const import ( CONF_ID, CONF_IMPORT_ACTIVE_ENERGY, CONF_IMPORT_REACTIVE_ENERGY, + CONF_PHASE_A, CONF_PHASE_ANGLE, + CONF_PHASE_B, + CONF_PHASE_C, CONF_POWER_FACTOR, CONF_REACTIVE_POWER, CONF_TOTAL_POWER, From 9007621fd70621b37df418203b76afdf4eee70c4 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 26 Aug 2025 09:15:44 +1000 Subject: [PATCH 085/208] Revert "[core] Dont copy platform source files if there are no entities of that type" (#10441) --- esphome/config.py | 5 +---- esphome/writer.py | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/esphome/config.py b/esphome/config.py index ebf1989693..90325cbf6e 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -1,7 +1,6 @@ from __future__ import annotations import abc -from collections import defaultdict from contextlib import contextmanager import contextvars import functools @@ -38,13 +37,11 @@ from esphome.yaml_util import ESPForceValue, ESPHomeDataBase, is_secret _LOGGER = logging.getLogger(__name__) -def iter_components(config: ConfigType, platform_counts: defaultdict[str, int]): +def iter_components(config): for domain, conf in config.items(): component = get_component(domain) yield domain, component if component.is_platform_component: - if not platform_counts[domain]: - continue for p_config in conf: p_name = f"{domain}.{p_config[CONF_PLATFORM]}" platform = get_platform(domain, p_config[CONF_PLATFORM]) diff --git a/esphome/writer.py b/esphome/writer.py index 45aa996295..4b25a25f7e 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -181,7 +181,7 @@ the custom_components folder or the external_components feature. def copy_src_tree(): source_files: list[loader.FileResource] = [] - for _, component in iter_components(CORE.config, CORE.platform_counts): + for _, component in iter_components(CORE.config): source_files += component.resources source_files_map = { Path(x.package.replace(".", "/") + "/" + x.resource): x for x in source_files From 9e712e412732aa056625a9fca55ebe9d148252ac Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 27 Aug 2025 06:49:47 +0200 Subject: [PATCH 086/208] [wifi] Fix reconnection failures after adapter restart by not clearing netif pointers (#10458) --- esphome/components/wifi/wifi_component_esp32_arduino.cpp | 6 ------ esphome/components/wifi/wifi_component_esp_idf.cpp | 6 ------ 2 files changed, 12 deletions(-) diff --git a/esphome/components/wifi/wifi_component_esp32_arduino.cpp b/esphome/components/wifi/wifi_component_esp32_arduino.cpp index 67b1f565ff..89298e07c7 100644 --- a/esphome/components/wifi/wifi_component_esp32_arduino.cpp +++ b/esphome/components/wifi/wifi_component_esp32_arduino.cpp @@ -547,8 +547,6 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ } case ESPHOME_EVENT_ID_WIFI_STA_STOP: { ESP_LOGV(TAG, "STA stop"); - // Clear the STA interface handle to prevent use-after-free - s_sta_netif = nullptr; break; } case ESPHOME_EVENT_ID_WIFI_STA_CONNECTED: { @@ -638,10 +636,6 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ } case ESPHOME_EVENT_ID_WIFI_AP_STOP: { ESP_LOGV(TAG, "AP stop"); -#ifdef USE_WIFI_AP - // Clear the AP interface handle to prevent use-after-free - s_ap_netif = nullptr; -#endif break; } case ESPHOME_EVENT_ID_WIFI_AP_STACONNECTED: { diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 94f1f5125f..d465b346b3 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -697,8 +697,6 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_STOP) { ESP_LOGV(TAG, "STA stop"); s_sta_started = false; - // Clear the STA interface handle to prevent use-after-free - s_sta_netif = nullptr; } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_AUTHMODE_CHANGE) { const auto &it = data->data.sta_authmode_change; @@ -797,10 +795,6 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_AP_STOP) { ESP_LOGV(TAG, "AP stop"); s_ap_started = false; -#ifdef USE_WIFI_AP - // Clear the AP interface handle to prevent use-after-free - s_ap_netif = nullptr; -#endif } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_AP_PROBEREQRECVED) { const auto &it = data->data.ap_probe_req_rx; From 65d63de9b608c7c911371db880b1ede3f2e9cdfb Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 27 Aug 2025 19:30:01 +1000 Subject: [PATCH 087/208] [mipi_spi] Fix dimensions (#10443) --- esphome/components/mipi/__init__.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/esphome/components/mipi/__init__.py b/esphome/components/mipi/__init__.py index f610f160b0..570a021cff 100644 --- a/esphome/components/mipi/__init__.py +++ b/esphome/components/mipi/__init__.py @@ -309,8 +309,12 @@ class DriverChip: CONF_NATIVE_HEIGHT, height + offset_height * 2 ) offset_height = native_height - height - offset_height - # Swap default dimensions if swap_xy is set - if transform[CONF_SWAP_XY] is True: + # Swap default dimensions if swap_xy is set, or if rotation is 90/270 and we are not using a buffer + rotated = not requires_buffer(config) and config.get(CONF_ROTATION, 0) in ( + 90, + 270, + ) + if transform[CONF_SWAP_XY] is True or rotated: width, height = height, width offset_height, offset_width = offset_width, offset_height return width, height, offset_width, offset_height From c171d13c8cc56fddc694ca1c84a71dde98cd2b35 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 27 Aug 2025 19:30:33 +1000 Subject: [PATCH 088/208] [i2c] Perform register reads as single transactions (#10389) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- .../touchscreen/axs15231_touchscreen.cpp | 2 +- esphome/components/bmi160/bmi160.cpp | 2 +- .../components/bmp280_base/bmp280_base.cpp | 25 +- esphome/components/bmp280_base/bmp280_base.h | 10 +- esphome/components/bmp280_i2c/bmp280_i2c.cpp | 13 - esphome/components/bmp280_i2c/bmp280_i2c.h | 10 +- esphome/components/bmp280_spi/bmp280_spi.cpp | 8 +- esphome/components/bmp280_spi/bmp280_spi.h | 8 +- esphome/components/ch422g/ch422g.cpp | 4 +- esphome/components/ee895/ee895.cpp | 2 +- esphome/components/hte501/hte501.cpp | 5 +- esphome/components/i2c/__init__.py | 27 +- esphome/components/i2c/i2c.cpp | 64 ++-- esphome/components/i2c/i2c.h | 89 +++-- esphome/components/i2c/i2c_bus.h | 112 +++--- esphome/components/i2c/i2c_bus_arduino.cpp | 93 ++--- esphome/components/i2c/i2c_bus_arduino.h | 4 +- esphome/components/i2c/i2c_bus_esp_idf.cpp | 357 +++--------------- esphome/components/i2c/i2c_bus_esp_idf.h | 46 +-- esphome/components/iaqcore/iaqcore.cpp | 2 +- esphome/components/ina2xx_i2c/ina2xx_i2c.cpp | 2 +- esphome/components/kmeteriso/kmeteriso.cpp | 4 +- esphome/components/lc709203f/lc709203f.cpp | 4 +- esphome/components/mcp4461/mcp4461.cpp | 4 +- esphome/components/mlx90614/mlx90614.cpp | 6 +- esphome/components/mpl3115a2/mpl3115a2.cpp | 12 +- esphome/components/npi19/npi19.cpp | 2 +- esphome/components/opt3001/opt3001.cpp | 2 +- esphome/components/pca6416a/pca6416a.cpp | 6 +- esphome/components/pca9554/pca9554.cpp | 4 +- esphome/components/st7567_i2c/st7567_i2c.cpp | 3 +- esphome/components/tca9548a/tca9548a.cpp | 14 +- esphome/components/tca9548a/tca9548a.h | 4 +- esphome/components/tee501/tee501.cpp | 4 +- .../components/tlc59208f/tlc59208f_output.cpp | 3 +- esphome/components/veml3235/veml3235.cpp | 6 +- esphome/components/veml7700/veml7700.cpp | 8 +- esphome/components/veml7700/veml7700.h | 1 - 38 files changed, 329 insertions(+), 643 deletions(-) diff --git a/esphome/components/axs15231/touchscreen/axs15231_touchscreen.cpp b/esphome/components/axs15231/touchscreen/axs15231_touchscreen.cpp index 4adf0bbbe0..6304516164 100644 --- a/esphome/components/axs15231/touchscreen/axs15231_touchscreen.cpp +++ b/esphome/components/axs15231/touchscreen/axs15231_touchscreen.cpp @@ -41,7 +41,7 @@ void AXS15231Touchscreen::update_touches() { i2c::ErrorCode err; uint8_t data[8]{}; - err = this->write(AXS_READ_TOUCHPAD, sizeof(AXS_READ_TOUCHPAD), false); + err = this->write(AXS_READ_TOUCHPAD, sizeof(AXS_READ_TOUCHPAD)); ERROR_CHECK(err); err = this->read(data, sizeof(data)); ERROR_CHECK(err); diff --git a/esphome/components/bmi160/bmi160.cpp b/esphome/components/bmi160/bmi160.cpp index b041c7c2dc..4fcc3edb82 100644 --- a/esphome/components/bmi160/bmi160.cpp +++ b/esphome/components/bmi160/bmi160.cpp @@ -203,7 +203,7 @@ void BMI160Component::dump_config() { i2c::ErrorCode BMI160Component::read_le_int16_(uint8_t reg, int16_t *value, uint8_t len) { uint8_t raw_data[len * 2]; // read using read_register because we have little-endian data, and read_bytes_16 will swap it - i2c::ErrorCode err = this->read_register(reg, raw_data, len * 2, true); + i2c::ErrorCode err = this->read_register(reg, raw_data, len * 2); if (err != i2c::ERROR_OK) { return err; } diff --git a/esphome/components/bmp280_base/bmp280_base.cpp b/esphome/components/bmp280_base/bmp280_base.cpp index e3cc2d9a57..39654f5875 100644 --- a/esphome/components/bmp280_base/bmp280_base.cpp +++ b/esphome/components/bmp280_base/bmp280_base.cpp @@ -63,12 +63,12 @@ void BMP280Component::setup() { // Read the chip id twice, to work around a bug where the first read is 0. // https://community.st.com/t5/stm32-mcus-products/issue-with-reading-bmp280-chip-id-using-spi/td-p/691855 - if (!this->read_byte(0xD0, &chip_id)) { + if (!this->bmp_read_byte(0xD0, &chip_id)) { this->error_code_ = COMMUNICATION_FAILED; this->mark_failed(ESP_LOG_MSG_COMM_FAIL); return; } - if (!this->read_byte(0xD0, &chip_id)) { + if (!this->bmp_read_byte(0xD0, &chip_id)) { this->error_code_ = COMMUNICATION_FAILED; this->mark_failed(ESP_LOG_MSG_COMM_FAIL); return; @@ -80,7 +80,7 @@ void BMP280Component::setup() { } // Send a soft reset. - if (!this->write_byte(BMP280_REGISTER_RESET, BMP280_SOFT_RESET)) { + if (!this->bmp_write_byte(BMP280_REGISTER_RESET, BMP280_SOFT_RESET)) { this->mark_failed("Reset failed"); return; } @@ -89,7 +89,7 @@ void BMP280Component::setup() { uint8_t retry = 5; do { delay(2); - if (!this->read_byte(BMP280_REGISTER_STATUS, &status)) { + if (!this->bmp_read_byte(BMP280_REGISTER_STATUS, &status)) { this->mark_failed("Error reading status register"); return; } @@ -115,14 +115,14 @@ void BMP280Component::setup() { this->calibration_.p9 = this->read_s16_le_(0x9E); uint8_t config_register = 0; - if (!this->read_byte(BMP280_REGISTER_CONFIG, &config_register)) { + if (!this->bmp_read_byte(BMP280_REGISTER_CONFIG, &config_register)) { this->mark_failed("Read config"); return; } config_register &= ~0b11111100; config_register |= 0b000 << 5; // 0.5 ms standby time config_register |= (this->iir_filter_ & 0b111) << 2; - if (!this->write_byte(BMP280_REGISTER_CONFIG, config_register)) { + if (!this->bmp_write_byte(BMP280_REGISTER_CONFIG, config_register)) { this->mark_failed("Write config"); return; } @@ -159,7 +159,7 @@ void BMP280Component::update() { meas_value |= (this->temperature_oversampling_ & 0b111) << 5; meas_value |= (this->pressure_oversampling_ & 0b111) << 2; meas_value |= 0b01; // Forced mode - if (!this->write_byte(BMP280_REGISTER_CONTROL, meas_value)) { + if (!this->bmp_write_byte(BMP280_REGISTER_CONTROL, meas_value)) { this->status_set_warning(); return; } @@ -188,9 +188,10 @@ void BMP280Component::update() { } float BMP280Component::read_temperature_(int32_t *t_fine) { - uint8_t data[3]; - if (!this->read_bytes(BMP280_REGISTER_TEMPDATA, data, 3)) + uint8_t data[3]{}; + if (!this->bmp_read_bytes(BMP280_REGISTER_TEMPDATA, data, 3)) return NAN; + ESP_LOGV(TAG, "Read temperature data, raw: %02X %02X %02X", data[0], data[1], data[2]); int32_t adc = ((data[0] & 0xFF) << 16) | ((data[1] & 0xFF) << 8) | (data[2] & 0xFF); adc >>= 4; if (adc == 0x80000) { @@ -212,7 +213,7 @@ float BMP280Component::read_temperature_(int32_t *t_fine) { float BMP280Component::read_pressure_(int32_t t_fine) { uint8_t data[3]; - if (!this->read_bytes(BMP280_REGISTER_PRESSUREDATA, data, 3)) + if (!this->bmp_read_bytes(BMP280_REGISTER_PRESSUREDATA, data, 3)) return NAN; int32_t adc = ((data[0] & 0xFF) << 16) | ((data[1] & 0xFF) << 8) | (data[2] & 0xFF); adc >>= 4; @@ -258,12 +259,12 @@ void BMP280Component::set_pressure_oversampling(BMP280Oversampling pressure_over void BMP280Component::set_iir_filter(BMP280IIRFilter iir_filter) { this->iir_filter_ = iir_filter; } uint8_t BMP280Component::read_u8_(uint8_t a_register) { uint8_t data = 0; - this->read_byte(a_register, &data); + this->bmp_read_byte(a_register, &data); return data; } uint16_t BMP280Component::read_u16_le_(uint8_t a_register) { uint16_t data = 0; - this->read_byte_16(a_register, &data); + this->bmp_read_byte_16(a_register, &data); return (data >> 8) | (data << 8); } int16_t BMP280Component::read_s16_le_(uint8_t a_register) { return this->read_u16_le_(a_register); } diff --git a/esphome/components/bmp280_base/bmp280_base.h b/esphome/components/bmp280_base/bmp280_base.h index 4b22e98f13..a47a794e96 100644 --- a/esphome/components/bmp280_base/bmp280_base.h +++ b/esphome/components/bmp280_base/bmp280_base.h @@ -67,12 +67,12 @@ class BMP280Component : public PollingComponent { float get_setup_priority() const override; void update() override; - virtual bool read_byte(uint8_t a_register, uint8_t *data) = 0; - virtual bool write_byte(uint8_t a_register, uint8_t data) = 0; - virtual bool read_bytes(uint8_t a_register, uint8_t *data, size_t len) = 0; - virtual bool read_byte_16(uint8_t a_register, uint16_t *data) = 0; - protected: + virtual bool bmp_read_byte(uint8_t a_register, uint8_t *data) = 0; + virtual bool bmp_write_byte(uint8_t a_register, uint8_t data) = 0; + virtual bool bmp_read_bytes(uint8_t a_register, uint8_t *data, size_t len) = 0; + virtual bool bmp_read_byte_16(uint8_t a_register, uint16_t *data) = 0; + /// Read the temperature value and store the calculated ambient temperature in t_fine. float read_temperature_(int32_t *t_fine); /// Read the pressure value in hPa using the provided t_fine value. diff --git a/esphome/components/bmp280_i2c/bmp280_i2c.cpp b/esphome/components/bmp280_i2c/bmp280_i2c.cpp index 04b8bd8b10..75d899008d 100644 --- a/esphome/components/bmp280_i2c/bmp280_i2c.cpp +++ b/esphome/components/bmp280_i2c/bmp280_i2c.cpp @@ -5,19 +5,6 @@ namespace esphome { namespace bmp280_i2c { -bool BMP280I2CComponent::read_byte(uint8_t a_register, uint8_t *data) { - return I2CDevice::read_byte(a_register, data); -}; -bool BMP280I2CComponent::write_byte(uint8_t a_register, uint8_t data) { - return I2CDevice::write_byte(a_register, data); -}; -bool BMP280I2CComponent::read_bytes(uint8_t a_register, uint8_t *data, size_t len) { - return I2CDevice::read_bytes(a_register, data, len); -}; -bool BMP280I2CComponent::read_byte_16(uint8_t a_register, uint16_t *data) { - return I2CDevice::read_byte_16(a_register, data); -}; - void BMP280I2CComponent::dump_config() { LOG_I2C_DEVICE(this); BMP280Component::dump_config(); diff --git a/esphome/components/bmp280_i2c/bmp280_i2c.h b/esphome/components/bmp280_i2c/bmp280_i2c.h index 66d78d788b..0ac956202b 100644 --- a/esphome/components/bmp280_i2c/bmp280_i2c.h +++ b/esphome/components/bmp280_i2c/bmp280_i2c.h @@ -11,10 +11,12 @@ static const char *const TAG = "bmp280_i2c.sensor"; /// This class implements support for the BMP280 Temperature+Pressure i2c sensor. class BMP280I2CComponent : public esphome::bmp280_base::BMP280Component, public i2c::I2CDevice { public: - bool read_byte(uint8_t a_register, uint8_t *data) override; - bool write_byte(uint8_t a_register, uint8_t data) override; - bool read_bytes(uint8_t a_register, uint8_t *data, size_t len) override; - bool read_byte_16(uint8_t a_register, uint16_t *data) override; + bool bmp_read_byte(uint8_t a_register, uint8_t *data) override { return read_byte(a_register, data); } + bool bmp_write_byte(uint8_t a_register, uint8_t data) override { return write_byte(a_register, data); } + bool bmp_read_bytes(uint8_t a_register, uint8_t *data, size_t len) override { + return read_bytes(a_register, data, len); + } + bool bmp_read_byte_16(uint8_t a_register, uint16_t *data) override { return read_byte_16(a_register, data); } void dump_config() override; }; diff --git a/esphome/components/bmp280_spi/bmp280_spi.cpp b/esphome/components/bmp280_spi/bmp280_spi.cpp index a35e829432..88983e77c3 100644 --- a/esphome/components/bmp280_spi/bmp280_spi.cpp +++ b/esphome/components/bmp280_spi/bmp280_spi.cpp @@ -28,7 +28,7 @@ void BMP280SPIComponent::setup() { // 0x77 is transferred, for read access, the byte 0xF7 is transferred. // https://www.bosch-sensortec.com/media/boschsensortec/downloads/datasheets/bst-bmp280-ds001.pdf -bool BMP280SPIComponent::read_byte(uint8_t a_register, uint8_t *data) { +bool BMP280SPIComponent::bmp_read_byte(uint8_t a_register, uint8_t *data) { this->enable(); this->transfer_byte(set_bit(a_register, 7)); *data = this->transfer_byte(0); @@ -36,7 +36,7 @@ bool BMP280SPIComponent::read_byte(uint8_t a_register, uint8_t *data) { return true; } -bool BMP280SPIComponent::write_byte(uint8_t a_register, uint8_t data) { +bool BMP280SPIComponent::bmp_write_byte(uint8_t a_register, uint8_t data) { this->enable(); this->transfer_byte(clear_bit(a_register, 7)); this->transfer_byte(data); @@ -44,7 +44,7 @@ bool BMP280SPIComponent::write_byte(uint8_t a_register, uint8_t data) { return true; } -bool BMP280SPIComponent::read_bytes(uint8_t a_register, uint8_t *data, size_t len) { +bool BMP280SPIComponent::bmp_read_bytes(uint8_t a_register, uint8_t *data, size_t len) { this->enable(); this->transfer_byte(set_bit(a_register, 7)); this->read_array(data, len); @@ -52,7 +52,7 @@ bool BMP280SPIComponent::read_bytes(uint8_t a_register, uint8_t *data, size_t le return true; } -bool BMP280SPIComponent::read_byte_16(uint8_t a_register, uint16_t *data) { +bool BMP280SPIComponent::bmp_read_byte_16(uint8_t a_register, uint16_t *data) { this->enable(); this->transfer_byte(set_bit(a_register, 7)); ((uint8_t *) data)[1] = this->transfer_byte(0); diff --git a/esphome/components/bmp280_spi/bmp280_spi.h b/esphome/components/bmp280_spi/bmp280_spi.h index dd226502f6..1bb7678e55 100644 --- a/esphome/components/bmp280_spi/bmp280_spi.h +++ b/esphome/components/bmp280_spi/bmp280_spi.h @@ -10,10 +10,10 @@ class BMP280SPIComponent : public esphome::bmp280_base::BMP280Component, public spi::SPIDevice { void setup() override; - bool read_byte(uint8_t a_register, uint8_t *data) override; - bool write_byte(uint8_t a_register, uint8_t data) override; - bool read_bytes(uint8_t a_register, uint8_t *data, size_t len) override; - bool read_byte_16(uint8_t a_register, uint16_t *data) override; + bool bmp_read_byte(uint8_t a_register, uint8_t *data) override; + bool bmp_write_byte(uint8_t a_register, uint8_t data) override; + bool bmp_read_bytes(uint8_t a_register, uint8_t *data, size_t len) override; + bool bmp_read_byte_16(uint8_t a_register, uint16_t *data) override; }; } // namespace bmp280_spi diff --git a/esphome/components/ch422g/ch422g.cpp b/esphome/components/ch422g/ch422g.cpp index 6f652cb0c6..9a4e342525 100644 --- a/esphome/components/ch422g/ch422g.cpp +++ b/esphome/components/ch422g/ch422g.cpp @@ -91,7 +91,7 @@ bool CH422GComponent::read_inputs_() { // Write a register. Can't use the standard write_byte() method because there is no single pre-configured i2c address. bool CH422GComponent::write_reg_(uint8_t reg, uint8_t value) { - auto err = this->bus_->write(reg, &value, 1); + auto err = this->bus_->write_readv(reg, &value, 1, nullptr, 0); if (err != i2c::ERROR_OK) { this->status_set_warning(str_sprintf("write failed for register 0x%X, error %d", reg, err).c_str()); return false; @@ -102,7 +102,7 @@ bool CH422GComponent::write_reg_(uint8_t reg, uint8_t value) { uint8_t CH422GComponent::read_reg_(uint8_t reg) { uint8_t value; - auto err = this->bus_->read(reg, &value, 1); + auto err = this->bus_->write_readv(reg, nullptr, 0, &value, 1); if (err != i2c::ERROR_OK) { this->status_set_warning(str_sprintf("read failed for register 0x%X, error %d", reg, err).c_str()); return 0; diff --git a/esphome/components/ee895/ee895.cpp b/esphome/components/ee895/ee895.cpp index 3a8a9b3725..c6eaf4e728 100644 --- a/esphome/components/ee895/ee895.cpp +++ b/esphome/components/ee895/ee895.cpp @@ -83,7 +83,7 @@ void EE895Component::write_command_(uint16_t addr, uint16_t reg_cnt) { crc16 = calc_crc16_(address, 6); address[5] = crc16 & 0xFF; address[6] = (crc16 >> 8) & 0xFF; - this->write(address, 7, true); + this->write(address, 7); } float EE895Component::read_float_() { diff --git a/esphome/components/hte501/hte501.cpp b/esphome/components/hte501/hte501.cpp index fa81640f50..b7d3be63fe 100644 --- a/esphome/components/hte501/hte501.cpp +++ b/esphome/components/hte501/hte501.cpp @@ -9,9 +9,8 @@ static const char *const TAG = "hte501"; void HTE501Component::setup() { uint8_t address[] = {0x70, 0x29}; - this->write(address, 2, false); uint8_t identification[9]; - this->read(identification, 9); + this->write_read(address, sizeof address, identification, sizeof identification); if (identification[8] != crc8(identification, 8, 0xFF, 0x31, true)) { this->error_code_ = CRC_CHECK_FAILED; this->mark_failed(); @@ -42,7 +41,7 @@ void HTE501Component::dump_config() { float HTE501Component::get_setup_priority() const { return setup_priority::DATA; } void HTE501Component::update() { uint8_t address_1[] = {0x2C, 0x1B}; - this->write(address_1, 2, true); + this->write(address_1, 2); this->set_timeout(50, [this]() { uint8_t i2c_response[6]; this->read(i2c_response, 6); diff --git a/esphome/components/i2c/__init__.py b/esphome/components/i2c/__init__.py index 4172b23845..35b9fab9e4 100644 --- a/esphome/components/i2c/__init__.py +++ b/esphome/components/i2c/__init__.py @@ -2,7 +2,6 @@ import logging from esphome import pins import esphome.codegen as cg -from esphome.components import esp32 from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( @@ -14,8 +13,6 @@ from esphome.const import ( CONF_SCL, CONF_SDA, CONF_TIMEOUT, - KEY_CORE, - KEY_FRAMEWORK_VERSION, PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040, @@ -48,28 +45,8 @@ def _bus_declare_type(value): def validate_config(config): - if ( - config[CONF_SCAN] - and CORE.is_esp32 - and CORE.using_esp_idf - and esp32.get_esp32_variant() - in [ - esp32.const.VARIANT_ESP32C5, - esp32.const.VARIANT_ESP32C6, - esp32.const.VARIANT_ESP32P4, - ] - ): - version: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] - if version.major == 5 and ( - (version.minor == 3 and version.patch <= 3) - or (version.minor == 4 and version.patch <= 1) - ): - LOGGER.warning( - "There is a bug in esp-idf version %s that breaks I2C scan, I2C scan " - "has been disabled, see https://github.com/esphome/issues/issues/7128", - str(version), - ) - config[CONF_SCAN] = False + if CORE.using_esp_idf: + return cv.require_framework_version(esp_idf=cv.Version(5, 4, 2))(config) return config diff --git a/esphome/components/i2c/i2c.cpp b/esphome/components/i2c/i2c.cpp index 2b2190d28b..e66ab8ba73 100644 --- a/esphome/components/i2c/i2c.cpp +++ b/esphome/components/i2c/i2c.cpp @@ -1,4 +1,6 @@ #include "i2c.h" + +#include "esphome/core/defines.h" #include "esphome/core/log.h" #include @@ -7,38 +9,48 @@ namespace i2c { static const char *const TAG = "i2c"; -ErrorCode I2CDevice::read_register(uint8_t a_register, uint8_t *data, size_t len, bool stop) { - ErrorCode err = this->write(&a_register, 1, stop); - if (err != ERROR_OK) - return err; - return bus_->read(address_, data, len); +void I2CBus::i2c_scan_() { + // suppress logs from the IDF I2C library during the scan +#if defined(USE_ESP32) && defined(USE_LOGGER) + auto previous = esp_log_level_get("*"); + esp_log_level_set("*", ESP_LOG_NONE); +#endif + + for (uint8_t address = 8; address != 120; address++) { + auto err = write_readv(address, nullptr, 0, nullptr, 0); + if (err == ERROR_OK) { + scan_results_.emplace_back(address, true); + } else if (err == ERROR_UNKNOWN) { + scan_results_.emplace_back(address, false); + } + } +#if defined(USE_ESP32) && defined(USE_LOGGER) + esp_log_level_set("*", previous); +#endif } -ErrorCode I2CDevice::read_register16(uint16_t a_register, uint8_t *data, size_t len, bool stop) { +ErrorCode I2CDevice::read_register(uint8_t a_register, uint8_t *data, size_t len) { + return bus_->write_readv(this->address_, &a_register, 1, data, len); +} + +ErrorCode I2CDevice::read_register16(uint16_t a_register, uint8_t *data, size_t len) { a_register = convert_big_endian(a_register); - ErrorCode const err = this->write(reinterpret_cast(&a_register), 2, stop); - if (err != ERROR_OK) - return err; - return bus_->read(address_, data, len); + return bus_->write_readv(this->address_, reinterpret_cast(&a_register), 2, data, len); } -ErrorCode I2CDevice::write_register(uint8_t a_register, const uint8_t *data, size_t len, bool stop) { - WriteBuffer buffers[2]; - buffers[0].data = &a_register; - buffers[0].len = 1; - buffers[1].data = data; - buffers[1].len = len; - return bus_->writev(address_, buffers, 2, stop); +ErrorCode I2CDevice::write_register(uint8_t a_register, const uint8_t *data, size_t len) const { + std::vector v{}; + v.push_back(a_register); + v.insert(v.end(), data, data + len); + return bus_->write_readv(this->address_, v.data(), v.size(), nullptr, 0); } -ErrorCode I2CDevice::write_register16(uint16_t a_register, const uint8_t *data, size_t len, bool stop) { - a_register = convert_big_endian(a_register); - WriteBuffer buffers[2]; - buffers[0].data = reinterpret_cast(&a_register); - buffers[0].len = 2; - buffers[1].data = data; - buffers[1].len = len; - return bus_->writev(address_, buffers, 2, stop); +ErrorCode I2CDevice::write_register16(uint16_t a_register, const uint8_t *data, size_t len) const { + std::vector v(len + 2); + v.push_back(a_register >> 8); + v.push_back(a_register); + v.insert(v.end(), data, data + len); + return bus_->write_readv(this->address_, v.data(), v.size(), nullptr, 0); } bool I2CDevice::read_bytes_16(uint8_t a_register, uint16_t *data, uint8_t len) { @@ -49,7 +61,7 @@ bool I2CDevice::read_bytes_16(uint8_t a_register, uint16_t *data, uint8_t len) { return true; } -bool I2CDevice::write_bytes_16(uint8_t a_register, const uint16_t *data, uint8_t len) { +bool I2CDevice::write_bytes_16(uint8_t a_register, const uint16_t *data, uint8_t len) const { // we have to copy in order to be able to change byte order std::unique_ptr temp{new uint16_t[len]}; for (size_t i = 0; i < len; i++) diff --git a/esphome/components/i2c/i2c.h b/esphome/components/i2c/i2c.h index 15f786245b..48a6e751cf 100644 --- a/esphome/components/i2c/i2c.h +++ b/esphome/components/i2c/i2c.h @@ -1,10 +1,10 @@ #pragma once -#include "i2c_bus.h" -#include "esphome/core/helpers.h" -#include "esphome/core/optional.h" #include #include +#include "esphome/core/helpers.h" +#include "esphome/core/optional.h" +#include "i2c_bus.h" namespace esphome { namespace i2c { @@ -161,51 +161,53 @@ class I2CDevice { /// @param data pointer to an array to store the bytes /// @param len length of the buffer = number of bytes to read /// @return an i2c::ErrorCode - ErrorCode read(uint8_t *data, size_t len) { return bus_->read(address_, data, len); } + ErrorCode read(uint8_t *data, size_t len) const { return bus_->write_readv(this->address_, nullptr, 0, data, len); } /// @brief reads an array of bytes from a specific register in the I²C device /// @param a_register an 8 bits internal address of the I²C register to read from /// @param data pointer to an array to store the bytes /// @param len length of the buffer = number of bytes to read - /// @param stop (true/false): True will send a stop message, releasing the bus after - /// transmission. False will send a restart, keeping the connection active. /// @return an i2c::ErrorCode - ErrorCode read_register(uint8_t a_register, uint8_t *data, size_t len, bool stop = true); + ErrorCode read_register(uint8_t a_register, uint8_t *data, size_t len); /// @brief reads an array of bytes from a specific register in the I²C device /// @param a_register the 16 bits internal address of the I²C register to read from /// @param data pointer to an array of bytes to store the information /// @param len length of the buffer = number of bytes to read - /// @param stop (true/false): True will send a stop message, releasing the bus after - /// transmission. False will send a restart, keeping the connection active. /// @return an i2c::ErrorCode - ErrorCode read_register16(uint16_t a_register, uint8_t *data, size_t len, bool stop = true); + ErrorCode read_register16(uint16_t a_register, uint8_t *data, size_t len); /// @brief writes an array of bytes to a device using an I2CBus /// @param data pointer to an array that contains the bytes to send /// @param len length of the buffer = number of bytes to write - /// @param stop (true/false): True will send a stop message, releasing the bus after - /// transmission. False will send a restart, keeping the connection active. /// @return an i2c::ErrorCode - ErrorCode write(const uint8_t *data, size_t len, bool stop = true) { return bus_->write(address_, data, len, stop); } + ErrorCode write(const uint8_t *data, size_t len) const { + return bus_->write_readv(this->address_, data, len, nullptr, 0); + } + + /// @brief writes an array of bytes to a device, then reads an array, as a single transaction + /// @param write_data pointer to an array that contains the bytes to send + /// @param write_len length of the buffer = number of bytes to write + /// @param read_data pointer to an array to store the bytes read + /// @param read_len length of the buffer = number of bytes to read + /// @return an i2c::ErrorCode + ErrorCode write_read(const uint8_t *write_data, size_t write_len, uint8_t *read_data, size_t read_len) const { + return bus_->write_readv(this->address_, write_data, write_len, read_data, read_len); + } /// @brief writes an array of bytes to a specific register in the I²C device /// @param a_register the internal address of the register to read from /// @param data pointer to an array to store the bytes /// @param len length of the buffer = number of bytes to read - /// @param stop (true/false): True will send a stop message, releasing the bus after - /// transmission. False will send a restart, keeping the connection active. /// @return an i2c::ErrorCode - ErrorCode write_register(uint8_t a_register, const uint8_t *data, size_t len, bool stop = true); + ErrorCode write_register(uint8_t a_register, const uint8_t *data, size_t len) const; /// @brief write an array of bytes to a specific register in the I²C device /// @param a_register the 16 bits internal address of the register to read from /// @param data pointer to an array to store the bytes /// @param len length of the buffer = number of bytes to read - /// @param stop (true/false): True will send a stop message, releasing the bus after - /// transmission. False will send a restart, keeping the connection active. /// @return an i2c::ErrorCode - ErrorCode write_register16(uint16_t a_register, const uint8_t *data, size_t len, bool stop = true); + ErrorCode write_register16(uint16_t a_register, const uint8_t *data, size_t len) const; /// /// Compat APIs @@ -217,7 +219,7 @@ class I2CDevice { return read_register(a_register, data, len) == ERROR_OK; } - bool read_bytes_raw(uint8_t *data, uint8_t len) { return read(data, len) == ERROR_OK; } + bool read_bytes_raw(uint8_t *data, uint8_t len) const { return read(data, len) == ERROR_OK; } template optional> read_bytes(uint8_t a_register) { std::array res; @@ -236,9 +238,7 @@ class I2CDevice { bool read_bytes_16(uint8_t a_register, uint16_t *data, uint8_t len); - bool read_byte(uint8_t a_register, uint8_t *data, bool stop = true) { - return read_register(a_register, data, 1, stop) == ERROR_OK; - } + bool read_byte(uint8_t a_register, uint8_t *data) { return read_register(a_register, data, 1) == ERROR_OK; } optional read_byte(uint8_t a_register) { uint8_t data; @@ -249,11 +249,11 @@ class I2CDevice { bool read_byte_16(uint8_t a_register, uint16_t *data) { return read_bytes_16(a_register, data, 1); } - bool write_bytes(uint8_t a_register, const uint8_t *data, uint8_t len, bool stop = true) { - return write_register(a_register, data, len, stop) == ERROR_OK; + bool write_bytes(uint8_t a_register, const uint8_t *data, uint8_t len) const { + return write_register(a_register, data, len) == ERROR_OK; } - bool write_bytes(uint8_t a_register, const std::vector &data) { + bool write_bytes(uint8_t a_register, const std::vector &data) const { return write_bytes(a_register, data.data(), data.size()); } @@ -261,13 +261,42 @@ class I2CDevice { return write_bytes(a_register, data.data(), data.size()); } - bool write_bytes_16(uint8_t a_register, const uint16_t *data, uint8_t len); + bool write_bytes_16(uint8_t a_register, const uint16_t *data, uint8_t len) const; - bool write_byte(uint8_t a_register, uint8_t data, bool stop = true) { - return write_bytes(a_register, &data, 1, stop); + bool write_byte(uint8_t a_register, uint8_t data) const { return write_bytes(a_register, &data, 1); } + + bool write_byte_16(uint8_t a_register, uint16_t data) const { return write_bytes_16(a_register, &data, 1); } + + // Deprecated functions + + ESPDEPRECATED("The stop argument is no longer used. This will be removed from ESPHome 2026.3.0", "2025.9.0") + ErrorCode read_register(uint8_t a_register, uint8_t *data, size_t len, bool stop) { + return this->read_register(a_register, data, len); } - bool write_byte_16(uint8_t a_register, uint16_t data) { return write_bytes_16(a_register, &data, 1); } + ESPDEPRECATED("The stop argument is no longer used. This will be removed from ESPHome 2026.3.0", "2025.9.0") + ErrorCode read_register16(uint16_t a_register, uint8_t *data, size_t len, bool stop) { + return this->read_register16(a_register, data, len); + } + + ESPDEPRECATED("The stop argument is no longer used; use write_read() for consecutive write and read. This will be " + "removed from ESPHome 2026.3.0", + "2025.9.0") + ErrorCode write(const uint8_t *data, size_t len, bool stop) const { return this->write(data, len); } + + ESPDEPRECATED("The stop argument is no longer used; use write_read() for consecutive write and read. This will be " + "removed from ESPHome 2026.3.0", + "2025.9.0") + ErrorCode write_register(uint8_t a_register, const uint8_t *data, size_t len, bool stop) const { + return this->write_register(a_register, data, len); + } + + ESPDEPRECATED("The stop argument is no longer used; use write_read() for consecutive write and read. This will be " + "removed from ESPHome 2026.3.0", + "2025.9.0") + ErrorCode write_register16(uint16_t a_register, const uint8_t *data, size_t len, bool stop) const { + return this->write_register16(a_register, data, len); + } protected: uint8_t address_{0x00}; ///< store the address of the device on the bus diff --git a/esphome/components/i2c/i2c_bus.h b/esphome/components/i2c/i2c_bus.h index da94aa940d..df4df628e8 100644 --- a/esphome/components/i2c/i2c_bus.h +++ b/esphome/components/i2c/i2c_bus.h @@ -1,9 +1,12 @@ #pragma once #include #include +#include #include #include +#include "esphome/core/helpers.h" + namespace esphome { namespace i2c { @@ -39,71 +42,66 @@ struct WriteBuffer { /// note https://www.nxp.com/docs/en/application-note/AN10216.pdf class I2CBus { public: - /// @brief Creates a ReadBuffer and calls the virtual readv() method to read bytes into this buffer - /// @param address address of the I²C component on the i2c bus - /// @param buffer pointer to an array of bytes that will be used to store the data received - /// @param len length of the buffer = number of bytes to read - /// @return an i2c::ErrorCode - virtual ErrorCode read(uint8_t address, uint8_t *buffer, size_t len) { - ReadBuffer buf; - buf.data = buffer; - buf.len = len; - return readv(address, &buf, 1); - } + virtual ~I2CBus() = default; - /// @brief This virtual method reads bytes from an I2CBus into an array of ReadBuffer. - /// @param address address of the I²C component on the i2c bus - /// @param buffers pointer to an array of ReadBuffer - /// @param count number of ReadBuffer to read - /// @return an i2c::ErrorCode - /// @details This is a pure virtual method that must be implemented in a subclass. - virtual ErrorCode readv(uint8_t address, ReadBuffer *buffers, size_t count) = 0; - - virtual ErrorCode write(uint8_t address, const uint8_t *buffer, size_t len) { - return write(address, buffer, len, true); - } - - /// @brief Creates a WriteBuffer and calls the writev() method to send the bytes from this buffer - /// @param address address of the I²C component on the i2c bus - /// @param buffer pointer to an array of bytes that contains the data to be sent - /// @param len length of the buffer = number of bytes to write - /// @param stop true or false: True will send a stop message, releasing the bus after - /// transmission. False will send a restart, keeping the connection active. - /// @return an i2c::ErrorCode - virtual ErrorCode write(uint8_t address, const uint8_t *buffer, size_t len, bool stop) { - WriteBuffer buf; - buf.data = buffer; - buf.len = len; - return writev(address, &buf, 1, stop); - } - - virtual ErrorCode writev(uint8_t address, WriteBuffer *buffers, size_t cnt) { - return writev(address, buffers, cnt, true); - } - - /// @brief This virtual method writes bytes to an I2CBus from an array of WriteBuffer. - /// @param address address of the I²C component on the i2c bus - /// @param buffers pointer to an array of WriteBuffer - /// @param count number of WriteBuffer to write - /// @param stop true or false: True will send a stop message, releasing the bus after + /// @brief This virtual method writes bytes to an I2CBus from an array, + /// then reads bytes into an array of ReadBuffer. + /// @param address address of the I²C device on the i2c bus + /// @param write_buffer pointer to data + /// @param write_count number of bytes to write + /// @param read_buffer pointer to an array to receive data + /// @param read_count number of bytes to read /// transmission. False will send a restart, keeping the connection active. /// @return an i2c::ErrorCode /// @details This is a pure virtual method that must be implemented in the subclass. - virtual ErrorCode writev(uint8_t address, WriteBuffer *buffers, size_t count, bool stop) = 0; + virtual ErrorCode write_readv(uint8_t address, const uint8_t *write_buffer, size_t write_count, uint8_t *read_buffer, + size_t read_count) = 0; + + // Legacy functions for compatibility + + ErrorCode read(uint8_t address, uint8_t *buffer, size_t len) { + return this->write_readv(address, nullptr, 0, buffer, len); + } + + ErrorCode write(uint8_t address, const uint8_t *buffer, size_t len, bool stop = true) { + return this->write_readv(address, buffer, len, nullptr, 0); + } + + ESPDEPRECATED("This method is deprecated and will be removed in ESPHome 2026.3.0. Use write_readv() instead.", + "2025.9.0") + ErrorCode readv(uint8_t address, ReadBuffer *read_buffers, size_t count) { + size_t total_len = 0; + for (size_t i = 0; i != count; i++) { + total_len += read_buffers[i].len; + } + std::vector buffer(total_len); + auto err = this->write_readv(address, nullptr, 0, buffer.data(), total_len); + if (err != ERROR_OK) + return err; + size_t pos = 0; + for (size_t i = 0; i != count; i++) { + if (read_buffers[i].len != 0) { + std::memcpy(read_buffers[i].data, buffer.data() + pos, read_buffers[i].len); + pos += read_buffers[i].len; + } + } + return ERROR_OK; + } + + ESPDEPRECATED("This method is deprecated and will be removed in ESPHome 2026.3.0. Use write_readv() instead.", + "2025.9.0") + ErrorCode writev(uint8_t address, const WriteBuffer *write_buffers, size_t count, bool stop = true) { + std::vector buffer{}; + for (size_t i = 0; i != count; i++) { + buffer.insert(buffer.end(), write_buffers[i].data, write_buffers[i].data + write_buffers[i].len); + } + return this->write_readv(address, buffer.data(), buffer.size(), nullptr, 0); + } protected: /// @brief Scans the I2C bus for devices. Devices presence is kept in an array of std::pair /// that contains the address and the corresponding bool presence flag. - virtual void i2c_scan() { - for (uint8_t address = 8; address < 120; address++) { - auto err = writev(address, nullptr, 0); - if (err == ERROR_OK) { - scan_results_.emplace_back(address, true); - } else if (err == ERROR_UNKNOWN) { - scan_results_.emplace_back(address, false); - } - } - } + void i2c_scan_(); std::vector> scan_results_; ///< array containing scan results bool scan_{false}; ///< Should we scan ? Can be set in the yaml }; diff --git a/esphome/components/i2c/i2c_bus_arduino.cpp b/esphome/components/i2c/i2c_bus_arduino.cpp index 24385745eb..221423418b 100644 --- a/esphome/components/i2c/i2c_bus_arduino.cpp +++ b/esphome/components/i2c/i2c_bus_arduino.cpp @@ -41,7 +41,7 @@ void ArduinoI2CBus::setup() { this->initialized_ = true; if (this->scan_) { ESP_LOGV(TAG, "Scanning bus for active devices"); - this->i2c_scan(); + this->i2c_scan_(); } } @@ -111,88 +111,37 @@ void ArduinoI2CBus::dump_config() { } } -ErrorCode ArduinoI2CBus::readv(uint8_t address, ReadBuffer *buffers, size_t cnt) { +ErrorCode ArduinoI2CBus::write_readv(uint8_t address, const uint8_t *write_buffer, size_t write_count, + uint8_t *read_buffer, size_t read_count) { #if defined(USE_ESP8266) this->set_pins_and_clock_(); // reconfigure Wire global state in case there are multiple instances #endif - - // logging is only enabled with vv level, if warnings are shown the caller - // should log them if (!initialized_) { - ESP_LOGVV(TAG, "i2c bus not initialized!"); - return ERROR_NOT_INITIALIZED; - } - size_t to_request = 0; - for (size_t i = 0; i < cnt; i++) - to_request += buffers[i].len; - 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; - } - - for (size_t i = 0; i < cnt; i++) { - const auto &buf = buffers[i]; - for (size_t j = 0; j < buf.len; j++) - buf.data[j] = wire_->read(); - } - -#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE - char debug_buf[4]; - std::string debug_hex; - - for (size_t i = 0; i < cnt; i++) { - const auto &buf = buffers[i]; - for (size_t j = 0; j < buf.len; j++) { - snprintf(debug_buf, sizeof(debug_buf), "%02X", buf.data[j]); - debug_hex += debug_buf; - } - } - ESP_LOGVV(TAG, "0x%02X RX %s", address, debug_hex.c_str()); -#endif - - return ERROR_OK; -} -ErrorCode ArduinoI2CBus::writev(uint8_t address, WriteBuffer *buffers, size_t cnt, bool stop) { -#if defined(USE_ESP8266) - this->set_pins_and_clock_(); // reconfigure Wire global state in case there are multiple instances -#endif - - // logging is only enabled with vv level, if warnings are shown the caller - // should log them - if (!initialized_) { - ESP_LOGVV(TAG, "i2c bus not initialized!"); + ESP_LOGD(TAG, "i2c bus not initialized!"); return ERROR_NOT_INITIALIZED; } -#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE - char debug_buf[4]; - std::string debug_hex; + ESP_LOGV(TAG, "0x%02X TX %s", address, format_hex_pretty(write_buffer, write_count).c_str()); - for (size_t i = 0; i < cnt; i++) { - const auto &buf = buffers[i]; - for (size_t j = 0; j < buf.len; j++) { - snprintf(debug_buf, sizeof(debug_buf), "%02X", buf.data[j]); - debug_hex += debug_buf; - } - } - ESP_LOGVV(TAG, "0x%02X TX %s", address, debug_hex.c_str()); -#endif - - wire_->beginTransmission(address); - size_t written = 0; - for (size_t i = 0; i < cnt; i++) { - const auto &buf = buffers[i]; - if (buf.len == 0) - continue; - size_t ret = wire_->write(buf.data, buf.len); - written += ret; - if (ret != buf.len) { - ESP_LOGVV(TAG, "TX failed at %u", written); + uint8_t status = 0; + if (write_count != 0 || read_count == 0) { + wire_->beginTransmission(address); + size_t ret = wire_->write(write_buffer, write_count); + if (ret != write_count) { + ESP_LOGV(TAG, "TX failed"); return ERROR_UNKNOWN; } + status = wire_->endTransmission(read_count == 0); + } + if (status == 0 && read_count != 0) { + size_t ret2 = wire_->requestFrom(address, read_count, true); + if (ret2 != read_count) { + ESP_LOGVV(TAG, "RX %u from %02X failed with error %u", read_count, address, ret2); + return ERROR_TIMEOUT; + } + for (size_t j = 0; j != read_count; j++) + read_buffer[j] = wire_->read(); } - uint8_t status = wire_->endTransmission(stop); switch (status) { case 0: return ERROR_OK; diff --git a/esphome/components/i2c/i2c_bus_arduino.h b/esphome/components/i2c/i2c_bus_arduino.h index 7e6616cbce..b441828353 100644 --- a/esphome/components/i2c/i2c_bus_arduino.h +++ b/esphome/components/i2c/i2c_bus_arduino.h @@ -19,8 +19,8 @@ class ArduinoI2CBus : public InternalI2CBus, public Component { public: void setup() override; void dump_config() override; - ErrorCode readv(uint8_t address, ReadBuffer *buffers, size_t cnt) override; - ErrorCode writev(uint8_t address, WriteBuffer *buffers, size_t cnt, bool stop) override; + ErrorCode write_readv(uint8_t address, const uint8_t *write_buffer, size_t write_count, uint8_t *read_buffer, + size_t read_count) override; float get_setup_priority() const override { return setup_priority::BUS; } void set_scan(bool scan) { scan_ = scan; } diff --git a/esphome/components/i2c/i2c_bus_esp_idf.cpp b/esphome/components/i2c/i2c_bus_esp_idf.cpp index cf31ba1c0d..bf50ea0586 100644 --- a/esphome/components/i2c/i2c_bus_esp_idf.cpp +++ b/esphome/components/i2c/i2c_bus_esp_idf.cpp @@ -1,6 +1,7 @@ #ifdef USE_ESP_IDF #include "i2c_bus_esp_idf.h" + #include #include #include @@ -9,10 +10,6 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 3, 0) -#define SOC_HP_I2C_NUM SOC_I2C_NUM -#endif - namespace esphome { namespace i2c { @@ -34,7 +31,6 @@ void IDFI2CBus::setup() { this->recover_(); -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2) next_port = (i2c_port_t) (next_port + 1); i2c_master_bus_config_t bus_conf{}; @@ -77,56 +73,8 @@ void IDFI2CBus::setup() { if (this->scan_) { ESP_LOGV(TAG, "Scanning for devices"); - this->i2c_scan(); + this->i2c_scan_(); } -#else -#if SOC_HP_I2C_NUM > 1 - next_port = (next_port == I2C_NUM_0) ? I2C_NUM_1 : I2C_NUM_MAX; -#else - next_port = I2C_NUM_MAX; -#endif - - i2c_config_t conf{}; - memset(&conf, 0, sizeof(conf)); - conf.mode = I2C_MODE_MASTER; - conf.sda_io_num = sda_pin_; - conf.sda_pullup_en = sda_pullup_enabled_; - conf.scl_io_num = scl_pin_; - conf.scl_pullup_en = scl_pullup_enabled_; - conf.master.clk_speed = frequency_; -#ifdef USE_ESP32_VARIANT_ESP32S2 - // workaround for https://github.com/esphome/issues/issues/6718 - conf.clk_flags = I2C_SCLK_SRC_FLAG_AWARE_DFS; -#endif - esp_err_t err = i2c_param_config(port_, &conf); - if (err != ESP_OK) { - ESP_LOGW(TAG, "i2c_param_config failed: %s", esp_err_to_name(err)); - this->mark_failed(); - return; - } - if (timeout_ > 0) { - err = i2c_set_timeout(port_, timeout_ * 80); // unit: APB 80MHz clock cycle - if (err != ESP_OK) { - ESP_LOGW(TAG, "i2c_set_timeout failed: %s", esp_err_to_name(err)); - this->mark_failed(); - return; - } else { - ESP_LOGV(TAG, "i2c_timeout set to %" PRIu32 " ticks (%" PRIu32 " us)", timeout_ * 80, timeout_); - } - } - err = i2c_driver_install(port_, I2C_MODE_MASTER, 0, 0, 0); - if (err != ESP_OK) { - ESP_LOGW(TAG, "i2c_driver_install failed: %s", esp_err_to_name(err)); - this->mark_failed(); - return; - } - - initialized_ = true; - if (this->scan_) { - ESP_LOGV(TAG, "Scanning bus for active devices"); - this->i2c_scan(); - } -#endif } void IDFI2CBus::dump_config() { @@ -166,267 +114,73 @@ void IDFI2CBus::dump_config() { } } -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2) -void IDFI2CBus::i2c_scan() { - for (uint8_t address = 8; address < 120; address++) { - auto err = i2c_master_probe(this->bus_, address, 20); - if (err == ESP_OK) { - this->scan_results_.emplace_back(address, true); - } - } -} -#endif - -ErrorCode IDFI2CBus::readv(uint8_t address, ReadBuffer *buffers, size_t cnt) { - // logging is only enabled with vv level, if warnings are shown the caller +ErrorCode IDFI2CBus::write_readv(uint8_t address, const uint8_t *write_buffer, size_t write_count, uint8_t *read_buffer, + size_t read_count) { + // logging is only enabled with v level, if warnings are shown the caller // should log them if (!initialized_) { - ESP_LOGVV(TAG, "i2c bus not initialized!"); + ESP_LOGW(TAG, "i2c bus not initialized!"); return ERROR_NOT_INITIALIZED; } -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2) - i2c_operation_job_t jobs[cnt + 4]; - uint8_t read = (address << 1) | I2C_MASTER_READ; - size_t last = 0, num = 0; - - jobs[num].command = I2C_MASTER_CMD_START; - num++; - - jobs[num].command = I2C_MASTER_CMD_WRITE; - jobs[num].write.ack_check = true; - jobs[num].write.data = &read; - jobs[num].write.total_bytes = 1; - num++; - - // find the last valid index - for (size_t i = 0; i < cnt; i++) { - const auto &buf = buffers[i]; - if (buf.len == 0) { - continue; + i2c_operation_job_t jobs[8]{}; + size_t num_jobs = 0; + uint8_t write_addr = (address << 1) | I2C_MASTER_WRITE; + uint8_t read_addr = (address << 1) | I2C_MASTER_READ; + ESP_LOGV(TAG, "Writing %zu bytes, reading %zu bytes", write_count, read_count); + if (read_count == 0 && write_count == 0) { + // basically just a bus probe. Send a start, address and stop + ESP_LOGV(TAG, "0x%02X BUS PROBE", address); + jobs[num_jobs++].command = I2C_MASTER_CMD_START; + jobs[num_jobs].command = I2C_MASTER_CMD_WRITE; + jobs[num_jobs].write.ack_check = true; + jobs[num_jobs].write.data = &write_addr; + jobs[num_jobs++].write.total_bytes = 1; + } else { + if (write_count != 0) { + ESP_LOGV(TAG, "0x%02X TX %s", address, format_hex_pretty(write_buffer, write_count).c_str()); + jobs[num_jobs++].command = I2C_MASTER_CMD_START; + jobs[num_jobs].command = I2C_MASTER_CMD_WRITE; + jobs[num_jobs].write.ack_check = true; + jobs[num_jobs].write.data = &write_addr; + jobs[num_jobs++].write.total_bytes = 1; + jobs[num_jobs].command = I2C_MASTER_CMD_WRITE; + jobs[num_jobs].write.ack_check = true; + jobs[num_jobs].write.data = (uint8_t *) write_buffer; + jobs[num_jobs++].write.total_bytes = write_count; } - last = i; - } - - for (size_t i = 0; i < cnt; i++) { - const auto &buf = buffers[i]; - if (buf.len == 0) { - continue; - } - if (i == last) { - // the last byte read before stop should always be a nack, - // split the last read if len is larger than 1 - if (buf.len > 1) { - jobs[num].command = I2C_MASTER_CMD_READ; - jobs[num].read.ack_value = I2C_ACK_VAL; - jobs[num].read.data = (uint8_t *) buf.data; - jobs[num].read.total_bytes = buf.len - 1; - num++; + if (read_count != 0) { + ESP_LOGV(TAG, "0x%02X RX bytes %zu", address, read_count); + jobs[num_jobs++].command = I2C_MASTER_CMD_START; + jobs[num_jobs].command = I2C_MASTER_CMD_WRITE; + jobs[num_jobs].write.ack_check = true; + jobs[num_jobs].write.data = &read_addr; + jobs[num_jobs++].write.total_bytes = 1; + if (read_count > 1) { + jobs[num_jobs].command = I2C_MASTER_CMD_READ; + jobs[num_jobs].read.ack_value = I2C_ACK_VAL; + jobs[num_jobs].read.data = read_buffer; + jobs[num_jobs++].read.total_bytes = read_count - 1; } - jobs[num].command = I2C_MASTER_CMD_READ; - jobs[num].read.ack_value = I2C_NACK_VAL; - jobs[num].read.data = (uint8_t *) buf.data + buf.len - 1; - jobs[num].read.total_bytes = 1; - num++; - } else { - jobs[num].command = I2C_MASTER_CMD_READ; - jobs[num].read.ack_value = I2C_ACK_VAL; - jobs[num].read.data = (uint8_t *) buf.data; - jobs[num].read.total_bytes = buf.len; - num++; + jobs[num_jobs].command = I2C_MASTER_CMD_READ; + jobs[num_jobs].read.ack_value = I2C_NACK_VAL; + jobs[num_jobs].read.data = read_buffer + read_count - 1; + jobs[num_jobs++].read.total_bytes = 1; } } - - jobs[num].command = I2C_MASTER_CMD_STOP; - num++; - - esp_err_t err = i2c_master_execute_defined_operations(this->dev_, jobs, num, 20); + jobs[num_jobs++].command = I2C_MASTER_CMD_STOP; + ESP_LOGV(TAG, "Sending %zu jobs", num_jobs); + esp_err_t err = i2c_master_execute_defined_operations(this->dev_, jobs, num_jobs, 20); if (err == ESP_ERR_INVALID_STATE) { - ESP_LOGVV(TAG, "RX from %02X failed: not acked", address); + ESP_LOGV(TAG, "TX to %02X failed: not acked", address); return ERROR_NOT_ACKNOWLEDGED; } else if (err == ESP_ERR_TIMEOUT) { - ESP_LOGVV(TAG, "RX from %02X failed: timeout", address); + ESP_LOGV(TAG, "TX to %02X failed: timeout", address); return ERROR_TIMEOUT; } else if (err != ESP_OK) { - ESP_LOGVV(TAG, "RX from %02X failed: %s", address, esp_err_to_name(err)); + ESP_LOGV(TAG, "TX to %02X failed: %s", address, esp_err_to_name(err)); return ERROR_UNKNOWN; } -#else - i2c_cmd_handle_t cmd = i2c_cmd_link_create(); - esp_err_t err = i2c_master_start(cmd); - if (err != ESP_OK) { - ESP_LOGVV(TAG, "RX from %02X master start failed: %s", address, esp_err_to_name(err)); - i2c_cmd_link_delete(cmd); - return ERROR_UNKNOWN; - } - err = i2c_master_write_byte(cmd, (address << 1) | I2C_MASTER_READ, true); - if (err != ESP_OK) { - ESP_LOGVV(TAG, "RX from %02X address write failed: %s", address, esp_err_to_name(err)); - i2c_cmd_link_delete(cmd); - return ERROR_UNKNOWN; - } - for (size_t i = 0; i < cnt; i++) { - const auto &buf = buffers[i]; - if (buf.len == 0) - continue; - err = i2c_master_read(cmd, buf.data, buf.len, i == cnt - 1 ? I2C_MASTER_LAST_NACK : I2C_MASTER_ACK); - if (err != ESP_OK) { - ESP_LOGVV(TAG, "RX from %02X data read failed: %s", address, esp_err_to_name(err)); - i2c_cmd_link_delete(cmd); - return ERROR_UNKNOWN; - } - } - err = i2c_master_stop(cmd); - if (err != ESP_OK) { - ESP_LOGVV(TAG, "RX from %02X stop failed: %s", address, esp_err_to_name(err)); - i2c_cmd_link_delete(cmd); - return ERROR_UNKNOWN; - } - err = i2c_master_cmd_begin(port_, cmd, 20 / portTICK_PERIOD_MS); - // i2c_master_cmd_begin() will block for a whole second if no ack: - // https://github.com/espressif/esp-idf/issues/4999 - i2c_cmd_link_delete(cmd); - if (err == ESP_FAIL) { - // transfer not acked - ESP_LOGVV(TAG, "RX from %02X failed: not acked", address); - return ERROR_NOT_ACKNOWLEDGED; - } else if (err == ESP_ERR_TIMEOUT) { - ESP_LOGVV(TAG, "RX from %02X failed: timeout", address); - return ERROR_TIMEOUT; - } else if (err != ESP_OK) { - ESP_LOGVV(TAG, "RX from %02X failed: %s", address, esp_err_to_name(err)); - return ERROR_UNKNOWN; - } -#endif - -#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE - char debug_buf[4]; - std::string debug_hex; - - for (size_t i = 0; i < cnt; i++) { - const auto &buf = buffers[i]; - for (size_t j = 0; j < buf.len; j++) { - snprintf(debug_buf, sizeof(debug_buf), "%02X", buf.data[j]); - debug_hex += debug_buf; - } - } - ESP_LOGVV(TAG, "0x%02X RX %s", address, debug_hex.c_str()); -#endif - - return ERROR_OK; -} - -ErrorCode IDFI2CBus::writev(uint8_t address, WriteBuffer *buffers, size_t cnt, bool stop) { - // logging is only enabled with vv level, if warnings are shown the caller - // should log them - if (!initialized_) { - ESP_LOGVV(TAG, "i2c bus not initialized!"); - return ERROR_NOT_INITIALIZED; - } - -#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE - char debug_buf[4]; - std::string debug_hex; - - for (size_t i = 0; i < cnt; i++) { - const auto &buf = buffers[i]; - for (size_t j = 0; j < buf.len; j++) { - snprintf(debug_buf, sizeof(debug_buf), "%02X", buf.data[j]); - debug_hex += debug_buf; - } - } - ESP_LOGVV(TAG, "0x%02X TX %s", address, debug_hex.c_str()); -#endif - -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2) - i2c_operation_job_t jobs[cnt + 3]; - uint8_t write = (address << 1) | I2C_MASTER_WRITE; - size_t num = 0; - - jobs[num].command = I2C_MASTER_CMD_START; - num++; - - jobs[num].command = I2C_MASTER_CMD_WRITE; - jobs[num].write.ack_check = true; - jobs[num].write.data = &write; - jobs[num].write.total_bytes = 1; - num++; - - for (size_t i = 0; i < cnt; i++) { - const auto &buf = buffers[i]; - if (buf.len == 0) { - continue; - } - jobs[num].command = I2C_MASTER_CMD_WRITE; - jobs[num].write.ack_check = true; - jobs[num].write.data = (uint8_t *) buf.data; - jobs[num].write.total_bytes = buf.len; - num++; - } - - if (stop) { - jobs[num].command = I2C_MASTER_CMD_STOP; - num++; - } - - esp_err_t err = i2c_master_execute_defined_operations(this->dev_, jobs, num, 20); - if (err == ESP_ERR_INVALID_STATE) { - ESP_LOGVV(TAG, "TX to %02X failed: not acked", address); - return ERROR_NOT_ACKNOWLEDGED; - } else if (err == ESP_ERR_TIMEOUT) { - ESP_LOGVV(TAG, "TX to %02X failed: timeout", address); - return ERROR_TIMEOUT; - } else if (err != ESP_OK) { - ESP_LOGVV(TAG, "TX to %02X failed: %s", address, esp_err_to_name(err)); - return ERROR_UNKNOWN; - } -#else - i2c_cmd_handle_t cmd = i2c_cmd_link_create(); - esp_err_t err = i2c_master_start(cmd); - if (err != ESP_OK) { - ESP_LOGVV(TAG, "TX to %02X master start failed: %s", address, esp_err_to_name(err)); - i2c_cmd_link_delete(cmd); - return ERROR_UNKNOWN; - } - err = i2c_master_write_byte(cmd, (address << 1) | I2C_MASTER_WRITE, true); - if (err != ESP_OK) { - ESP_LOGVV(TAG, "TX to %02X address write failed: %s", address, esp_err_to_name(err)); - i2c_cmd_link_delete(cmd); - return ERROR_UNKNOWN; - } - for (size_t i = 0; i < cnt; i++) { - const auto &buf = buffers[i]; - if (buf.len == 0) - continue; - err = i2c_master_write(cmd, buf.data, buf.len, true); - if (err != ESP_OK) { - ESP_LOGVV(TAG, "TX to %02X data write failed: %s", address, esp_err_to_name(err)); - i2c_cmd_link_delete(cmd); - return ERROR_UNKNOWN; - } - } - if (stop) { - err = i2c_master_stop(cmd); - if (err != ESP_OK) { - ESP_LOGVV(TAG, "TX to %02X master stop failed: %s", address, esp_err_to_name(err)); - i2c_cmd_link_delete(cmd); - return ERROR_UNKNOWN; - } - } - err = i2c_master_cmd_begin(port_, cmd, 20 / portTICK_PERIOD_MS); - i2c_cmd_link_delete(cmd); - if (err == ESP_FAIL) { - // transfer not acked - ESP_LOGVV(TAG, "TX to %02X failed: not acked", address); - return ERROR_NOT_ACKNOWLEDGED; - } else if (err == ESP_ERR_TIMEOUT) { - ESP_LOGVV(TAG, "TX to %02X failed: timeout", address); - return ERROR_TIMEOUT; - } else if (err != ESP_OK) { - ESP_LOGVV(TAG, "TX to %02X failed: %s", address, esp_err_to_name(err)); - return ERROR_UNKNOWN; - } -#endif return ERROR_OK; } @@ -436,8 +190,8 @@ ErrorCode IDFI2CBus::writev(uint8_t address, WriteBuffer *buffers, size_t cnt, b void IDFI2CBus::recover_() { ESP_LOGI(TAG, "Performing bus recovery"); - const gpio_num_t scl_pin = static_cast(scl_pin_); - const gpio_num_t sda_pin = static_cast(sda_pin_); + const auto scl_pin = static_cast(scl_pin_); + const auto sda_pin = static_cast(sda_pin_); // For the upcoming operations, target for a 60kHz toggle frequency. // 1000kHz is the maximum frequency for I2C running in standard-mode, @@ -545,5 +299,4 @@ void IDFI2CBus::recover_() { } // namespace i2c } // namespace esphome - #endif // USE_ESP_IDF diff --git a/esphome/components/i2c/i2c_bus_esp_idf.h b/esphome/components/i2c/i2c_bus_esp_idf.h index 4e8f86fd0c..f565be4535 100644 --- a/esphome/components/i2c/i2c_bus_esp_idf.h +++ b/esphome/components/i2c/i2c_bus_esp_idf.h @@ -2,14 +2,9 @@ #ifdef USE_ESP_IDF -#include "esp_idf_version.h" #include "esphome/core/component.h" #include "i2c_bus.h" -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2) #include -#else -#include -#endif namespace esphome { namespace i2c { @@ -24,36 +19,33 @@ class IDFI2CBus : public InternalI2CBus, public Component { public: void setup() override; void dump_config() override; - ErrorCode readv(uint8_t address, ReadBuffer *buffers, size_t cnt) override; - ErrorCode writev(uint8_t address, WriteBuffer *buffers, size_t cnt, bool stop) override; + ErrorCode write_readv(uint8_t address, const uint8_t *write_buffer, size_t write_count, uint8_t *read_buffer, + size_t read_count) override; float get_setup_priority() const override { return setup_priority::BUS; } - void set_scan(bool scan) { scan_ = scan; } - void set_sda_pin(uint8_t sda_pin) { sda_pin_ = sda_pin; } - void set_sda_pullup_enabled(bool sda_pullup_enabled) { sda_pullup_enabled_ = sda_pullup_enabled; } - void set_scl_pin(uint8_t scl_pin) { scl_pin_ = scl_pin; } - void set_scl_pullup_enabled(bool scl_pullup_enabled) { scl_pullup_enabled_ = scl_pullup_enabled; } - void set_frequency(uint32_t frequency) { frequency_ = frequency; } - void set_timeout(uint32_t timeout) { timeout_ = timeout; } + void set_scan(bool scan) { this->scan_ = scan; } + void set_sda_pin(uint8_t sda_pin) { this->sda_pin_ = sda_pin; } + void set_sda_pullup_enabled(bool sda_pullup_enabled) { this->sda_pullup_enabled_ = sda_pullup_enabled; } + void set_scl_pin(uint8_t scl_pin) { this->scl_pin_ = scl_pin; } + void set_scl_pullup_enabled(bool scl_pullup_enabled) { this->scl_pullup_enabled_ = scl_pullup_enabled; } + void set_frequency(uint32_t frequency) { this->frequency_ = frequency; } + void set_timeout(uint32_t timeout) { this->timeout_ = timeout; } - int get_port() const override { return static_cast(this->port_); } + int get_port() const override { return this->port_; } private: void recover_(); - RecoveryCode recovery_result_; + RecoveryCode recovery_result_{}; protected: -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 2) - i2c_master_dev_handle_t dev_; - i2c_master_bus_handle_t bus_; - void i2c_scan() override; -#endif - i2c_port_t port_; - uint8_t sda_pin_; - bool sda_pullup_enabled_; - uint8_t scl_pin_; - bool scl_pullup_enabled_; - uint32_t frequency_; + i2c_master_dev_handle_t dev_{}; + i2c_master_bus_handle_t bus_{}; + i2c_port_t port_{}; + uint8_t sda_pin_{}; + bool sda_pullup_enabled_{}; + uint8_t scl_pin_{}; + bool scl_pullup_enabled_{}; + uint32_t frequency_{}; uint32_t timeout_ = 0; bool initialized_ = false; }; diff --git a/esphome/components/iaqcore/iaqcore.cpp b/esphome/components/iaqcore/iaqcore.cpp index 2a84eabf75..274f9086b6 100644 --- a/esphome/components/iaqcore/iaqcore.cpp +++ b/esphome/components/iaqcore/iaqcore.cpp @@ -35,7 +35,7 @@ void IAQCore::setup() { void IAQCore::update() { uint8_t buffer[sizeof(SensorData)]; - if (this->read_register(0xB5, buffer, sizeof(buffer), false) != i2c::ERROR_OK) { + if (this->read_register(0xB5, buffer, sizeof(buffer)) != i2c::ERROR_OK) { ESP_LOGD(TAG, "Read failed"); this->status_set_warning(); this->publish_nans_(); diff --git a/esphome/components/ina2xx_i2c/ina2xx_i2c.cpp b/esphome/components/ina2xx_i2c/ina2xx_i2c.cpp index d28525635d..a363a9c12f 100644 --- a/esphome/components/ina2xx_i2c/ina2xx_i2c.cpp +++ b/esphome/components/ina2xx_i2c/ina2xx_i2c.cpp @@ -21,7 +21,7 @@ void INA2XXI2C::dump_config() { } bool INA2XXI2C::read_ina_register(uint8_t reg, uint8_t *data, size_t len) { - auto ret = this->read_register(reg, data, len, false); + auto ret = this->read_register(reg, data, len); if (ret != i2c::ERROR_OK) { ESP_LOGE(TAG, "read_ina_register_ failed. Reg=0x%02X Err=%d", reg, ret); } diff --git a/esphome/components/kmeteriso/kmeteriso.cpp b/esphome/components/kmeteriso/kmeteriso.cpp index 3aedac3f5f..d20e07460b 100644 --- a/esphome/components/kmeteriso/kmeteriso.cpp +++ b/esphome/components/kmeteriso/kmeteriso.cpp @@ -22,7 +22,7 @@ void KMeterISOComponent::setup() { this->reset_to_construction_state(); } - auto err = this->bus_->writev(this->address_, nullptr, 0); + auto err = this->bus_->write_readv(this->address_, nullptr, 0, nullptr, 0); if (err == esphome::i2c::ERROR_OK) { ESP_LOGCONFIG(TAG, "Could write to the address %d.", this->address_); } else { @@ -33,7 +33,7 @@ void KMeterISOComponent::setup() { } uint8_t read_buf[4] = {1}; - if (!this->read_bytes(KMETER_ERROR_STATUS_REG, read_buf, 1)) { + if (!this->read_register(KMETER_ERROR_STATUS_REG, read_buf, 1)) { ESP_LOGCONFIG(TAG, "Could not read from the device."); this->error_code_ = COMMUNICATION_FAILED; this->mark_failed(); diff --git a/esphome/components/lc709203f/lc709203f.cpp b/esphome/components/lc709203f/lc709203f.cpp index f711cb4f0e..7e6ac878f8 100644 --- a/esphome/components/lc709203f/lc709203f.cpp +++ b/esphome/components/lc709203f/lc709203f.cpp @@ -185,7 +185,7 @@ uint8_t Lc709203f::get_register_(uint8_t register_to_read, uint16_t *register_va // function will send a stop between the read and the write portion of the I2C // transaction. This is bad in this case and will result in reading nothing but 0xFFFF // from the registers. - return_code = this->read_register(register_to_read, &read_buffer[3], 3, false); + return_code = this->read_register(register_to_read, &read_buffer[3], 3); if (return_code != i2c::NO_ERROR) { // Error on the i2c bus this->status_set_warning( @@ -226,7 +226,7 @@ uint8_t Lc709203f::set_register_(uint8_t register_to_set, uint16_t value_to_set) for (uint8_t i = 0; i <= LC709203F_I2C_RETRY_COUNT; i++) { // Note: we don't write the first byte of the write buffer to the device. // This is done automatically by the write() function. - return_code = this->write(&write_buffer[1], 4, true); + return_code = this->write(&write_buffer[1], 4); if (return_code == i2c::NO_ERROR) { return return_code; } else { diff --git a/esphome/components/mcp4461/mcp4461.cpp b/esphome/components/mcp4461/mcp4461.cpp index 6634c5057e..55ce9b7899 100644 --- a/esphome/components/mcp4461/mcp4461.cpp +++ b/esphome/components/mcp4461/mcp4461.cpp @@ -328,7 +328,7 @@ bool Mcp4461Component::increase_wiper_(Mcp4461WiperIdx wiper) { ESP_LOGV(TAG, "Increasing wiper %u", wiper_idx); uint8_t addr = this->get_wiper_address_(wiper_idx); uint8_t reg = addr | static_cast(Mcp4461Commands::INCREMENT); - auto err = this->write(&this->address_, reg, sizeof(reg)); + auto err = this->write(&this->address_, reg); if (err != i2c::ERROR_OK) { this->error_code_ = MCP4461_STATUS_I2C_ERROR; this->status_set_warning(); @@ -359,7 +359,7 @@ bool Mcp4461Component::decrease_wiper_(Mcp4461WiperIdx wiper) { ESP_LOGV(TAG, "Decreasing wiper %u", wiper_idx); uint8_t addr = this->get_wiper_address_(wiper_idx); uint8_t reg = addr | static_cast(Mcp4461Commands::DECREMENT); - auto err = this->write(&this->address_, reg, sizeof(reg)); + auto err = this->write(&this->address_, reg); if (err != i2c::ERROR_OK) { this->error_code_ = MCP4461_STATUS_I2C_ERROR; this->status_set_warning(); diff --git a/esphome/components/mlx90614/mlx90614.cpp b/esphome/components/mlx90614/mlx90614.cpp index 24024df090..8e53b9e3c3 100644 --- a/esphome/components/mlx90614/mlx90614.cpp +++ b/esphome/components/mlx90614/mlx90614.cpp @@ -75,18 +75,18 @@ float MLX90614Component::get_setup_priority() const { return setup_priority::DAT void MLX90614Component::update() { uint8_t emissivity[3]; - if (this->read_register(MLX90614_EMISSIVITY, emissivity, 3, false) != i2c::ERROR_OK) { + if (this->read_register(MLX90614_EMISSIVITY, emissivity, 3) != i2c::ERROR_OK) { this->status_set_warning(); return; } uint8_t raw_object[3]; - if (this->read_register(MLX90614_TEMPERATURE_OBJECT_1, raw_object, 3, false) != i2c::ERROR_OK) { + if (this->read_register(MLX90614_TEMPERATURE_OBJECT_1, raw_object, 3) != i2c::ERROR_OK) { this->status_set_warning(); return; } uint8_t raw_ambient[3]; - if (this->read_register(MLX90614_TEMPERATURE_AMBIENT, raw_ambient, 3, false) != i2c::ERROR_OK) { + if (this->read_register(MLX90614_TEMPERATURE_AMBIENT, raw_ambient, 3) != i2c::ERROR_OK) { this->status_set_warning(); return; } diff --git a/esphome/components/mpl3115a2/mpl3115a2.cpp b/esphome/components/mpl3115a2/mpl3115a2.cpp index 9e8467a29b..a689149c89 100644 --- a/esphome/components/mpl3115a2/mpl3115a2.cpp +++ b/esphome/components/mpl3115a2/mpl3115a2.cpp @@ -10,7 +10,7 @@ static const char *const TAG = "mpl3115a2"; void MPL3115A2Component::setup() { uint8_t whoami = 0xFF; - if (!this->read_byte(MPL3115A2_WHOAMI, &whoami, false)) { + if (!this->read_byte(MPL3115A2_WHOAMI, &whoami)) { this->error_code_ = COMMUNICATION_FAILED; this->mark_failed(); return; @@ -54,24 +54,24 @@ void MPL3115A2Component::dump_config() { void MPL3115A2Component::update() { uint8_t mode = MPL3115A2_CTRL_REG1_OS128; - this->write_byte(MPL3115A2_CTRL_REG1, mode, true); + this->write_byte(MPL3115A2_CTRL_REG1, mode); // Trigger a new reading mode |= MPL3115A2_CTRL_REG1_OST; if (this->altitude_ != nullptr) mode |= MPL3115A2_CTRL_REG1_ALT; - this->write_byte(MPL3115A2_CTRL_REG1, mode, true); + this->write_byte(MPL3115A2_CTRL_REG1, mode); // Wait until status shows reading available uint8_t status = 0; - if (!this->read_byte(MPL3115A2_REGISTER_STATUS, &status, false) || (status & MPL3115A2_REGISTER_STATUS_PDR) == 0) { + if (!this->read_byte(MPL3115A2_REGISTER_STATUS, &status) || (status & MPL3115A2_REGISTER_STATUS_PDR) == 0) { delay(10); - if (!this->read_byte(MPL3115A2_REGISTER_STATUS, &status, false) || (status & MPL3115A2_REGISTER_STATUS_PDR) == 0) { + if (!this->read_byte(MPL3115A2_REGISTER_STATUS, &status) || (status & MPL3115A2_REGISTER_STATUS_PDR) == 0) { return; } } uint8_t buffer[5] = {0, 0, 0, 0, 0}; - this->read_register(MPL3115A2_REGISTER_PRESSURE_MSB, buffer, 5, false); + this->read_register(MPL3115A2_REGISTER_PRESSURE_MSB, buffer, 5); float altitude = 0, pressure = 0; if (this->altitude_ != nullptr) { diff --git a/esphome/components/npi19/npi19.cpp b/esphome/components/npi19/npi19.cpp index e8c4e8abd5..c531d2ec8f 100644 --- a/esphome/components/npi19/npi19.cpp +++ b/esphome/components/npi19/npi19.cpp @@ -33,7 +33,7 @@ float NPI19Component::get_setup_priority() const { return setup_priority::DATA; i2c::ErrorCode NPI19Component::read_(uint16_t &raw_temperature, uint16_t &raw_pressure) { // initiate data read from device - i2c::ErrorCode w_err = write(&READ_COMMAND, sizeof(READ_COMMAND), true); + i2c::ErrorCode w_err = write(&READ_COMMAND, sizeof(READ_COMMAND)); if (w_err != i2c::ERROR_OK) { return w_err; } diff --git a/esphome/components/opt3001/opt3001.cpp b/esphome/components/opt3001/opt3001.cpp index 2d65f1090d..f5f7ab9412 100644 --- a/esphome/components/opt3001/opt3001.cpp +++ b/esphome/components/opt3001/opt3001.cpp @@ -72,7 +72,7 @@ void OPT3001Sensor::read_lx_(const std::function &f) { } this->set_timeout("read", OPT3001_CONVERSION_TIME_800, [this, f]() { - if (this->write(&OPT3001_REG_CONFIGURATION, 1, true) != i2c::ERROR_OK) { + if (this->write(&OPT3001_REG_CONFIGURATION, 1) != i2c::ERROR_OK) { ESP_LOGW(TAG, "Starting configuration register read failed"); f(NAN); return; diff --git a/esphome/components/pca6416a/pca6416a.cpp b/esphome/components/pca6416a/pca6416a.cpp index dc8662d1a2..730c494e34 100644 --- a/esphome/components/pca6416a/pca6416a.cpp +++ b/esphome/components/pca6416a/pca6416a.cpp @@ -33,7 +33,7 @@ void PCA6416AComponent::setup() { } // Test to see if the device supports pull-up resistors - if (this->read_register(PCAL6416A_PULL_EN0, &value, 1, true) == i2c::ERROR_OK) { + if (this->read_register(PCAL6416A_PULL_EN0, &value, 1) == i2c::ERROR_OK) { this->has_pullup_ = true; } @@ -105,7 +105,7 @@ bool PCA6416AComponent::read_register_(uint8_t reg, uint8_t *value) { return false; } - this->last_error_ = this->read_register(reg, value, 1, true); + this->last_error_ = this->read_register(reg, value, 1); if (this->last_error_ != i2c::ERROR_OK) { this->status_set_warning(); ESP_LOGE(TAG, "read_register_(): I2C I/O error: %d", (int) this->last_error_); @@ -122,7 +122,7 @@ bool PCA6416AComponent::write_register_(uint8_t reg, uint8_t value) { return false; } - this->last_error_ = this->write_register(reg, &value, 1, true); + this->last_error_ = this->write_register(reg, &value, 1); if (this->last_error_ != i2c::ERROR_OK) { this->status_set_warning(); ESP_LOGE(TAG, "write_register_(): I2C I/O error: %d", (int) this->last_error_); diff --git a/esphome/components/pca9554/pca9554.cpp b/esphome/components/pca9554/pca9554.cpp index f77d680bec..1166cc1a09 100644 --- a/esphome/components/pca9554/pca9554.cpp +++ b/esphome/components/pca9554/pca9554.cpp @@ -96,7 +96,7 @@ bool PCA9554Component::read_inputs_() { return false; } - this->last_error_ = this->read_register(INPUT_REG * this->reg_width_, inputs, this->reg_width_, true); + this->last_error_ = this->read_register(INPUT_REG * this->reg_width_, inputs, this->reg_width_); if (this->last_error_ != i2c::ERROR_OK) { this->status_set_warning(); ESP_LOGE(TAG, "read_register_(): I2C I/O error: %d", (int) this->last_error_); @@ -114,7 +114,7 @@ bool PCA9554Component::write_register_(uint8_t reg, uint16_t value) { uint8_t outputs[2]; outputs[0] = (uint8_t) value; outputs[1] = (uint8_t) (value >> 8); - this->last_error_ = this->write_register(reg * this->reg_width_, outputs, this->reg_width_, true); + this->last_error_ = this->write_register(reg * this->reg_width_, outputs, this->reg_width_); if (this->last_error_ != i2c::ERROR_OK) { this->status_set_warning(); ESP_LOGE(TAG, "write_register_(): I2C I/O error: %d", (int) this->last_error_); diff --git a/esphome/components/st7567_i2c/st7567_i2c.cpp b/esphome/components/st7567_i2c/st7567_i2c.cpp index 4970367343..710e473b11 100644 --- a/esphome/components/st7567_i2c/st7567_i2c.cpp +++ b/esphome/components/st7567_i2c/st7567_i2c.cpp @@ -51,8 +51,7 @@ void HOT I2CST7567::write_display_data() { static const size_t BLOCK_SIZE = 64; for (uint8_t x = 0; x < (uint8_t) this->get_width_internal(); x += BLOCK_SIZE) { this->write_register(esphome::st7567_base::ST7567_SET_START_LINE, &buffer_[y * this->get_width_internal() + x], - this->get_width_internal() - x > BLOCK_SIZE ? BLOCK_SIZE : this->get_width_internal() - x, - true); + this->get_width_internal() - x > BLOCK_SIZE ? BLOCK_SIZE : this->get_width_internal() - x); } } } diff --git a/esphome/components/tca9548a/tca9548a.cpp b/esphome/components/tca9548a/tca9548a.cpp index edd8af9a27..1de3c49108 100644 --- a/esphome/components/tca9548a/tca9548a.cpp +++ b/esphome/components/tca9548a/tca9548a.cpp @@ -6,23 +6,15 @@ namespace tca9548a { static const char *const TAG = "tca9548a"; -i2c::ErrorCode TCA9548AChannel::readv(uint8_t address, i2c::ReadBuffer *buffers, size_t cnt) { +i2c::ErrorCode TCA9548AChannel::write_readv(uint8_t address, const uint8_t *write_buffer, size_t write_count, + uint8_t *read_buffer, size_t read_count) { auto err = this->parent_->switch_to_channel(channel_); if (err != i2c::ERROR_OK) return err; - err = this->parent_->bus_->readv(address, buffers, cnt); + err = this->parent_->bus_->write_readv(address, write_buffer, write_count, read_buffer, read_count); this->parent_->disable_all_channels(); return err; } -i2c::ErrorCode TCA9548AChannel::writev(uint8_t address, i2c::WriteBuffer *buffers, size_t cnt, bool stop) { - auto err = this->parent_->switch_to_channel(channel_); - if (err != i2c::ERROR_OK) - return err; - err = this->parent_->bus_->writev(address, buffers, cnt, stop); - this->parent_->disable_all_channels(); - return err; -} - void TCA9548AComponent::setup() { uint8_t status = 0; if (this->read(&status, 1) != i2c::ERROR_OK) { diff --git a/esphome/components/tca9548a/tca9548a.h b/esphome/components/tca9548a/tca9548a.h index 08f1674d11..0fb9ada99a 100644 --- a/esphome/components/tca9548a/tca9548a.h +++ b/esphome/components/tca9548a/tca9548a.h @@ -14,8 +14,8 @@ class TCA9548AChannel : public i2c::I2CBus { void set_channel(uint8_t channel) { channel_ = channel; } void set_parent(TCA9548AComponent *parent) { parent_ = parent; } - i2c::ErrorCode readv(uint8_t address, i2c::ReadBuffer *buffers, size_t cnt) override; - i2c::ErrorCode writev(uint8_t address, i2c::WriteBuffer *buffers, size_t cnt, bool stop) override; + i2c::ErrorCode write_readv(uint8_t address, const uint8_t *write_buffer, size_t write_count, uint8_t *read_buffer, + size_t read_count) override; protected: uint8_t channel_; diff --git a/esphome/components/tee501/tee501.cpp b/esphome/components/tee501/tee501.cpp index dc803be775..d6513dbbe0 100644 --- a/esphome/components/tee501/tee501.cpp +++ b/esphome/components/tee501/tee501.cpp @@ -9,9 +9,9 @@ static const char *const TAG = "tee501"; void TEE501Component::setup() { uint8_t address[] = {0x70, 0x29}; - this->write(address, 2, false); uint8_t identification[9]; this->read(identification, 9); + this->write_read(address, sizeof address, identification, sizeof identification); if (identification[8] != crc8(identification, 8, 0xFF, 0x31, true)) { this->error_code_ = CRC_CHECK_FAILED; this->mark_failed(); @@ -41,7 +41,7 @@ void TEE501Component::dump_config() { float TEE501Component::get_setup_priority() const { return setup_priority::DATA; } void TEE501Component::update() { uint8_t address_1[] = {0x2C, 0x1B}; - this->write(address_1, 2, true); + this->write(address_1, 2); this->set_timeout(50, [this]() { uint8_t i2c_response[3]; this->read(i2c_response, 3); diff --git a/esphome/components/tlc59208f/tlc59208f_output.cpp b/esphome/components/tlc59208f/tlc59208f_output.cpp index a524f92f75..85311a877c 100644 --- a/esphome/components/tlc59208f/tlc59208f_output.cpp +++ b/esphome/components/tlc59208f/tlc59208f_output.cpp @@ -74,7 +74,8 @@ void TLC59208FOutput::setup() { ESP_LOGV(TAG, " Resetting all devices on the bus"); // Reset all devices on the bus - if (this->bus_->write(TLC59208F_SWRST_ADDR >> 1, TLC59208F_SWRST_SEQ, 2) != i2c::ERROR_OK) { + if (this->bus_->write_readv(TLC59208F_SWRST_ADDR >> 1, TLC59208F_SWRST_SEQ, sizeof TLC59208F_SWRST_SEQ, nullptr, 0) != + i2c::ERROR_OK) { ESP_LOGE(TAG, "RESET failed"); this->mark_failed(); return; diff --git a/esphome/components/veml3235/veml3235.cpp b/esphome/components/veml3235/veml3235.cpp index f3016fb171..1e02e3e802 100644 --- a/esphome/components/veml3235/veml3235.cpp +++ b/esphome/components/veml3235/veml3235.cpp @@ -14,14 +14,12 @@ void VEML3235Sensor::setup() { this->mark_failed(); return; } - if ((this->write(&ID_REG, 1, false) != i2c::ERROR_OK) || !this->read_bytes_raw(device_id, 2)) { + if ((this->read_register(ID_REG, device_id, sizeof device_id) != i2c::ERROR_OK)) { ESP_LOGE(TAG, "Unable to read ID"); this->mark_failed(); - return; } else if (device_id[0] != DEVICE_ID) { ESP_LOGE(TAG, "Incorrect device ID - expected 0x%.2x, read 0x%.2x", DEVICE_ID, device_id[0]); this->mark_failed(); - return; } } @@ -49,7 +47,7 @@ float VEML3235Sensor::read_lx_() { } uint8_t als_regs[] = {0, 0}; - if ((this->write(&ALS_REG, 1, false) != i2c::ERROR_OK) || !this->read_bytes_raw(als_regs, 2)) { + if ((this->read_register(ALS_REG, als_regs, sizeof als_regs) != i2c::ERROR_OK)) { this->status_set_warning(); return NAN; } diff --git a/esphome/components/veml7700/veml7700.cpp b/esphome/components/veml7700/veml7700.cpp index 2a4c246ac9..c3b601e288 100644 --- a/esphome/components/veml7700/veml7700.cpp +++ b/esphome/components/veml7700/veml7700.cpp @@ -279,20 +279,18 @@ ErrorCode VEML7700Component::reconfigure_time_and_gain_(IntegrationTime time, Ga } ErrorCode VEML7700Component::read_sensor_output_(Readings &data) { - auto als_err = - this->read_register((uint8_t) CommandRegisters::ALS, (uint8_t *) &data.als_counts, VEML_REG_SIZE, false); + auto als_err = this->read_register((uint8_t) CommandRegisters::ALS, (uint8_t *) &data.als_counts, VEML_REG_SIZE); if (als_err != i2c::ERROR_OK) { ESP_LOGW(TAG, "Error reading ALS register, err = %d", als_err); } auto white_err = - this->read_register((uint8_t) CommandRegisters::WHITE, (uint8_t *) &data.white_counts, VEML_REG_SIZE, false); + this->read_register((uint8_t) CommandRegisters::WHITE, (uint8_t *) &data.white_counts, VEML_REG_SIZE); if (white_err != i2c::ERROR_OK) { ESP_LOGW(TAG, "Error reading WHITE register, err = %d", white_err); } ConfigurationRegister conf{0}; - auto err = - this->read_register((uint8_t) CommandRegisters::ALS_CONF_0, (uint8_t *) conf.raw_bytes, VEML_REG_SIZE, false); + auto err = this->read_register((uint8_t) CommandRegisters::ALS_CONF_0, (uint8_t *) conf.raw_bytes, VEML_REG_SIZE); if (err != i2c::ERROR_OK) { ESP_LOGW(TAG, "Error reading ALS_CONF_0 register, err = %d", white_err); } diff --git a/esphome/components/veml7700/veml7700.h b/esphome/components/veml7700/veml7700.h index b0d1451cf0..4b5edf733d 100644 --- a/esphome/components/veml7700/veml7700.h +++ b/esphome/components/veml7700/veml7700.h @@ -3,7 +3,6 @@ #include "esphome/components/i2c/i2c.h" #include "esphome/components/sensor/sensor.h" #include "esphome/core/component.h" -#include "esphome/core/optional.h" namespace esphome { namespace veml7700 { From e5d1c3079766bcbc14031b94ab5677a78b186258 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Thu, 28 Aug 2025 07:16:26 +1000 Subject: [PATCH 089/208] [wifi] Fix retry with hidden networks. (#10445) --- esphome/components/wifi/wifi_component.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 987e276e0c..d16c94fa13 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -151,6 +151,8 @@ void WiFiComponent::loop() { this->status_set_warning("waiting to reconnect"); if (millis() - this->action_started_ > 5000) { if (this->fast_connect_ || this->retry_hidden_) { + if (!this->selected_ap_.get_bssid().has_value()) + this->selected_ap_ = this->sta_[0]; this->start_connecting(this->selected_ap_, false); } else { this->start_scanning(); @@ -670,10 +672,12 @@ void WiFiComponent::check_connecting_finished() { return; } + ESP_LOGI(TAG, "Connected"); // We won't retry hidden networks unless a reconnect fails more than three times again + if (this->retry_hidden_ && !this->selected_ap_.get_hidden()) + ESP_LOGW(TAG, "Network '%s' should be marked as hidden", this->selected_ap_.get_ssid().c_str()); this->retry_hidden_ = false; - ESP_LOGI(TAG, "Connected"); this->print_connect_params_(); if (this->has_ap()) { From 3c7aba06813dd3a3ef85e177167b61f4e5a90390 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 27 Aug 2025 23:23:43 +0200 Subject: [PATCH 090/208] Fix AttributeError when uploading OTA to offline OpenThread devices (#10459) --- esphome/__main__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index 8e8fc7d5d9..aab3035a5e 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -132,14 +132,17 @@ def choose_upload_log_host( ] resolved.append(choose_prompt(options, purpose=purpose)) elif device == "OTA": - if (show_ota and "ota" in CORE.config) or ( - show_api and "api" in CORE.config + if CORE.address and ( + (show_ota and "ota" in CORE.config) + or (show_api and "api" in CORE.config) ): resolved.append(CORE.address) elif show_mqtt and has_mqtt_logging(): resolved.append("MQTT") else: resolved.append(device) + if not resolved: + _LOGGER.error("All specified devices: %s could not be resolved.", defaults) return resolved # No devices specified, show interactive chooser From 75595b08be2d58b3c030ec1a1b2508b48845a278 Mon Sep 17 00:00:00 2001 From: Vinicius Fortuna Date: Wed, 27 Aug 2025 21:53:57 -0400 Subject: [PATCH 091/208] [rtttl] Fix RTTTL for speakers (#10381) --- esphome/components/rtttl/rtttl.cpp | 63 +++++++++++++++++------------- esphome/components/rtttl/rtttl.h | 25 ++++++++++++ 2 files changed, 60 insertions(+), 28 deletions(-) diff --git a/esphome/components/rtttl/rtttl.cpp b/esphome/components/rtttl/rtttl.cpp index 65a3af1bbc..5aedc74489 100644 --- a/esphome/components/rtttl/rtttl.cpp +++ b/esphome/components/rtttl/rtttl.cpp @@ -138,11 +138,37 @@ void Rtttl::stop() { this->set_state_(STATE_STOPPING); } #endif + this->position_ = this->rtttl_.length(); + this->note_duration_ = 0; +} + +void Rtttl::finish_() { + ESP_LOGV(TAG, "Rtttl::finish_()"); +#ifdef USE_OUTPUT + if (this->output_ != nullptr) { + this->output_->set_level(0.0); + this->set_state_(State::STATE_STOPPED); + } +#endif +#ifdef USE_SPEAKER + if (this->speaker_ != nullptr) { + SpeakerSample sample[2]; + sample[0].left = 0; + sample[0].right = 0; + sample[1].left = 0; + sample[1].right = 0; + this->speaker_->play((uint8_t *) (&sample), 8); + this->speaker_->finish(); + this->set_state_(State::STATE_STOPPING); + } +#endif + // Ensure no more notes are played in case finish_() is called for an error. + this->position_ = this->rtttl_.length(); this->note_duration_ = 0; } void Rtttl::loop() { - if (this->note_duration_ == 0 || this->state_ == State::STATE_STOPPED) { + if (this->state_ == State::STATE_STOPPED) { this->disable_loop(); return; } @@ -152,6 +178,8 @@ void Rtttl::loop() { if (this->state_ == State::STATE_STOPPING) { if (this->speaker_->is_stopped()) { this->set_state_(State::STATE_STOPPED); + } else { + return; } } else if (this->state_ == State::STATE_INIT) { if (this->speaker_->is_stopped()) { @@ -207,7 +235,7 @@ void Rtttl::loop() { if (this->output_ != nullptr && millis() - this->last_note_ < this->note_duration_) return; #endif - if (!this->rtttl_[this->position_]) { + if (this->position_ >= this->rtttl_.length()) { this->finish_(); return; } @@ -346,31 +374,6 @@ void Rtttl::loop() { this->last_note_ = millis(); } -void Rtttl::finish_() { -#ifdef USE_OUTPUT - if (this->output_ != nullptr) { - this->output_->set_level(0.0); - this->set_state_(State::STATE_STOPPED); - } -#endif -#ifdef USE_SPEAKER - if (this->speaker_ != nullptr) { - SpeakerSample sample[2]; - sample[0].left = 0; - sample[0].right = 0; - sample[1].left = 0; - sample[1].right = 0; - this->speaker_->play((uint8_t *) (&sample), 8); - - this->speaker_->finish(); - this->set_state_(State::STATE_STOPPING); - } -#endif - this->note_duration_ = 0; - this->on_finished_playback_callback_.call(); - ESP_LOGD(TAG, "Playback finished"); -} - #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG static const LogString *state_to_string(State state) { switch (state) { @@ -397,7 +400,11 @@ void Rtttl::set_state_(State state) { LOG_STR_ARG(state_to_string(state))); // Clear loop_done when transitioning from STOPPED to any other state - if (old_state == State::STATE_STOPPED && state != State::STATE_STOPPED) { + if (state == State::STATE_STOPPED) { + this->disable_loop(); + this->on_finished_playback_callback_.call(); + ESP_LOGD(TAG, "Playback finished"); + } else if (old_state == State::STATE_STOPPED) { this->enable_loop(); } } diff --git a/esphome/components/rtttl/rtttl.h b/esphome/components/rtttl/rtttl.h index 420948bfbf..d536c6c08e 100644 --- a/esphome/components/rtttl/rtttl.h +++ b/esphome/components/rtttl/rtttl.h @@ -60,35 +60,60 @@ class Rtttl : public Component { } return ret; } + /** + * @brief Finalizes the playback of the RTTTL string. + * + * This method is called internally when the end of the RTTTL string is reached + * or when a parsing error occurs. It stops the output, sets the component state, + * and triggers the on_finished_playback_callback_. + */ void finish_(); void set_state_(State state); + /// The RTTTL string to play. std::string rtttl_{""}; + /// The current position in the RTTTL string. size_t position_{0}; + /// The duration of a whole note in milliseconds. uint16_t wholenote_; + /// The default duration of a note (e.g. 4 for a quarter note). uint16_t default_duration_; + /// The default octave for a note. uint16_t default_octave_; + /// The time the last note was started. uint32_t last_note_; + /// The duration of the current note in milliseconds. uint16_t note_duration_; + /// The frequency of the current note in Hz. uint32_t output_freq_; + /// The gain of the output. float gain_{0.6f}; + /// The current state of the RTTTL player. State state_{State::STATE_STOPPED}; #ifdef USE_OUTPUT + /// The output to write the sound to. output::FloatOutput *output_; #endif #ifdef USE_SPEAKER + /// The speaker to write the sound to. speaker::Speaker *speaker_{nullptr}; + /// The sample rate of the speaker. int sample_rate_{16000}; + /// The number of samples for one full cycle of a note's waveform, in Q10 fixed-point format. int samples_per_wave_{0}; + /// The number of samples sent. int samples_sent_{0}; + /// The total number of samples to send. int samples_count_{0}; + /// The number of samples for the gap between notes. int samples_gap_{0}; #endif + /// The callback to call when playback is finished. CallbackManager on_finished_playback_callback_; }; From a92a08c2de7736369f533679b0685e07ad59d990 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 28 Aug 2025 13:40:36 -0500 Subject: [PATCH 092/208] [api] Fix string lifetime issue in fill_and_encode_entity_info for dynamic object_id (#10482) --- esphome/components/api/api_connection.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 6254854238..72254d1536 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -303,11 +303,13 @@ class APIConnection final : public APIServerConnection { msg.key = entity->get_object_id_hash(); // Try to use static reference first to avoid allocation StringRef static_ref = entity->get_object_id_ref_for_api_(); + // Store dynamic string outside the if-else to maintain lifetime + std::string object_id; if (!static_ref.empty()) { msg.set_object_id(static_ref); } else { // Dynamic case - need to allocate - std::string object_id = entity->get_object_id(); + object_id = entity->get_object_id(); msg.set_object_id(StringRef(object_id)); } From 2f2f2f7d15b78cf9ed96fc9078dc3e2c9c2fb82e Mon Sep 17 00:00:00 2001 From: DAVe3283 Date: Thu, 28 Aug 2025 16:04:19 -0600 Subject: [PATCH 093/208] [absolute_humidity] Fix typo (#10474) --- .../components/absolute_humidity/absolute_humidity.cpp | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/esphome/components/absolute_humidity/absolute_humidity.cpp b/esphome/components/absolute_humidity/absolute_humidity.cpp index b8717ac5f1..7ba3c5a1ab 100644 --- a/esphome/components/absolute_humidity/absolute_humidity.cpp +++ b/esphome/components/absolute_humidity/absolute_humidity.cpp @@ -61,11 +61,10 @@ void AbsoluteHumidityComponent::loop() { ESP_LOGW(TAG, "No valid state from temperature sensor!"); } if (no_humidity) { - ESP_LOGW(TAG, "No valid state from temperature sensor!"); + ESP_LOGW(TAG, "No valid state from humidity sensor!"); } - ESP_LOGW(TAG, "Unable to calculate absolute humidity."); this->publish_state(NAN); - this->status_set_warning(); + this->status_set_warning("Unable to calculate absolute humidity."); return; } @@ -87,9 +86,8 @@ void AbsoluteHumidityComponent::loop() { es = es_wobus(temperature_c); break; default: - ESP_LOGE(TAG, "Invalid saturation vapor pressure equation selection!"); this->publish_state(NAN); - this->status_set_error(); + this->status_set_error("Invalid saturation vapor pressure equation selection!"); return; } ESP_LOGD(TAG, "Saturation vapor pressure %f kPa", es); From d4c11dac8c19e78be78efc3dd63096c07d39a9d7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 28 Aug 2025 17:12:38 -0500 Subject: [PATCH 094/208] [esphome] Fix OTA watchdog resets by validating all magic bytes before blocking (#10401) --- .../components/esphome/ota/ota_esphome.cpp | 77 ++++++++++--------- esphome/components/esphome/ota/ota_esphome.h | 8 +- 2 files changed, 45 insertions(+), 40 deletions(-) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index 5217e9c61f..fc10e5366e 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -100,8 +100,8 @@ void ESPHomeOTAComponent::handle_handshake_() { /// Handle the initial OTA handshake. /// /// This method is non-blocking and will return immediately if no data is available. - /// It waits for the first magic byte (0x6C) before proceeding to handle_data_(). - /// A 10-second timeout is enforced from initial connection. + /// It reads all 5 magic bytes (0x6C, 0x26, 0xF7, 0x5C, 0x45) non-blocking + /// before proceeding to handle_data_(). A 10-second timeout is enforced from initial connection. if (this->client_ == nullptr) { // We already checked server_->ready() in loop(), so we can accept directly @@ -126,6 +126,7 @@ void ESPHomeOTAComponent::handle_handshake_() { } this->log_start_("handshake"); this->client_connect_time_ = App.get_loop_component_start_time(); + this->magic_buf_pos_ = 0; // Reset magic buffer position } // Check for handshake timeout @@ -136,34 +137,47 @@ void ESPHomeOTAComponent::handle_handshake_() { return; } - // Try to read first byte of magic bytes - uint8_t first_byte; - ssize_t read = this->client_->read(&first_byte, 1); + // Try to read remaining magic bytes + if (this->magic_buf_pos_ < 5) { + // Read as many bytes as available + uint8_t bytes_to_read = 5 - this->magic_buf_pos_; + ssize_t read = this->client_->read(this->magic_buf_ + this->magic_buf_pos_, bytes_to_read); - if (read == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) { - return; // No data yet, try again next loop - } - - if (read <= 0) { - // Error or connection closed - if (read == -1) { - this->log_socket_error_("reading first byte"); - } else { - ESP_LOGW(TAG, "Remote closed during handshake"); + if (read == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) { + return; // No data yet, try again next loop } - this->cleanup_connection_(); - return; + + if (read <= 0) { + // Error or connection closed + if (read == -1) { + this->log_socket_error_("reading magic bytes"); + } else { + ESP_LOGW(TAG, "Remote closed during handshake"); + } + this->cleanup_connection_(); + return; + } + + this->magic_buf_pos_ += read; } - // Got first byte, check if it's the magic byte - if (first_byte != 0x6C) { - ESP_LOGW(TAG, "Invalid initial byte: 0x%02X", first_byte); - this->cleanup_connection_(); - return; - } + // Check if we have all 5 magic bytes + if (this->magic_buf_pos_ == 5) { + // Validate magic bytes + static const uint8_t MAGIC_BYTES[5] = {0x6C, 0x26, 0xF7, 0x5C, 0x45}; + if (memcmp(this->magic_buf_, MAGIC_BYTES, 5) != 0) { + ESP_LOGW(TAG, "Magic bytes mismatch! 0x%02X-0x%02X-0x%02X-0x%02X-0x%02X", this->magic_buf_[0], + this->magic_buf_[1], this->magic_buf_[2], this->magic_buf_[3], this->magic_buf_[4]); + // Send error response (non-blocking, best effort) + uint8_t error = static_cast(ota::OTA_RESPONSE_ERROR_MAGIC); + this->client_->write(&error, 1); + this->cleanup_connection_(); + return; + } - // First byte is valid, continue with data handling - this->handle_data_(); + // All 5 magic bytes are valid, continue with data handling + this->handle_data_(); + } } void ESPHomeOTAComponent::handle_data_() { @@ -186,18 +200,6 @@ void ESPHomeOTAComponent::handle_data_() { size_t size_acknowledged = 0; #endif - // Read remaining 4 bytes of magic (we already read the first byte 0x6C in handle_handshake_) - if (!this->readall_(buf, 4)) { - this->log_read_error_("magic bytes"); - goto error; // NOLINT(cppcoreguidelines-avoid-goto) - } - // Check remaining magic bytes: 0x26, 0xF7, 0x5C, 0x45 - if (buf[0] != 0x26 || buf[1] != 0xF7 || buf[2] != 0x5C || buf[3] != 0x45) { - ESP_LOGW(TAG, "Magic bytes mismatch! 0x6C-0x%02X-0x%02X-0x%02X-0x%02X", buf[0], buf[1], buf[2], buf[3]); - error_code = ota::OTA_RESPONSE_ERROR_MAGIC; - goto error; // NOLINT(cppcoreguidelines-avoid-goto) - } - // Send OK and version - 2 bytes buf[0] = ota::OTA_RESPONSE_OK; buf[1] = USE_OTA_VERSION; @@ -487,6 +489,7 @@ void ESPHomeOTAComponent::cleanup_connection_() { this->client_->close(); this->client_ = nullptr; this->client_connect_time_ = 0; + this->magic_buf_pos_ = 0; } void ESPHomeOTAComponent::yield_and_feed_watchdog_() { diff --git a/esphome/components/esphome/ota/ota_esphome.h b/esphome/components/esphome/ota/ota_esphome.h index 8397b86528..c1919c71e9 100644 --- a/esphome/components/esphome/ota/ota_esphome.h +++ b/esphome/components/esphome/ota/ota_esphome.h @@ -41,11 +41,13 @@ class ESPHomeOTAComponent : public ota::OTAComponent { std::string password_; #endif // USE_OTA_PASSWORD - uint16_t port_; - uint32_t client_connect_time_{0}; - std::unique_ptr server_; std::unique_ptr client_; + + uint32_t client_connect_time_{0}; + uint16_t port_; + uint8_t magic_buf_[5]; + uint8_t magic_buf_pos_{0}; }; } // namespace esphome From a7786b75a0670898d189b2cebd2160c69fc0fb64 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 28 Aug 2025 17:14:51 -0500 Subject: [PATCH 095/208] [esp32_ble_tracker] Remove duplicate client promotion logic (#10321) --- .../esp32_ble_tracker/esp32_ble_tracker.cpp | 32 ++----------------- .../esp32_ble_tracker/esp32_ble_tracker.h | 7 +--- 2 files changed, 3 insertions(+), 36 deletions(-) diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index 0455d136df..00bd1fe34c 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -307,14 +307,7 @@ void ESP32BLETracker::gap_scan_event_handler(const BLEScanResult &scan_result) { if (scan_result.search_evt == ESP_GAP_SEARCH_INQ_RES_EVT) { // Process the scan result immediately - bool found_discovered_client = this->process_scan_result_(scan_result); - - // If we found a discovered client that needs promotion, stop scanning - // This replaces the promote_to_connecting logic from loop() - if (found_discovered_client && this->scanner_state_ == ScannerState::RUNNING) { - ESP_LOGD(TAG, "Found discovered client, stopping scan for connection"); - this->stop_scan_(); - } + this->process_scan_result_(scan_result); } else if (scan_result.search_evt == ESP_GAP_SEARCH_INQ_CMPL_EVT) { // Scan finished on its own if (this->scanner_state_ != ScannerState::RUNNING) { @@ -720,20 +713,9 @@ bool ESPBTDevice::resolve_irk(const uint8_t *irk) const { ecb_ciphertext[13] == ((addr64 >> 16) & 0xff); } -bool ESP32BLETracker::has_connecting_clients_() const { - for (auto *client : this->clients_) { - auto state = client->state(); - if (state == ClientState::CONNECTING || state == ClientState::READY_TO_CONNECT) { - return true; - } - } - return false; -} #endif // USE_ESP32_BLE_DEVICE -bool ESP32BLETracker::process_scan_result_(const BLEScanResult &scan_result) { - bool found_discovered_client = false; - +void ESP32BLETracker::process_scan_result_(const BLEScanResult &scan_result) { // Process raw advertisements if (this->raw_advertisements_) { for (auto *listener : this->listeners_) { @@ -759,14 +741,6 @@ bool ESP32BLETracker::process_scan_result_(const BLEScanResult &scan_result) { for (auto *client : this->clients_) { if (client->parse_device(device)) { found = true; - // Check if this client is discovered and needs promotion - if (client->state() == ClientState::DISCOVERED) { - // Only check for connecting clients if we found a discovered client - // This matches the original logic: !connecting && client->state() == DISCOVERED - if (!this->has_connecting_clients_()) { - found_discovered_client = true; - } - } } } @@ -775,8 +749,6 @@ bool ESP32BLETracker::process_scan_result_(const BLEScanResult &scan_result) { } #endif // USE_ESP32_BLE_DEVICE } - - return found_discovered_client; } void ESP32BLETracker::cleanup_scan_state_(bool is_stop_complete) { diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index 3022eb25d2..763fa9f1c6 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -292,12 +292,7 @@ class ESP32BLETracker : public Component, /// Common cleanup logic when transitioning scanner to IDLE state void cleanup_scan_state_(bool is_stop_complete); /// Process a single scan result immediately - /// Returns true if a discovered client needs promotion to READY_TO_CONNECT - bool process_scan_result_(const BLEScanResult &scan_result); -#ifdef USE_ESP32_BLE_DEVICE - /// Check if any clients are in connecting or ready to connect state - bool has_connecting_clients_() const; -#endif + void process_scan_result_(const BLEScanResult &scan_result); /// Handle scanner failure states void handle_scanner_failure_(); /// Try to promote discovered clients to ready to connect From c526ab9a3f49cfc48632f50639ccb9f3c6af6aab Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 28 Aug 2025 22:20:23 +0000 Subject: [PATCH 096/208] Bump ruff from 0.12.10 to 0.12.11 (#10483) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston --- .pre-commit-config.yaml | 2 +- requirements_test.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8b76523b2e..e05733ec96 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ ci: repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.12.10 + rev: v0.12.11 hooks: # Run the linter. - id: ruff diff --git a/requirements_test.txt b/requirements_test.txt index f55618c0f8..22f58fd3d7 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==3.3.8 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating -ruff==0.12.10 # also change in .pre-commit-config.yaml when updating +ruff==0.12.11 # also change in .pre-commit-config.yaml when updating pyupgrade==3.20.0 # also change in .pre-commit-config.yaml when updating pre-commit From 5dc691874b2139e0b6468d4559ed80e1eba3f17e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 28 Aug 2025 17:30:14 -0500 Subject: [PATCH 097/208] [bluetooth_proxy] Remove unused ClientState::SEARCHING state (#10318) --- .../bluetooth_proxy/bluetooth_proxy.cpp | 16 +++++++------- .../esp32_ble_client/ble_client_base.cpp | 5 ++--- .../esp32_ble_tracker/esp32_ble_tracker.cpp | 21 +++++++------------ .../esp32_ble_tracker/esp32_ble_tracker.h | 9 +------- 4 files changed, 19 insertions(+), 32 deletions(-) diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index 723466a5ff..80b7fbe960 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -183,6 +183,12 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest this->send_device_connection(msg.address, false); return; } + if (!msg.has_address_type) { + ESP_LOGE(TAG, "[%d] [%s] Missing address type in connect request", connection->get_connection_index(), + connection->address_str().c_str()); + this->send_device_connection(msg.address, false); + return; + } if (connection->state() == espbt::ClientState::CONNECTED || connection->state() == espbt::ClientState::ESTABLISHED) { this->log_connection_request_ignored_(connection, connection->state()); @@ -209,13 +215,9 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest connection->set_connection_type(espbt::ConnectionType::V3_WITHOUT_CACHE); this->log_connection_info_(connection, "v3 without cache"); } - if (msg.has_address_type) { - uint64_to_bd_addr(msg.address, connection->remote_bda_); - connection->set_remote_addr_type(static_cast(msg.address_type)); - connection->set_state(espbt::ClientState::DISCOVERED); - } else { - connection->set_state(espbt::ClientState::SEARCHING); - } + uint64_to_bd_addr(msg.address, connection->remote_bda_); + connection->set_remote_addr_type(static_cast(msg.address_type)); + connection->set_state(espbt::ClientState::DISCOVERED); this->send_connections_free(); break; } diff --git a/esphome/components/esp32_ble_client/ble_client_base.cpp b/esphome/components/esp32_ble_client/ble_client_base.cpp index 4805855960..af5162afb0 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.cpp +++ b/esphome/components/esp32_ble_client/ble_client_base.cpp @@ -93,7 +93,7 @@ bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) { return false; if (this->address_ == 0 || device.address_uint64() != this->address_) return false; - if (this->state_ != espbt::ClientState::IDLE && this->state_ != espbt::ClientState::SEARCHING) + if (this->state_ != espbt::ClientState::IDLE) return false; this->log_event_("Found device"); @@ -168,8 +168,7 @@ void BLEClientBase::unconditional_disconnect() { this->log_gattc_warning_("esp_ble_gattc_close", err); } - if (this->state_ == espbt::ClientState::SEARCHING || this->state_ == espbt::ClientState::READY_TO_CONNECT || - this->state_ == espbt::ClientState::DISCOVERED) { + if (this->state_ == espbt::ClientState::READY_TO_CONNECT || this->state_ == espbt::ClientState::DISCOVERED) { this->set_address(0); this->set_state(espbt::ClientState::IDLE); } else { diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index 00bd1fe34c..0edde169eb 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -49,8 +49,6 @@ const char *client_state_to_string(ClientState state) { return "DISCONNECTING"; case ClientState::IDLE: return "IDLE"; - case ClientState::SEARCHING: - return "SEARCHING"; case ClientState::DISCOVERED: return "DISCOVERED"; case ClientState::READY_TO_CONNECT: @@ -136,9 +134,8 @@ void ESP32BLETracker::loop() { ClientStateCounts counts = this->count_client_states_(); if (counts != this->client_state_counts_) { this->client_state_counts_ = counts; - ESP_LOGD(TAG, "connecting: %d, discovered: %d, searching: %d, disconnecting: %d", - this->client_state_counts_.connecting, this->client_state_counts_.discovered, - this->client_state_counts_.searching, this->client_state_counts_.disconnecting); + ESP_LOGD(TAG, "connecting: %d, discovered: %d, disconnecting: %d", this->client_state_counts_.connecting, + this->client_state_counts_.discovered, this->client_state_counts_.disconnecting); } if (this->scanner_state_ == ScannerState::FAILED || @@ -158,10 +155,8 @@ void ESP32BLETracker::loop() { https://github.com/espressif/esp-idf/issues/6688 */ - bool promote_to_connecting = counts.discovered && !counts.searching && !counts.connecting; - if (this->scanner_state_ == ScannerState::IDLE && !counts.connecting && !counts.disconnecting && - !promote_to_connecting) { + if (this->scanner_state_ == ScannerState::IDLE && !counts.connecting && !counts.disconnecting && !counts.discovered) { #ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE this->update_coex_preference_(false); #endif @@ -170,12 +165,11 @@ void ESP32BLETracker::loop() { } } // If there is a discovered client and no connecting - // clients and no clients using the scanner to search for - // devices, then promote the discovered client to ready to connect. + // clients, then promote the discovered client to ready to connect. // We check both RUNNING and IDLE states because: // - RUNNING: gap_scan_event_handler initiates stop_scan_() but promotion can happen immediately // - IDLE: Scanner has already stopped (naturally or by gap_scan_event_handler) - if (promote_to_connecting && + if (counts.discovered && !counts.connecting && (this->scanner_state_ == ScannerState::RUNNING || this->scanner_state_ == ScannerState::IDLE)) { this->try_promote_discovered_clients_(); } @@ -633,9 +627,8 @@ void ESP32BLETracker::dump_config() { this->scan_duration_, this->scan_interval_ * 0.625f, this->scan_window_ * 0.625f, this->scan_active_ ? "ACTIVE" : "PASSIVE", YESNO(this->scan_continuous_)); ESP_LOGCONFIG(TAG, " Scanner State: %s", this->scanner_state_to_string_(this->scanner_state_)); - ESP_LOGCONFIG(TAG, " Connecting: %d, discovered: %d, searching: %d, disconnecting: %d", - this->client_state_counts_.connecting, this->client_state_counts_.discovered, - this->client_state_counts_.searching, this->client_state_counts_.disconnecting); + ESP_LOGCONFIG(TAG, " Connecting: %d, discovered: %d, disconnecting: %d", this->client_state_counts_.connecting, + this->client_state_counts_.discovered, this->client_state_counts_.disconnecting); if (this->scan_start_fail_count_) { ESP_LOGCONFIG(TAG, " Scan Start Fail Count: %d", this->scan_start_fail_count_); } diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index 763fa9f1c6..dd67156108 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -141,12 +141,10 @@ class ESPBTDeviceListener { struct ClientStateCounts { uint8_t connecting = 0; uint8_t discovered = 0; - uint8_t searching = 0; uint8_t disconnecting = 0; bool operator==(const ClientStateCounts &other) const { - return connecting == other.connecting && discovered == other.discovered && searching == other.searching && - disconnecting == other.disconnecting; + return connecting == other.connecting && discovered == other.discovered && disconnecting == other.disconnecting; } bool operator!=(const ClientStateCounts &other) const { return !(*this == other); } @@ -159,8 +157,6 @@ enum class ClientState : uint8_t { DISCONNECTING, // Connection is idle, no device detected. IDLE, - // Searching for device. - SEARCHING, // Device advertisement found. DISCOVERED, // Device is discovered and the scanner is stopped @@ -316,9 +312,6 @@ class ESP32BLETracker : public Component, case ClientState::DISCOVERED: counts.discovered++; break; - case ClientState::SEARCHING: - counts.searching++; - break; case ClientState::CONNECTING: case ClientState::READY_TO_CONNECT: counts.connecting++; From cde00a1f4c072ea170c0f5182dc2718dc989c61b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 29 Aug 2025 11:02:15 +1200 Subject: [PATCH 098/208] Bump esphome-dashboard from 20250814.0 to 20250828.0 (#10484) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 2665211381..910f70fe45 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ pyserial==3.5 platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile esptool==5.0.2 click==8.1.7 -esphome-dashboard==20250814.0 +esphome-dashboard==20250828.0 aioesphomeapi==39.0.0 zeroconf==0.147.0 puremagic==1.30 From bc960cf6d2b48ea5ace544dcd73fa9dd27fdea20 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Fri, 29 Aug 2025 10:52:37 +1000 Subject: [PATCH 099/208] [mapping] Use custom allocator (#9972) --- esphome/components/mapping/__init__.py | 18 +++-- esphome/components/mapping/mapping.h | 69 ++++++++++++++++++ tests/components/mapping/.gitattributes | 1 + tests/components/mapping/common.yaml | 19 ++++- tests/components/mapping/helvetica.ttf | Bin 0 -> 83644 bytes tests/components/mapping/test.esp32-ard.yaml | 18 ++--- .../components/mapping/test.esp32-c3-ard.yaml | 14 ++-- .../components/mapping/test.esp32-c3-idf.yaml | 14 ++-- tests/components/mapping/test.esp32-idf.yaml | 14 ++-- .../components/mapping/test.esp8266-ard.yaml | 14 ++-- tests/components/mapping/test.host.yaml | 16 ++-- tests/components/mapping/test.rp2040-ard.yaml | 14 ++-- 12 files changed, 152 insertions(+), 59 deletions(-) create mode 100644 esphome/components/mapping/mapping.h create mode 100644 tests/components/mapping/.gitattributes create mode 100644 tests/components/mapping/helvetica.ttf diff --git a/esphome/components/mapping/__init__.py b/esphome/components/mapping/__init__.py index 79657084fa..94c7c10a82 100644 --- a/esphome/components/mapping/__init__.py +++ b/esphome/components/mapping/__init__.py @@ -10,7 +10,8 @@ from esphome.loader import get_component CODEOWNERS = ["@clydebarrow"] MULTI_CONF = True -map_ = cg.std_ns.class_("map") +mapping_ns = cg.esphome_ns.namespace("mapping") +mapping_class = mapping_ns.class_("Mapping") CONF_ENTRIES = "entries" CONF_CLASS = "class" @@ -29,7 +30,11 @@ class IndexType: INDEX_TYPES = { "int": IndexType(cv.int_, cg.int_, int), - "string": IndexType(cv.string, cg.std_string, str), + "string": IndexType( + cv.string, + cg.std_string, + str, + ), } @@ -47,7 +52,7 @@ def to_schema(value): BASE_SCHEMA = cv.Schema( { - cv.Required(CONF_ID): cv.declare_id(map_), + cv.Required(CONF_ID): cv.declare_id(mapping_class), cv.Required(CONF_FROM): cv.one_of(*INDEX_TYPES, lower=True), cv.Required(CONF_TO): cv.string, }, @@ -123,12 +128,15 @@ async def to_code(config): if list(entries.values())[0].op != ".": value_type = value_type.operator("ptr") varid = config[CONF_ID] - varid.type = map_.template(index_type, value_type) + varid.type = mapping_class.template( + index_type, + value_type, + ) var = MockObj(varid, ".") decl = VariableDeclarationExpression(varid.type, "", varid) add_global(decl) CORE.register_variable(varid, var) for key, value in entries.items(): - cg.add(var.insert((key, value))) + cg.add(var.set(key, value)) return var diff --git a/esphome/components/mapping/mapping.h b/esphome/components/mapping/mapping.h new file mode 100644 index 0000000000..99c1f38829 --- /dev/null +++ b/esphome/components/mapping/mapping.h @@ -0,0 +1,69 @@ +#pragma once + +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" +#include +#include + +namespace esphome::mapping { + +using alloc_string_t = std::basic_string, RAMAllocator>; + +/** + * + * Mapping class with custom allocator. + * Additionally, when std::string is used as key or value, it will be replaced with a custom string type + * that uses RAMAllocator. + * @tparam K The type of the key in the mapping. + * @tparam V The type of the value in the mapping. Should be a basic type or pointer. + */ + +static const char *const TAG = "mapping"; + +template class Mapping { + public: + // Constructor + Mapping() = default; + + using key_t = const std::conditional_t, + alloc_string_t, // if K is std::string, custom string type + K>; + using value_t = std::conditional_t, + alloc_string_t, // if V is std::string, custom string type + V>; + + void set(const K &key, const V &value) { this->map_[key_t{key}] = value; } + + V get(const K &key) const { + auto it = this->map_.find(key_t{key}); + if (it != this->map_.end()) { + return V{it->second}; + } + if constexpr (std::is_pointer_v) { + esph_log_e(TAG, "Key '%p' not found in mapping", key); + } else if constexpr (std::is_same_v) { + esph_log_e(TAG, "Key '%s' not found in mapping", key.c_str()); + } else { + esph_log_e(TAG, "Key '%s' not found in mapping", to_string(key).c_str()); + } + return {}; + } + + // index map overload + V operator[](K key) { return this->get(key); } + + // convenience function for strings to get a C-style string + template, int> = 0> + const char *operator[](K key) const { + auto it = this->map_.find(key_t{key}); + if (it != this->map_.end()) { + return it->second.c_str(); // safe since value remains in map + } + return ""; + } + + protected: + std::map, RAMAllocator>> map_; +}; + +} // namespace esphome::mapping diff --git a/tests/components/mapping/.gitattributes b/tests/components/mapping/.gitattributes new file mode 100644 index 0000000000..9d74867fcf --- /dev/null +++ b/tests/components/mapping/.gitattributes @@ -0,0 +1 @@ +*.ttf -text diff --git a/tests/components/mapping/common.yaml b/tests/components/mapping/common.yaml index 07ca458146..7ffcfa4f67 100644 --- a/tests/components/mapping/common.yaml +++ b/tests/components/mapping/common.yaml @@ -50,6 +50,14 @@ mapping: red: red_id blue: blue_id green: green_id + - id: string_map_2 + from: string + to: string + entries: + one: "one" + two: "two" + three: "three" + seventy-seven: "seventy-seven" color: - id: red_id @@ -65,7 +73,14 @@ color: green: 0.0 blue: 1.0 +font: + - file: "$component_dir/helvetica.ttf" + id: font_id + size: 20 + display: lambda: |- - it.image(0, 0, id(weather_map)[0]); - it.image(0, 100, id(weather_map)[1]); + std::string value = id(int_map)[2]; + it.print(0, 0, id(font_id), TextAlign::TOP_LEFT, value.c_str()); + it.image(0, 0, id(weather_map)["clear-night"]); + it.image(0, 100, id(weather_map)["sunny"]); diff --git a/tests/components/mapping/helvetica.ttf b/tests/components/mapping/helvetica.ttf new file mode 100644 index 0000000000000000000000000000000000000000..7aec6f3f3cc74a7138ce350def27a2209b44ad89 GIT binary patch literal 83644 zcmd?S2Y4LSy*_--%=S9lyR@rSv9z}3wJcl0vbDA(<3cgUCXxb)gk`V+n_|4l3Yj}864tp4Kc*DPz-_8EACAp&9+A0d(P~xMoF$YH;vl_5BwZ(;T03>_4_4lSGs9R&7vjZ%kcq{@Qh;)5%$=?q#g} zf{RvM(D>reVjMq!`XvYzZ>Dz;J#pwC)YsuHBHE|;7CB3BoAiR!pc4c^6m+aQk!c#19JkpR1?PI^$aU2FmZwUjrJp5%RgvsGCekmrlQfkohalyJW?>Rm;2cmyF_% zBuCYiE6%%UT0Oy+@jrnu`V-;>{jn#M5QAW0Cvc#ZumkOn5+T1=_$-th`KjG0;5&7A zTLUo?6EPDDu@W1xqqUtxA}-=49z54a{3JkvBt*g_LZT!_N=PY*lLRRvNm5QKNF}Kv z6G%0wAt^GEOd^v>Etx{nWGbm6(?~s;PG*n>(nyXYGf5MfMP`#`yxtr#m$Z;p5M~}Z zp3El;$U?G+oIp+_Cy|rMVsZ*uLQW-1$ue>p`37kx9i)?dlblY@Am1Y2CS7=?d&&Le zLGmzpk~~9RB7>x#Y$ZP++sO{Hle|h^A-l+r$!_u!@>B9U*+bqWZ;+pnx5!@d4%tWE zCGXLtDBZax{RJp7t>Q|JGqlQLT)FIl84A9@;&l6d5kUF0>gpL|L_ zBflh{lP}1x$-k3PN+_iQ6{(IIsf{|Qhx%y*H7=t`nxa!^n$DoJ@r)Buzi-lR{`Tk6 zwW#x5!cT;s3LlF@ac?{vFNv4ME8|n*$HiOY>*CKR$`X}jLYbk=S>`VDm4(Yn%2H+X z%eu-|RJ{J5|M-J&Pymk;@XjW**o(ZTJI2-Y$7rjun*NIXh76CZsU9`8QzvQ~0CibS zYv^RubUJNB3!H$OoyGLIeeP#3|^(ysB zb@j-G&l*3Q{#pHJ(>_aoHsQ1QXMsFEj>`!Oz@7jOH{$=}@esbRallCv#-@HG$ z-?rcU;rCrPxUMo>bCebUf&c&eClTXX<66T>`W^aT>o;M6rq$m7TeL&JPd8b25_I=p z1QC0a#bsi%FbhkyMy=7$(UR}09crCgN3%4p-yvM6zoJOVcgQ=!rMhY&gGZmlD)Z%M z&?-Za-hRm0cIco1NdG#r4SHz<wST1TI{X4w`Yyc7kO zJ%<+_cErCYvuNRh`Nz*|Yi*f3C(}H8R@2Pm8XIOzub);oH9e(v@}!BWn(7Hvl@;a5 zvP8VJBo>W?L&1RG=k>T(Gg#=L8iVeB|4X92JX-vh<9m8w#56i zC5gKh#`}`picVjdti*e69UWcTo=C_ff-C0r&FktrzVG<1ZaKc3Q;2pY+g7zsn%I_{ zm&DU5yJcNva{hdNq6^1eg=5rgL0s-#(ysJo`tW9FUcD-qY}*(~v~}Qlt;s|y-oLH2 ztrcG@2XVD*WX)qtCA(Lxz!eP*8hse?+mvc=DmgbXX<{~Rr*o67FT=T!;MnL|Ym~A? zUuzOIT69Ws(UMMlnYS(3n)Om|TNln!mpk4VuO-RmNfWn0(0kfWX-~>sr?$u2rR+>Hcvq1|AdNfQ|pB@z}?XF<<$Ttl=EcGTC0gTCb(aaOVED(0@2 zgDvfH90alTBzrb&&5$#bZE?A4aeE>Xmp9=PC1<+3Iyxe49ZIaWuQkH9AZ4o1 zd=WX8VilIyw!gV`rLuyxgI$?WrL{-(B5fuj7XahLJycEFkLfje9cC%1>B+Ae8Z72GhlCE0f7+^pGDy%lWKhu3OJcB65j zG1@_|ws^a2YFRFuTe>=0+xpu2+RuatG0yMM*q8A$+vDt(o}5bMR@7Ff=hnz+>`i5R zlP1n-CMyUtS!Y8jur1S+XA|afolR7}9pT$Ue0z{@5Af|j`1bF7yN_>w&bM##?OS}i zmv4W@w{P+-3vtfo>iKN)Z-mVG1CCzj=YGn!EQmWB=8tV(7xL{I28DuH^V-LBDZ?QSz8=l~29_QQd@$DwQeT;7(<=aR2_Hxwl@TXtQ&#&X#wS0RK z-y$T(mFM&AYJS&we19e1XSDBO-=y(tyF4u-*L85~SK!HHmVi53;(ZX1w$=!YRZZJ+ z=$=){zU54LGok6)N=m!>`ntE0cz0jCt#3o$M(l1#g8Nr>_sG?4iv zoHQ}5wsrK&Xh%l^I#6jeRU}jG1PZQCF|&Otm$6+~##BTxmeh2}{+4mq@j~r7c8lff zuH%Jc-}klGvG22teczGK1z~&7gkF22H3=V~6Lu^v-`bk&h|6E{{S*2A9em%;_v3Ij zKw<2&_v3`-YE=gv*aYpjYu7F`5iHMkqzbCYNo_q#=59yMpq?z&c0CCGPiDI>!`RNp zvJl$78^^ht#_Gnlxr{lw=Vt0W4c-~T{F&Z`NCV6h=g)UFyE4LpR`+6ZqU$8j$-4QA z+|!XW(DDWKhWU;9HYhu$Gvn=Xxw_>{xu#`#mt4XWy1wF)_{z8(M|;J)`ZjI|c67-4 z%0zq>8WK)GD|;q$&$h7?Mr0d_lhO{}$`g}wyW~m9x$->r-EDHJrK>OA(%G)SH{D96Kuc~KYx=;yU7b~`{!@+YBvSIfrsCuz03G_ z8mnGFyD5mY*Vs+0-t4BrCn&=A*(W<6#7!NAzezYLuo7ha4j{{BBs!U-%8+&8f9yOV zBR^p(mBPH>8gkHp&-)I-ujYhcQB0DY!0zW z_zDXRv9CY{m$Yvsz2S~*hWLlW=_wr^(N$OH5|e3ToiF*j+T}g9^($U~ot_~)M1QKz z7+G=qn|RaP#Q|YH;&eu(HSA3lN((P&D!w4)FKCb{e?fEc1r6*4x9FD|8|hN9v3a<8 zn7)i8q(O)og?qMe4_QpAOj0Dsyxr}1&1I=u_%=&_4or}cQ`B=VFH?t-il~-D%N@G#5!Y=uSri*#5$vn zuO5^MrL)!9*U$ci`xn(%XI(!_#rlE!gKDg^(-YKc{SE3OI-oAPPW|LMbt!$Gt=G}A z>*&D9>*^=845@xHcU00<8zv&pd>m;Lx-<##79=E9Xsmg~#!B(Va?2^+SjQI|%ZNpkhDuF#fGWoX9$&s zbCr!D1dNT1$`pz+12zM}0GTo+ppkWpKNO<1K@_5y&`7zqinNV2^gv~6Wuv_jr|?g= z!GJ#6AFHOJl(c?m*CDEu#-t&+RFX^gDe*YnNp>pnQiFrpu1CMb<;~>oe!WCPZhpC9 z(Bo=@BpdcAMkBx4Xkb?x$=&Sg45E)!r$SD<6nFCiBmf#K-6>YVfyPRSy0m;;$$01K^19Y^IrZH^sfQ*K*|=15?Q#>JAqt<`R$ zBvet^n8xc*VL=xEle7=8j3OPE+Kz6644ItkZEg#vQnIh5J?HkfHE$)8Gq&b7?75rq z;ciHE6tApL7p_dzrtA5Y^=a&-X5k|>cI9BN(T4v>{%fhhXyX?fZ4~U^gq7lA{>V^i zyn>g-D?%0V(%D!;DC?j;f50C|C)1Txl~pe6B6(yq7=6iloT#r)*QHsx3x$>S(`HPc zQSUMs4L&bB=b6qjQ7EZ$2`gV(-D`Tmy11!p?(WmSdHrQS>3-+JYZd(|bFT{T{r0!7 zzG&O(*~ibCby@e7Z6_SpxURx5ah<*MoNbq6(pFV+)pd0}`qN8Sn|J@zxnkX?*6%Nz zX%m9R`fr_a|Bee*ST9olW}4A_#gfK`68iqZC5GeXT-DV)yTm-AqK9Yk=qH9Z^qat{ zA@twWkQww@O{<9oL1`!oipT|}N*1}GG!_M=ifOfN=o0#&LhM_~5BnogLt_d%N_iUF zTr6C&sCj7Uis9fstYdq0FcOVKtHL69eeaM-iA0rHcq_Rq8wp2KB2~w|_#{~5lfhoQg#SXD8qEF>Z#RF)5b~Rwuo?<& zYPNa&TA2rX=4QXgW-gQwQS$II$wNGnI2$V|l!*qrq?OsRC(e@W2C-09UNvn7FPk=_ ze8#k@CamR!vZ|W&G_5R+y{hJE>6)rSSxI6thy|9OoGwXEPRzzyQYcH5rzUGBhL%_es=rqd-vXc`_F_)SMn>DUU}eS{VNA%M~^OG+P8CbzrI?( zjg+I0Zw`%Uyc8?&(%d31#W*kdv;rG#DKe9fF_RKr+^qN)H7hXO8FK{*S70GfCK>Z= zBF@Ib4oEH`R)?9Q%sHK67`m#K_E-B7Bqr^`$&9&{#A>lAI*L>W zurM8^RNHXEMyhRC_z5q&!b<{PEc`?wg%c@~NMYe>O)>bST^aLCVwj1Ae{Et0u9-m+ zGqCWREbJx=Nm;OHH%UHepX}VjjA6dzpe#HAD26)d1tU#X84O9*a5z&{SxKi(pE09K z%&QEK*B=OQrBRtIgKjds_Pu+yd~5z{)u*3q-DE$`?W6RUH=X{$b+7#Aw-$cSGWQhf zKWTH5<(%L3%xYh>$bVdI_1yG=(-$q8JXv?necN8VZN-{pbJg8p%SCJNx?^&c`jbuH zKJ(odpIG;Q%{_C!WlI+a6!rC;sAAU z@9cQ3l}VwMb;VXDyp_=z$u)+)*vJBFS$R;ziaRq6Oz z6*c8ptu#$Z@|tq2R)oK|wNd1?vRa6!tH^F((%=b98l{H zB3gL!&wrtBQeG`e7K;}OKh?hmCA>(Zm9x+q-Xg7>yfsAG$y>u$q?O1;j#z7JsqB<; zM$zhF9tY!5V|qrwm<|}L(#EP8>8crlbQS$K<0n;L+_~bW8&}-&Rezdduw$^Xu1w0Lwxce>6D)WCYJHbuYPR;-QYw;S_*yS-Q&Bd3wkAPQT_x}3>O zXf6EKLs)6fqBodvcF?L53HmqSd3=;F8#EeCUN4~wTE7|^g4?h2iWaMvNM8-%pS?+< zJ^}Vh_!6$9U{%}b%lFZj)wcWSR_#-5WA)CeZ`1YU7l=OdR4j6wt5|Q1rxCSrD!PmH z)}h}0!cwDi5oz8xoYS3wnoGMFU(cwotAmC%rt8xX-Wkbc_S|#MT-zM<=)`kZuO58p zq3E}*Gbf#n=XcUy2#bZgp-1yFRphNPnbLTR=6P#Orl{762>RA$MWMABBxC%IH}0f= zRV~8Z$Zp~dvALOifG3=IgiMV+;lGmFZRnZ^DI(r+cpI9)FEa<1y(8kd?7Q!N@ByLd z*?wR6rv3{&BQLi?ffw-%qZXD5oEM2K6f}cVJI^I#fQXF(h0oADL=}-B7!u)SgC8!( zo53|;?TKD~;hU=M7W$R`i+_A&upu@(Ve|uCmFCN!Vq9Ys=@%}xpGH4(o_>*H`j+>I?cgAMM6VmrB6F*Gy}c%lY-#KydiSzFAK=$HHk8}p58ch&9+(wr!o&CH2m zx8H}`zo&UC{ZiEIkKiVHm?t}!v3oL-f6PNEdKw0gWNyM*^iV`Y(L=$WILq%D^E8N9 z^lV7TFV4nV^ia&UqKAS#bF*aUo=u0ojJPCHH4ed*pazBTFYZ+D669b!xCZ3nu* z^a!4a&WRc($O~Ext%Vwv6?wzTYiN4jI77DWfEJW`A zlClITvi{O=#%v;16Bg!DFiwZ&U`ra3oSx>cn9d!%GMqrnfu;=jbLZNC^`;R z{*unPf6bGBco{@7l8YKdAH4OBhw= zL1rd_D)r<8jVh%Ds!T6ZrIb@8s1?`%04n5=f?Va;&}fm9o0-qIO^zVk^?D*)IFwYX zv346A!54;esi~={*@(@CXifwe$tE3AN+Sq{O*T;G*fHz_U-AOeSh#A^!c|a92?nLT z+mKv-U|5-4D(%Y#z077Imvc(HJnxbY*|`tBsD#|lbQ6U`tkG-CVEk*VWPA{bBQ+-@WXbg&C8mdsJ_*PHuvR zg8Qkf)jx)mXn>xs(YvS{gGG94y3t;scc@5jJEylE^oFp&ft4gXvTtrZ@&-EQ{SGN+ za0QN@}#4Y5Vw*GvpO-F!WE{hTZ|!g zquDOEXFPz<3;zxU*Cira>SjCNv(bxbP>_kvSE37ga;yS4MZ5joR=yv+F9 z!ii(vmI&jv#JJm5;Jz&J8F95@^hr(GjN8Y)-Eg34bWKFRpL%bzDa)`HN zs@Req&7Xp>`A=>pcrlh&+cKP!BtK!owhidk=ZCig9gA*e1j$r%nT9OE zE}90r9boS9xsFV{vNu^-@FXO}Fg#C?tdx)gXu>KLatI!OJgJq%vFB+{#{C|iLzt>% zuXsdhOoQrft``ND1RYpRC?MPvx_5kL%36BQeY*1`(@lH#{6qcY$HSu=`dXgo?R^64 zl2i1X-snGf-KJZHpHu(w-h1i+U}R6B7r*}cA^!$h-mpRGXoqy2z;)ma@D$G0+Qd+3 zld<^8z}v(m8}gn)!Qhy9n>b*HO^`4nQjb}KPOm9Dnsr!kVwa+GD1<4jti_@EiV3&J z4YsOJ_^ojWS@Ev!|1}1bG@S?Vt7B5zRufD0ip`r&~Z8pO!FA@d5>W%C#L5$4S;)0nrVDq38c4?EyTo}$StXq24J zqG2x%Gmb|J z&sW6N&(>_*xMt19dGprX`1I2^-nbbN{(*4S>fGyp{p;(wHI*T6@LLbQ_|7{oKKQLr z^VZC zgg8(5!4rgO6atL`!C(gK&4=x zs}!cy4`+c)WxO{TFSOhddSg~zZ}6P>5&C9!Uf&?fD0)@!q|ye281>WOssdAKG+cTbPa( zQ{#K@QG>cgeM?>S(MRI%iz)@}!lPPU5^`%Kmul@&jNHe13J;HK$9f8RHVXY9G?dl1 zB2&TQTh?C)(?=J?`xdvakTU=w8j}sOp8GB6Eiq6(oTaTS0G4Y9Fg(07hC389V(LY5@RatP&Vx{}NpqTx%2V*y z$>%CW;oNuMMYiJMhoaxI&YXM(e?N+G4w7!6_*WSD%eA*FW_rxUx6?8`rhKNyQhYnE z%?Ctc5{Rj`_M-(eGiGDPWW>TkpNv_T5p`I2YE@^ft9zaQ4` ze@-t0p~#`+=_O<^T0@{3z4RG_9t)?JUI4v7J7b<+qDc>Wfnv<_qV=E_Xr^0uC@2O& zvft9JYWouyD6#knwS5tz86aTdAN21*)5S@@Ml*du(6tLK`|ChNJTD=W6Fjqz>P z>n>T?nlm@X0WHXwkuQU~GxOTTNiCFL3E%;W+j~gSn^_R3mlRv+HtHv_eZx}59Lm^2 zSlIP)s8;bHsh=^sGce_D4?pRy#VxXapQ6)o7fPpRE|iY+e2H`m`bKz!JRJ@V>mD&0 z41r`N!aCmLCPGeymYZ*uWb2=PKJ?{Tmz^|Qly#lwFK(Ta`C|8OedB$bzxexy%F2P& z^X^%7&)((pEvHF33!nl1R378(|}0+*vv!ke-pQ6AgY5 z4anWk8PhBEb63BjX8)ufQ17GXQ_Focwq*So>LWi^2hIC{QjeYM!TC3io{t?S9*PZj^Zn_f#r{>X} z>P_l@eyBctd?4aH=j8`Kqw^ki-!}5VlosKvzdzM+#@%c0ewj%{3}Z%SqR&G@{BGcF zPo#oW1W8#w&m}Rb0Imm249HE07S3~Rt_Y3Ig8{!&FhHsAV$wMvVsHTus1cb1cSBYT zcsznLU}xT8?J&uCtR8FDDcDj2NNM|x9)$Y{5ENwiSa!@=nHrP;z>ugvHE6fm1JM2{ z#i-+j1{5+K3f-R?G+K;)FLtOxC222`15?5uh5L10FO?RyUh!2>2}tZq$*L@s5YSnr zSSstmMpXPd_Jw3+R@9-uqCT&VbndzLj`Tlx z*4e_{!}MpL>vtTOOP^Iw)BSp6ov@Ml*>JY?I#4t~%E%;|)#NEqpy_0i&}(_&0M|4w zIlyUJOG|9pc^CJoe2bdMfTfmw(uleA`R)XWKsq5lPZ|6oh4;qc2EY!&Q(?< z%GlL37YrrgB=_WKfClop$)O>1ftHZFawd}tu>N<^pbG_I()S8w^f%Paht1}-Bw+W+o})0?Qqwww!EdL2z{`h@;_oz3}d!KvksjPare#g#7x11ex zg{xL=f8#fLui9|&HEYfuxqoEz#@nufSwb(#===J$`d^b6ovZPSvA{1Sq(rCjg^}}% zT{d!lDJ}AgmO|4lMs6lVa>Ec)Bq1%+ap(azC=Lrt)EbQ}RpBR(C>GuNVTyd1O!g_U z7&kL9!puyJJc9Hj3LN_sr;`^r9jw5~3P?8YjG>KkBohPYfOpUiV^EgCUXLIDg?2Dn zib;O#8Ik0-7z@FX&R9(H>q34blKe*9_%?ESdYzua7g-!m4YT0%SUgTg6IM&%i-;~1 zOpm{dOy9CHvNNU;zQB* zp7~fk@aNw=_sa?)rv5?wH~Iv62bRt9Kwmxu>0SZpc7WIi zwYDnu3xtcZt@R6-^8Etnk;%)p7VYaBn-weM87+KP+a6*NIlj_L{s*e!NatEpI-_ zGQPC}-rj(>KxDnii)TX+ya9c{Yih#Eq=pAXCacSTJJbu+m*B!)^enB!TR17kvLj|By8i$@ zL*#E7HH%Ts7&Wz6!k6!#9HS-&R^-Sc*q>%OIJ6yI7Z{;{JUq+!ZifSZ77?UFh(U#X zddp5$;LtAFvjnzhi^u1dtag@S(H7`Nq$ z=G;~b7_=uRfH%feW!9{lW@hpLHV@*v`}NZX)gT zVS}B*Cp#hQ>JHr_#t(feUaH^G-mW%d^|Z5$6QkO*pqht#as<_YZOz*kjcQ_^YUmr! z^C0gV&(TH?d{|ioos4Xf0MH3S@6pVNjnTA>(r zn+#ksnb4u%gAT1C2Q-=#BccgK9jZk{ zmV8fXb&)1oZL_a6=q#p@1`x zLl#9Qhi)iX16hyDqg%0?K(E>`z>K?OizHk2DOM|2t5yqBt5&8~k5Hmccdyf35KGbE z1a%>nZYTD{S@i`14-taiVh?OZ=%=J zhCkA8mJCI|7<^CtE2Y2xe52nTOsI9jjaN;v4oslWVMzSxK&Afa=g@;W^-t<&>T6Gh zV^tfQ-J1{nb({SiX1y>BN1p;WM#xT$@{xk|iWa#s!mXE6j&SQ0qY1la)to$fu!)VX zFr3HcT{*vB!#yz+C%DjW^c%Bb=FFIkKIG|xqPyI*hiQ52&2n; zmiLU%U?VQi;Hpb`nLioz73s_%SCDJl^U|(8zYI)CxufO@J zdHcF^&$~vy#){``sHDZ#@j&!U%T~q5A)tx5yZ8l{m)nmZC%T!CQ(8 ztA+VS6p8`w!gTUDrZFKE4py=tyx<8N7G78pd|=SPc#ax$I&`Uz*!>B8$HPak?n zc<PUS7sv)f)0fL4A>KCK!TK$Szyrft^ zO^dtpTD-KVOBlQzIT`_qL@}b(om$HSnI4M@JZ*%~G=lN)AwVqIU(OP-hPY1An|S?= z;0YFl;#HWPCU`g`PrxqJE!*|t1(^hAa*|urFYLjf@ScrJ=4MSXjAH45N2fQ4QcF8V zjCE*AMNhN#0918PzvPw-?!(KnVYdj2yTux^WFn!kDJ<=NKMd*C*d%IVY+^A*Q`)g5 z!j2=+!f_DDM&=DtHy##Au2l6N-*xUuc06!GL89NslX`NM8d`?qDIfYv@7N z_y@c*rsXI?-8B71SNH9=(&TkY=ZQoA6QG2Q0}=ITB=Sgw0NFO^K=2gut; zv{LYES}AmRD{Zj~i=2&qw9-&B293ZM8Sr9;0yhSVus>y7dyH#@v8I7y7xh3{uUGsa z{5h;6QZd0iK;%G`bI>|ixPIMB6?7)y8%2{wMnA~9GHV4{70I1HXN8ZuGVAk3nL~CM zeKaM@2$QC#cXeHQneb^>*Xq?HR?uXb?m5upC3t>|;d}fJz6UVp$HHmlBeZyLA_Yr z@oO4ZfBoxUsrdZWm=Cg4eNz3tx=hB5Kx}rLMM#zXS^Y{K1pS#md;t9DhTaHi90?zQ z;TcVIOmDck-80GVBO+)mDr5FxTz(~r5tI4+iq|W$F-gd;*h%hC&TIGDvo7Y6SnXaf zS07^@hZ2RGp|I0biD?0wo90<@C}(#&7^r6#bVv{(8w=i8%ob#kHDb0Pi`jxP#B9NQ z%r~#7ny`+G*}Skutjufm22IWQU|o~r5*vnOxn?rZ ziJVVqq~+y8{M|qP@m=*dPb}~G3El9}9d|sWUaa5o-j271sW@_<=)Ykj%T_RMzk_qT zmsHUbMr|Z$3Zs#ViHk9(`|!lYnA5E#E|96%PD(&(3vgBudjh>8wY3V;8CfqWXMVU& z8V2r%x^z$t1AbpsK*!K%_@g&g?x z*;rXpqeEYnHWrwdRG zWWz%h)eZE{lsch6IR|^ z&s?psJRsW5(_vI+)8fPJa5A3k!b#)wS`8#7%mWP?Gu!$fwg?bGtf$ ziPLaj@A|vcq<+W0?b^9I;*(AsRKJ>goG<_psnFeC&@Lu>FSCBtpr!7zgMcMn_3*{mp{-(`qgM7{S*(Z%p^?$ za~Kpftb{yln4bwWdgtZP zJ|!IHo_{#RJpV9@aJK=TdRPAx$0U!$Z<3QkN_q-zNK0~hA23w>29jns5OUWMQ;e2K% zPFM~im(HO4h-7B2H&Id;Z6|q(LXSPEIZ?s^NX2n^MqS~`nn`Tr1K7x@!(PoSeAGY% z6m!o+*GqH-o|}{==zYM&1Q(NZ6V^$El$$3M)^t#~#8VOu&Bm(Xn$lpqJUT?LrKJ<` z!ic;g+_QpXO3Le|>H=w9)V}kH%`YBz+)I?aiR95+>WKR3SXxgp!eo{fE$?K4T{M44mO0(daglB#Om1_($z(9iP33G3DsDq2 z%7m;Z9KbP)O0x!LKzAt)h7s_(4GUVcCL}h$U=b)wuAxUrv0C^pVr1sPv7`~pb`-K| zfKWSBr|ucmiIB+Aj=uwc!kat}udNDi5+MJny-Cq{jd`(}@$%=h?qgo8pZC(35n=eS zii=^yoG?~EMk+?s1px$t9R*o%wu?j`;NBRB+0)OuZ6PzYJUUQ3Jbgy|#4qjTXY^U( zv4AhLZjS|HMsw&LK8igDd!07?7g$L&*x+Y?m2ALf+YGa@j!P32Q%IuIO`6?KElotx zaS5xWIky@jj_>QMkQVxb`YZMQ&#&?*`gpEz{h8}GdHhIkK)(=d~q zeiCTsB!A(wBgHsUA|>e1--?0yJdPxVkxVXHk{=PFq2qyh8?XqC2E9Ytv!{7T$D=I} z3(?jfz82Ra$cB@TEIA_Zap8F(Djx*n7)&S}aAICFjQM7f1A4EVb}P$1HY#@Kf*!eY zaeFRfuXIX419vETf`J#z*LM1DGhyKeaZCbN zQmP${%PI}BoGoP})|s&|5{IIkStFo-v;+$)4EeBN2Vhrp_hrmzb~f3QHuA3;6LQH@ z7=5d!5RJYiQFaxk4TxjWt}2Nau8L17;a4F)9A%@&(VL9fF(fS7l@6)UUQ(q)yRuxW zK)b@mn5m)zQ?1Mr${NlWWz|K)l^`4z^YL*7J5gOxh=(fxnOp@3W#QOzM7bAzoGY{O z@#u+b8-G$b*IT@F{!&54_y+g zx7w$Nb$th~iZs}*4Kdw~h&L~cHCXL4q6aZkFml~QUkmPv(R(!RDvnD^Bqig{qgo9WZmy9Js2GKno@UcP` zY_n9DBBHwkBCN-qfTapdatE1^0$Ti%}eI zq}w?d-vF4>%B;Y^_>#kvHE}S$R)l(@foV*OvPVntiJQAA^=Y{1R(XWCt; z2JVu8q(vqWU+CbewQQH!bd>0u%~mG*%t94JA37osJnF(gz{f5O2E$_)f-3+c$mi(_ zpZRH(q+#$cPZU;tF-Odg0}l3!ey|j=Unw9+@=a6HXpsJ`%@i zG@_+pO*LCpjBk&yx{8xI#W{q+g{TvD7wuE9yE&)CN&vU$x9YeBMsTHL@om;oLZvId z2qG=Q+aLM!ae2K#hRVY5xYVHDh=nz%8M$RNs1*%rHeq3fHU}2$P({E~FruiC&C+AA z8>AP5-7sa8DQgr!cD>dj_#U)~(}p6w5iMe4MIf}vcod;ccn%0{H0q0l2Azv!&Qse3 zgFk=dopl#OB|p7(?Z~^ZV<(6gjm!{!dT7H4=0OAM4PHzR^z@JqHF_3NuNYm*Bxm$s zRztl?dDJU<(DIVQoNJPEFESwyinN~FJvWtgqkoH>t+?^h7&C=TIU6sO^$-kZY0SGg zSrdcYorR1Z3t`w*7CQs~Wz80bi=kUACbMKf7bHr7fPxb=&cU%`^eZx`QYAbkv%u$5 z(TFEl7+q~eEr%&|a4f*QfiN>@-V^cxO{lp;^|j)LLqE{XI&`tvckt>LblvUkr+wq# zW7vW!*Qsj+qTf9>%C%_K$40pxo*f<=<;t^I1ESDq!3bsa1|HTY%F~s;#C7yqK%h}aW9Oa! z*nQ`*#$^_*tL|{?O2xc&#pu#}EAoEO0f9CWU}O2XOn7fY@In*;hURk3(4Yo-Oguo} z=HoIa7;OLeig8b)x`u8YPp4v*dW=q5wy!W^dW=rmcu=zi<3aOu(q#l47L87TcNnp7 zIvI^R9jDWPL2nif`dZLrJwwLnF|I<-E|E#CY~ct<^zHH9p!!4_(8udO{+M2?uDR3D za3|_J`h^e>Ki1!fF7ag=w--k!jA2@}5ehUvLSYQk%F&`ZQ=`7Mxj9EMZ!B{=fL9ew z)Bv_+`4m2OH4W2NCsfickEoBSk3T{O=jm@ebhq%z$ZT;nqZt_$bZbY?0Yd8kaCAK% zFWUprIwe06*oW~Cy0z*`HgR7}w*z>~Pw?&eQNZIlA&+9yP`l$eA%9-$5f_#bV3J|n zVYOro0hYzx{LSh-v|9aYe?J}&S3hO>uZ5$Z>t^dtf)1HWztHGeZ2y*G45YPxEZ0&ykA*=8PeOvj}NBI&_McfgB|>ZX58@`iz;!zO9PE zysTX@-~ix8FTjmc*{&{|@R=E!9%H0{O<*^%-8@b!n;CZoL~0Aur0KeXoy2`(CmMLxaj=(jKJfQPD)Y zfjzzfLqxK@4I+D|22>0$7)!GQuZG4%g|q1?4J0s^k8FM6slSv&l9ku5JO66?Q}$1O_UfKQ)KuyYALl)5 za*5p=UZ0&h>-*PVc4p^oXSVotf*9L;`yJ2vN=2i2dbRnq?z7L5m1iCw>5n9Iy6Y{r zhKklu;BxxobwP8`y=*~e_leV6?a>(c^HT69GEKx!D(T4@e-`n)Riq4~_*rzO;d!lj zJnsZr;?mCNqciZPDFYB$j6AED0^2a%w}XwXQN;RY)?I`C6zTnJCKh095(}^7-CmL9rCA%2EN^JBVM|9XaWK9BK^dq~y~$?;v- z5pzE9Nh`QG%)l1eUTc_raX5~|PB_!Rcmn(p_p_6}F|=+R;{la8d-~yM!Av058;%ut z!(?S1BqHZ<%oGb-o3Iv#@K_xIOx(lD0@$-Q(;wA-gWOS3l{=3@T(UDb^l-0->I)|@ZWgt zJ9~b*^B2RZaV zD8NVmD-<9m+B!B05CzDi9gPAUFMI!+P=HJzG3F2Om*RN9{Cq+)(&AguU;pSGl^Q?U z`D;v4)wlNMhwiv-)vW<~-Rt}4RQy_mNb7#SZS6H54()k!{Z){u`DhyzG8M!a+HW7B z^D%}tFH@S%m-0HlvWS!B9wXQH2G%4Y@|p|OeQ92>(Ah~LSO8UVr@aQ*Bcz%zc>^1|$z&`_HenbilQ9V+1GbX$*b$R4 z?-2Y3xok-GjgzsmD3`G)*S%3MWGIRU<%P^qc7%|XMR5T`4j&<7rJ>$vs32pafyL>N zqbP9yQE?VN#$~6XrCJ$q{!wu@*1WnET+@Ko1aMxvH4V?e%{TuJ!c*CaqWB7dx*B|MGr@cbzz}&kMw~0U9`Sxq?{`m0^zeFo8fo4oV z!a``pA06F_CQZh)RxHe?fsxR88RH&qp%s-HU}XB01d(FHDfB`kXsyB9JfkEG>GYjlvv+B`oCX!?oiC|KFC4%3krE#&{ zMO=1qHr70ska|AxhYu4;Fqk%n_tvEi1`iuo$)=OS==jFZ)kDYG%{Yv(|OW+k~6$J@xc+PF>HuO1OJL{m)^l zhBakytS{Nh!a`v6@q-rH+7nDsCAhk*GP38Nc!?4X zO1oGoN4Uo_OzVNaF*}pR>(dlzIeAR+`nabnlJl}&pHI`Ht|7UGyDLC=M#m8KQ6^q7 z@)*Rc=)u&mF%C+34eFRFw=4iahsk_iyLzP~v}+C2f>LvocI{1+#oj^W-hNXR+U?3V`bgd7`+4}C>xzrRI-@T(Ivo_$q#Y$jMuXEhaRWr=P>g9 z^_>^mpP_#`IQXuAt91Ew_iwm=!wsA6RNs2_4sMC4ba-qBtu{yy%f?v$8nlQqB_zT9)@nIYv~`!ALTUh7}PL zW?$TnZky$pe`XjNd}+4=OF|(kC9s4H-v^_}@O`Wo31=A+6mv1c{N?b&Pe_Jy&8X<3 zi1*!q{+yR%puVv#n5Y{qg{~$TxTY}%PK%q6Pjv|iU8t^)R zqt|@K0%niT0O9wse&e)D8#BxW?K_>Fr5DS}P-o{^Yj57v*$FRk|AbqHl#!$$f9K*K zZytG-$<-|&;~Y@Xj8JQfM#Cb;)KSz%MT{w4_3JREiV0-YH8%r0rjtI}!wFWV5sYC? zL9oNHrbr1$=)Ky9bPTYnHp}tJl_fNXnRH`fcsZPg!W~Hyem%~pp z=-l3E-T|}2X$b05Mf744^X2-I8#?hPY~8AxeDH1kjy*&e{Zw5>XX72&*y0zpcPx%Z z^A+EbjYeY*4MJPOG_F3caV5Q^FKS%HbPD?AfTVSM@rn$;idW?LRd_;{T{$9&dN#Hd z7*})}L4PYQ4}V2T?{STLMRxd4ykb5uVDQ$edNyTcn!Vx8ot?dRR40=s&Cp&mbMKA% zN7C~Qi}x^@K#S@q-ZnyY+PjX!O&6jxj+-8jZFOrt%9u*{|M+ImZ8C*5_?T6Go8UVMq#e=8G`Lkj(o zx_0E-LcCAKuT*e)(pg+W@-uc7<-=0soqtL`n5tniGLD?b5{&ee0kg$=1Y@J7ta9l5 z)3lzU3bt?>{Zwn=X$4DIU(|ZjxFrn8)40}~UetO4o+k=vqRO?Nhs6%1WVzx2US;`! z)u^kiL)Q%q6bsBTghXI-EwQ<<%kJu#%IF z`#W#2x~jUWDlm23MreWEt0c5SzVPeVC)7w zR0g-mM#HdTO#_%sC!EpYJSI(20@GpX#!e}?GOpl@;(g27yH0EG=NGS#+2x0oEp8u_ zoK@9H7)=?^h~Rnrh&}J0zA#eOq5U7J$fKpb(b9q<_xOu}iKjH`Z^FtD7<@pK)mcOf zStnLIffjP|#DJ#ced+u(KMdi;#%Uj=><`!c%TBAfeDfX4Dt)na-`l*Uv$Ln`;=az# zZ+_#l?{;?TCOF71sn`~;H+{YmEcb1(tL9?l=uekmJM$AX+HIusO)~mTmFkPp3_D>nDAj9FQO!LR8b49L zq@GVcrEc{j)ZGt#@`E4j-~WRjd;;&~Qnf>u1g(R> zjCN|eu-LaWp{NVBz9n0JQQKTmW7|TW3lq1+- z32UC7)Ny~mY?tgAmpX}-F@4u-i?U}#%XsRf;M?nX&SqSCGM_m)Iz1U0uZ7IEq82ij zMs3hSV{>`ioW-;X_H513kkeL7Px`p-Dds1+3SwS=-sMDs(tqi))6DCBs3j;3u&wzF zIXy*d!(zY3kvTBuSPtwkFP`;z95V;z zK^xl8hCBym{TFgzthXAimrs02z`Ya_Uy_rxo>P*Ui?85_CBCf4dhPn>vtG9U`&ln! zE7>Ibn&mh$>lJ+G;GcKBdXe4oOlPNg&C~bZa?dT-J$i2p*m-2Vs-JfIUObQzl2)X> zUU~b+TG|Vi0weT<&AjC{Y3-Ju`qD&0McE5-Kg1yixgQcP%ANy$2vr=2kO>PK6dTl= z&12F97Bx$|ce4p9kyA*-I+Pj!50($7vQEi??1kdPGvIDOs>THvbin4&8@(711CKID zF@Yct#dw#Z>|`C8MLUX46Qlrn;(^oR(PgIv!e`wZ3Rm3q7}MulQpsE3;Jx%Xw}o?& zpB=;-)*gkLoltPOrjU|+5`6-HRY{(}Uo~A+>3P)bg!Gg#)U0-(p=Ku(P_tm-R0bhv z6bNgEnhoSpv(JzeP_yM$5C547r8bpE%|6=iuS|I+9EO@@vkuAuNe=A8)So;l7Ywkh zK~Vdti&Fg5#bigSfSOGVDYcVvL#-s&j-h62*$w2VU%QZ4@;GNLmnN_d+5u}hTgFhc zveBwV48HcFaoUdP}1PEUe_otFF>0_M~Qf zuxa~_ftsC=DxzkwHvy>G)P%7s2LYMIf59(H)fQ2+DIKW=YL@@>1^3H5fuNRX4&sr) zz-+8Xp=RN`@*fr`WIFgy3!u*q|M_{?0%_s*rt4N;aB+3@726+fnft3(fBA{+1^fC9 zmtS37ebx5+Gnx0czWlLK_#bOgHTjpN+*!JAeMQ5Gf-oQ?j&&jO2Um4uQ z{QubKp9P1$6K#5vCU?dBVW`+o%=3rbxMUq(J z)Qv7908ZxuDZ7`?8?YSa3^Q}@qWyqDdNBB87ohSc_VXyaFz6VWME0{dZ1`XW;?v4{ zs1^nmV4}vb73jdM6C9`4T+)5dJ^jx-(>NtV9V@RDcHT`Vs{8I9X)fop<8Oe3{a!a2 zdA+Z+cL7{b%jzYHIwYFcAyF<^WyM&A=@6pCq28H_4?%q3b!J08VD{1Ht%KT7%m`B9 zVX~pTG30Wv8^^8#;eRqq_)uPSE+b*HUWudok{RNmoY!r}V6O9k9ku}_nAao-OX95a zI_EZ-(M{OYl(9IFHg{+6!v#Evb7Y4_ua4rsz+<5(#uu}6Gu4H3G&&or_NypbKQEhh zwJ>6?KFvCNHG%^1*Szv#=4mcF&Fx?N!r&m&Q6ndISc1KPosERU>~5A9C2LrMvw)p7 zV!+s%Y#3ulHB?y;KT5DBYc>{+k1V%g!jRQjf1pqvK;Po(tQ8PG?f$WHnl&2oIN59< z2%reZ&e}1JFvrtECu!JOPde?Xt7F?dVpdG2#UwpZ-Mf7G8*laWy!Syzr}|b`_s4H_ zfkmDdmmYdb>^%6k*m~#(Or`>msbTQSF_Ex&vwsv4HlIr?AYogNiG)S7emxQvg%}e2 zHAq+#emxQvh5rXg*wtT;gkAIXNZ8e1kAz+Ge-{Z`Y+sIm#fE(>4C@8@5j zK0w!9aH0Aa^#JDDnjjc3i_+C2e;fIbKA^7T?W~?8{toRNCVRLQBE???i4=7L|5XqY z&i^V%w5Sup+zJ^^X&!XgU51bgzbL`{Bi>7Z<~U{wRAE^&6e4Rj$l%Ta267EkwlLH( zb%dS#JG@SXjoDwFbzscCsMx~nMu-r_4?ksn;TV9^UQ^bN9Z|11(7Q?x!;e!6w1T0k z-XCB;j{~Cc(f#~W{GGb$TX)|*rK)=NtR>S#b?sSa(-K;C=5nOXU;5hv)b_NK_T1vq zWe#l+FNP#ELlSm^LdQh5GD#RcbTqOxF9}6tYwHon)^q;}Wb3*ABC@qo8@j`gt=K&_ zvUT+l$X4+G*CSh-h3iLN6kA5F5$-&6&8?z!(V}_BANmWnpvr0LIl^f`Y$nKcn)iYM z|C;wQ_FD-i?!6EhjsnI~j{Q~wM{MStjRe1OgIOGdCj2ss$;oH9*~1#2$|h-0w28J* z5M8hM0T*C2y_ii23&&`BYx@BcaK`vgyD&_l0Y9ULV4USs(7PJ&RibYd^T_ha##{i6 z%@9s|^104$e53QZC(k+l_{#b7D~~_^9Q7P~@A~0aU%%}2SBKa4+BcY{E`3w|<;sRo zZ2C&|mv1heYGU%SQzi5?16Y8Zq`hY`%K}7f9`C4SS-$2sjK(8kS!ecIq(9hC4zQmx z+Ig>vzU#lU=M?B!R-k9`ImsiQ6V7LI%8TPlxg6#^d3u^HVa&-8#;mPKH|OaYmIh1x zrGB#|QlO_J<9A@;^mNqrQ^2J41$uhw{U9VJ_nyHz{#_ugv3|PW$Iy2^)@_Y>+)f*# zqej`&mZ|4(64H{D(_^9kr@bqIkE*)*@4h!{GLxCiWG2fblgwlpNFX63A%rbM2y2iX zgvd_9zOUknD+mZy5fl;EDsJseCIL~g*rH;KwJNpRy0tE?O114*>qcI_|GDqY9;&vs z-`C%-3@`7^dvET$_rANFd(QuyfB(hIO;xxfrjGr7l=D)yMpGf~2V!JwLVe~Wl+&!1 zGs?fhFZJDBO@poL6ADk0}3#OZKp&4wvetELTInAv+y z*+~C}(bcP_mM`ty_ku$9-s}mUp6;TtBiGNKP*RcY9y^lkf`5@$u_rWr@M4nLQzOfb zD0`|B;1mWg-pHfusS3>3sj{b<>Hvxcd#V=V`A{MCG^kGj?~kyjLfYW$soGS|o(k$z zirCHBQ?-neO|0M_le~(Fd+Igk>RVQ8<#;Bir0F=0s3mjAd2O>m`j-IWRZH*`#p-BV{ty zm0)$akW3|j3qaE5>9{NPalbLwPYazqF4hHdD?;?G%?soZW}93P!67UDG*_Bg%w^2o zqbD*bs5F!6P6BKP;omrHCqr4cCs?h^%D0q70-1Y#5h^|$c|pbJ^Tw{g4P{#lW#QG4 z!<7bQ8dgUc$U=sr9y5}5tWGB4hLH;^K_oN9Gv^N+ zGjLPpBN?MJ^IrFruDRLyNbbka4(>PP{NXc(UB#^LRg}qRf1TsH;oh;iKCR&;SM{>T zH~1ZCXFoPxx#{T_nE8#rm5jd?P(2N+)}OsVGHYIc>iI<_*)%>i!i%~YoWhO5=Sn+K z$wv%qc%+?})$!iU+sTMXJK^Jf&;-|LH<9)E1h)?#8hV6ap${fw2^25j5kXv8K^lq= zT_WB*<3N1qOR#Y$0Nee|0D2t{m6w~y5gyTLEB)n_`0zojvLZysZ6-IsKh8{2?%-ZW zMxRPLVC@Lc=P+AOoIZialnSm=e3ZwO3z_BkC|@dtj7og?GyB8;FI4EQ02GZ3*;wnx z3@bh*Ot25#iz_?eAPiqM@a^#TK`eX-?yNF>W|HMf9h)@Gy$=k_pEPmgy!zT5y%(># z&Th1w-d$>QGVRN6eD$b)&lOv4_f$C96!~ptZz-!~S1&FZHF(5`TwimJztU=2TQ=doG_)(=hI__rH97?>*aARWG`r>~d|xxJx%xm9jEGC#)qd>f?37i<$u% z@02b7x3$8%-6bDo%l$)dy0;Q&y@Eg)~PA zJjEzNFZka|YA|UK30%LbelXdA8P4f&D)ewV>EH`{_x}9nd-uK|-xj=f$=qcN*u154 zmt7rfyK4E|B ziQo5n4u0h~774y0eE2V1xy401EqK3^FCgh+pr|8vFCKumR8y)1;3a1jNWyW>eJQCa zigO=L*?a-4L5_oIZb-d|^_P*In`3i}y?jN*f(Jno@oe?(WsC06|<9rj3QpEJ2ZR*&_eu0>{X>Z;^aXG z@2i~2YUB-z7qb@DvS_iqk@b_W)UYe%%Vy4!FJo6~#J%gq%`nN%=-j$q_wai82DWEzFB@ye5iA(MxejQ({%X?O}6%Zcy5dl-dFNe9&Uq8 zkv3S)`;{$F&imEaNWbE1e>K6E-zZVeP5 z0(hNZtHX!iIkFCSW;^N59zu2xeE6L<;-R4)ACLUTkj7*BcB2F)=}ToOr5%O}=!Bu% z!)Sq-3Z@8g@Iy~aKhQ5af5yri_Pq4SH6`V?*}r@^_&m<7oIQ8r&A0w_$2Go+l-Uax zvR_?&=aR7nc~e_{x@b&Z&Q$ID&-`HV+WF)AU*5E=V|8Ffex`lVg8ffE8(6w-{`jix z%{P2+)%}a|GfWGX?VmH}tFt37n^e;;&Ea1(a?Ykfb=C~9I093t`K35rd;d3TPdZf& zjkYJ1H=bqar3@yGX{3n%)Ov{UL7?#T;EaF$4A|y%4vpXt9q@2!q%+i>pdc>BHtDg) z*x1J&)87BmOD_rH**|r?Em^d0k>yQWHnkA#1j^c$5&;!DZ9hM^i z>V>t4K)qJMYL*6JBTz5b%QX3xq4F+h zOg<(*DGy`E5S|KuI|VEb*UGyPdg`z8ZnlP*AaBSMfwtSNISiepif@d>l7J_+I=(0Q zZm8$^Iw7Ee7#a!S97y3{;cp(lIgs~!H*OmN%k%$wpACQ_Nzwoz3YQyVgbI$xX1l$w z9XR^&$IspVi=1s4TNht(%Wa!mw`YsvZ+(Ore*S9L1AC^=d-S$FPqxky;PCx!*V{Tt z`xf&FOGR?E)Ch;v*TXFC6`pFvP9>8x=Rc3W7EO#++fGWD)i` z<}l*M7r6+b{U}Hh?{CYpkP{zZ=_X-Qi1^D@7d}|Z%TEV-9}n~d##wRU%QzhbcHvZL zXznmh45B#h3FS5Pz#2qb0&@a)9;rcA;Fy(LPKAnb+$-;01Cvas+~1oDMMJdeT31W) zX_Gzcu%_2j^HCnkzT-do^s&AP-ZDmSIisa1`OZ(LJTfV$?uKY-L7L%lA zjn3=nl{1EUdRmPs#@;q>2;4qQXenx7=JQn`BV(A41r-Tt}{>EgNCqIx=8e%Y})S7O+7pT$Z`= z9IFQ?&x2{{d?5ifFVez7AXp3&62e14ldi6FLXI3K^nDyMQ4opyX6A9pcuHT0zet~$O1!cG<=(#K=+awC>JIs1rAkMDIXcZ z@@}|i(%`vW6DGd5XT*q|$L3#f!Te*aNZNGynu|wNo8^na7A#-EJ{{52FeGM%DiqeJ zGt^q~1I$oss5fDT${QW5>VAf*qM4^Ey(TD%Q>F)`8iJx?s`8FrSn#ND9Qm*Ae#WY% z-&M!|4qT&{y_k@pmBhW;&6vmjS+9D8VpaIH;Z>g@Ua6HfZp1jZN*X1e(B6*KcxKqM zK7y)_^1BkM8to)tb$ zz3h=kF1z%RN7$H6)240QIBnV{?d|-;rI$S-|8m+UewN1I30-Hw$!g5J`n#;`M~Iaj!!Dy1-7PPB(2QVSiRA2vR7PxC=nW|jQ;N!_ zMdLMvUsoC1wV1bJS=;5|M5-m0?`5OqBYWi|lfazJz#}#oZ}*>Kcpe2UR&64~b3kEu z-iV-^|1iVzeud$AKLsxM`wY)mKE*OTLrTnaihGybcza!XD*I#S+M9klyRmt~%<0nA z;v=_RGj)pQmRT2#nb2U*ylndq7xkIwZx}Os;Z66C8ACOaBK8#bqed#i?9T4+h!)ii zxp`~MR4$kh947qA6-a%~7{E|!0i`b#o(SXsuVuI8@ToRfT1ul@nGiS=DlIE5Ys*pD zodcZR*`0$CO<{L-TLbRX!Tfv<+sbzn3M*f@4H{ltNM?6NO$EXK3mtKZaNC(+sT=Ih znRc6^iHT=--cO4zr8f{8c9`8+rJ;c!r21%u$1k{=V72P;OrLZ9oLMu@t2?i}T=RoL^^+%U+jGg1`9mG%i~(ydxpC(WGsd1ba?IGJ z9uMmEOU;ww^*nyYaHS_j>^a}iZbZ{W1#1&$fq(;Lu-mKHmxtewKUc$MFky#0h2w{^ zg@^eQga`&PYDwW3L_~~JFo;o0ih_E{29m>wEgkqF@Tat;TM*bej308QW8w&9XJr$n zFi2n-1tXMZQZPcKl#0a&2^=G2GJrUcV}w%uDL@HDF+xEn5~7z8!TnUF7ZT#fM?U`` zU`}WCYu0Q!*#6M^SC-Uev;*w3a`)~Nd&KEqy|n0Bu`W~gh<_CSNNu)QzDN2Jektj~ zUzEB9fq^m$N5w=9Z?ndLhL508F;Ve3Mx5+`;W#Knj)KN+#72sCZJk&T4{o?v>8;_( z_{P<&%}De=R)VEFfGc-|5Z?uSy(x&qu5p6`pVkyT<5Fh zCO>th+6KPyYLKG z-TF8h1wkr-Wl_N)Ciki#jXnXXWarSmv zPgh|*{kZn`TsM@*prJ3D&H=zmKBL-@crSZIvQYCGiH6>WS9DDEtI&ZR@b5cSkh|@L z!5kA7))WB2NYJNWP(L_;WbRM&uL!w}c`CJK9Qm}_4n0*^Nu@KYiYTjHXSW&$VT(~V zMHsGhcBce}tJt^(5r!)v+bMzJDpt9rD!!~{=)R2g*pYHeT?dm<1}=ngjus>-uuZo; z@W5@f&6q!b25s`oVsqzxQho65d)nIWxjVSzqSb3IzIe^*i%u^<+wOjK8X3}~!;YeY zwx}w@=&-}x3qw*Pyx{6XrbZs970XXap?9X$sfj|y0sG)j!Fl>_lmuFrDl1f)0bp3E z(K;O3;_$(59Gd!*~M_<;5Hq(|ON4rMu(%U;Bn$W&Rm(!8hNAMgQbVfAlL#Bbor_eBhz!6-uE3a$ zGXmxlSwc_?>TwJVeq*N9YStoTofal<4NWTuya`B4o5Uc|ZUo#I6pIB2qzZRnU?p`r ztwsZkG5upfo!NRCgU9fojyRdy{2ICu?7ZOO!egu0UF`aP{Vuy~>mN;$Zb-c-bpCnebI+}0L*<`ay1InDU8i*> zwCF zR`t#PcfA5FOho1Z`rtcRul#o${LdQ)XQH=UtoD}AcyHMzOya%eI_VSMTSVb8d4Z-@ zGXPRsfpBkvbROmCj^{6na&&Y4GN9Utqr0Ha2+_s=^&H(Mu$<+Q5j6kwSp=j`;OMsH z1L^a3IJvb%DM~Si6g?Ic9lk0j?8Y!0XFBa@?&6}|JI#mmfB981CwEu3l-!ynzrc(j zKZlx`$qFF9c zPDr%mWO;)*N<1N~T|gq^pApOgh#70YHIdgvWUr7zFR zccU99gl=FS=?2c3<6Ut%&USf&*$FvzCFd9ss?{9JuOP>z$gx2vRa%j*qC#jhXq)O^ zkA^TstXc+!b?fWL@t*`%J5eJdb8~)n&(^K?{A>J4#uEXeZ)3lj2} zmz+m;wmY212O+`DXIj{NJ(Nc%f(2yKPe@TfMx9nfb^u%y_%CFwI2JBi6^rYVAIzNF z>~1N}ZOSRENLi3QcY0<^FHe)Bu#YkG!i70Dv#p|6cpojtl7^%h#!oQ94i?2#A_iajGptCxB$aVFnJO-R{tt9f+?1 z)k=nBeYf4}FgodbJi1|v)r|lD7Yj!{SD<&da7-P)V#RpcdJh@Wo3?4n*YPVRD&Kn# ziMEK1j{1M zP*;{KtF71!TxFh%w0MM-^Yu1kElW!bC_P#`diFFI`*aE&eW<6Vr-t9(QQA}L4F#IQ zMjz$fJghic!Sh3>#fL-MQlVf7`izLBOTo)ijd1hO7{Man6Mw)Ykz*-@8VHyghuh)I zbGr+Td^v?+xilw%G!q{JH3=1My~{)_px~#81Z}XAfzpJN1ZO|WK$uGbd%%0vL@lgM>l`a00v&?`PL=Qw*OCq7eZy1{(4l*fL*2JfebxRQ)mQ#E*L-0F zXqKxuLptDz0{znqw~GHm2e=jhw(?^>KsJD9+d95h@XkiO^UUgmc;TK5Fj@UXx|vnq+yAnv_6I;SHJ+a+!kFdQ^7c<%XB3 z&ZyMnV=&Z>k4te^T#Bh?Z!k3>MRRhBDW()P#ja10;%GHRk-zO5Sbj_%Z5A&`s>8Zj zMZU2lr)W*JhVw|UV~?I{vJ7i+l9=`|H&Rq;QZjsP9>X=6VpVd9euICUCmzsjHki+f zETVpc?LCg8JFI?#?OkPrRHA#C!1`hXPdo@gOyG&PflUD8o&;(PK=u5R4j&prLUaTI z0uZlhD1v1!eM_FTfSGhmX9M#y*J-xAD5 zYnXu^eVrJF%k(4>^-He0$9QBsXNtSxQtY;`C#Q(<=-esZ5SL=N<56;o7?0vpq2y%_GkFi?DGGUPur^pMJvi~x5XE{^ zyFSMacBhuw@ls?#-pOGFO4qWa?U=SR+>h&78iyKOqv#JvG zvDH3SfWW4;B@$tIGvT@c3$Wlx2d69;Hd%?3C*Qu~RWQgdk{8Lp{?Q@-!w)jfG27!9 zjvpqPSt6KiWw){g+r>NNOKu`w+28N~YIFOe1CG7G8j1HlbY#F2zmOjz+F9*v;+uVM zKReE@xsjv}&K}2md2P6ti|rcDrg#P7$lt_!Oy6hD*JNr>OKvqx7|T49}HViG|Z7jYS==$$@H3?h+r0)T9Om zcrR?v@iV#;K{^s@((bTZZKz4iz5z&tWO2`c9;r>~QyJkt<%!I`8N5%W1~Pb`f*wg- ze{f4_NU8(P0m2XGfL8lZhe4;;OLj$uCp$vFOY#Tt0G42JM5YN!$(axs^DTnUfw5N-@0o-M|ba15kk#T;0SX$}lxz(^?*UTwd? zf)@6Z=RW)FIeGAc7U|LhyX6gR$L?|Cv`_w(o%zv|A2E5GRB-XNvM{UxW8QUL?`Xb< zDAxUj=assM;1B8|7Hz_h2-Wdf*r8Va=ne`Wzb%3x6APpQF& zz65Iuz}xG}w4OL(E%%lCd?}e!TjbEoYXza8DEexMB4(fhQ00CN9QQQ*w*(~(nh~SM zZ!Gj@6yl?#-`8*Mix0o2Zz0kz?F)TehBe^g?j4G^4xtPBYT(&HE{GJTjzBUsVWl(0 z_R+y|4ZU-%f(U?b41N3%>Aj=M9 zq(l7f^mfx@^mfTP?ug5A;|OoCF(Jnzl5;$4_%Joc{{p>TavqPw<*{j$H`tVr$5F|7 z9NjoNoX2~?@jer4gnv?RcQ5pIc`YTbCYP$WbGE8_yX>&OF1)ANMk&YBF|FfhTscNe z_6A2Jlw)#oIfhM8>-yiLw^MR`e@w0)=ebs9i)`U*TS!=f8#X$e@2=ThuL!R&9<@{W z57ygdo>Om^lc={V?N)Ec>sDuqsoP_5bvqj4#pr|<5jS3h>vqK05lY*ZsFmrSMHIb3 z!g!%s+^EbJ2qvsZqZcT%Mb~?6K0b4%1blqvoT&uR>w3P8OUS_@;PV7TX?J0-$G#NN zhT7Xau2IBzQUr077&9_m7|8{o_o|8Use z8|zF{-3LDdHCBVa568GQ21|Q`8mkmvRBEhxI@Gjz>UtAJMjiRo;1JKkVZ^5<=1iM^ zYBSwAwHb6B6a39Q**2lsjNbqzXXf9D)x%6N=7TmfZllx0>Y?03KcYNBA{Z{**(Ox` zAY7kmuk6z&#mD83z>w2{!N>i^G=HBoeE8%zBdy|is6fl_s0;Y4fx6Sd{{1;TwSOJK zQ^D5-6zYVDj|w-nrKY5S6@((Fu>fcb5P;C>hYz889qyyL z9F7vevnOo`eTnhx88Ub{dG-wP52oCMRAk}|LIzIsPoVFi<{1h>FJcpzoKzQ1Ke0nm zTFQwaF^nj{eZYe=YcZ$Wl9)Xr3|p$Xjpp=tzaf@Py;U4T2U78)7GIWl?K?m31RaQe z%!K_5Z%uFWeRTA#%L_7#YH#?({*DLk{$sC_e9u)1N!PF059>x8xNO6M3$C9x+@TRg z@iRli=$-q=)mk*;rh0lwcMx&NUCncdIOK++(waV-@85mn!x?UUilMSNRUz%_oN|>r z)oGj71dgu$qs-Yk?}A`t)p^@6J^#PcvksQ4I0uyvjgS86h2x@?4loCC_8cyuAn*v+ zlZC|dA`*&D@D+Q5zJw8{IC+I%(xW7-Ge}OcBQC`fSfff3QtXkOVzICI+$kQ3OR+b! z5WN#p>_!VwR#Fz9BF$mpQa{d9RJ9Po*XA)i&K8#`xr9Ux)DPQ)7){17jx%B%`IeFD zRVD`xuvFu7f8$8?KcPtV#Jzxb%Qe57`MZ54{B9XI6;n5g;H(z|^|)%-rr+(=P*x0>!Xzp*KX zSFN1hbWV$4x7T=& zBG@c^E$T4|)LGATIINZ|KBi)NP#B;{pQA(*&y}Fh3FW&49pcf1oR(bfRT#7Z@`|8a zxz?>AgRwaGEP_EGIA*5BnP$m!TAbHf2U!PgO*?t=NLHr9X|+)5%IApHoaM-sa7!9J zopXL_y#fyno=L;2PYX>9z|C9_lS!HwrqIfhV~<`Z4tNW}!lf4u0(pR>xF&TE`C?cxa=`qzxBu3vfHirFjA z$REis$cM!qhkx~}!-rp8zMMuR;H|(aLsIbswbdRK>mn^8)m_-f8!Sxd?R@}oj;@)w zy`rFZfhAwj!O+_)<85qWxF5u%_>bsrlT+-7OYv*9mvMt7T>;*q!%zrF5ONs;O? zT@!vw2LEiM1)+2}A#BhH zerP8*`_E9dGK%9GfwRNlekdZuX#*xs9Dq%(=Kfqd_3Ixe4y2=j6WRQI;^y!dtjz1W zUeYvaW}aLBtFjOz>3`9~C{C=Z|AnNT;ph5ag^l1F>wlH;;y+3M3oTD2d2d2n&h@{( zY5}o(NdHT^pLAj;$Yix8V#BW|ZQFIILjL09)tA*@yIcfU!E@JKQh)6#qAR$+oauiv ziT&pD7WU>7*Uvrs=4HF9Zn@2_ucS$eWg{j5Nt9c8IYl;?^XKa8YuKo)*@Z>dlfm)Ltq7bRcbaSpd z*;?<0?lhq$VI9k{DB51mEdgzBCire=E2Bch)*9uzRa_lXRab{|^4+?L?=~yKcN>yw zoCy0E)EdAwXGOmj`&qzC_o5! z2AV&ofH@VGR2TRqoMCN~?xFmiFj|2GOqrqbVX zC2S(SM04|oCz9DEKy3E=2fAgKKo3*e1+}$syLgYcsz+>zs*+hQ2%H?6z$nSj2nkpv6*fokz{iivZ__(TF7pR*WiG%>n}`N3NiK7K zQ9ds-a`=1XkZd(!#PYYx;kcw6CfDIdadr4DbBHp-cS+mI-*bujRz47SM&7eFHP@Rw zZ+j6LCOU5`qhf)}kjCfjvvSuvLOWXae>t@qAu$9T!>3bprT;@tZTaPI&8f}0Fvkkl zvsZ!nZAy^6(j(dqMfTFDvX?EQ?cni*LgExlT|IbQvD#um_~R28U#h99f-9^ZhFs`) zYby&YX3uWAXn-?ax`1t1@cp|sU?Bvg1!v&Og8X&5^^~MOaEo<&eT+!i(67)0XdS9X3k7a1Nd4A@~mkSY-Fnm*apo2 zTn$2h3g;q=C79O55=`?@Z7RWZ&;sNZgnk<^+gS*;R0#pYJlvNMz+qrM&Rd?>o3o}S(u)$ZX zSaHQyeJ(SD!|bV-o>4f=(9a3e^D&poed_%4n>Yk@PHwVaQ*JVXI?t6CMo=r(2a>PD zn*BH8Yz4(WZ?HH)a`7cwA9|GZP|<~5_h1b=OZWrU8wI=#izE|9Vp71O$AePv04YAy z)2zoH`@@SQPw=wo`ynUcvV9s?wr+KV$z==U%^?~7qd8IPj)$C`yUzJgGan$QO0#fGvDiznB;@6tE?g0HtKqw-94IUZr z@u0CL?Bnt2Kj)Dl9Kag}^bj@Ko1Ven@CJIr=;7CO-oiP~eZqO+YwTI|8j$yzv3x~Q zPS;S&yM(StEo%fzx;JP^sO9wJI!Uvp8CAcX>rl(NJjHRyg_7cj(GJP_RwGsMm-yf?HLj>2y_^&SdU1#TJlGQcy@}GQe*xkbx*q>EttqcwC77glb#X zZkC{wfaGI6ei4O`B1x*H&(1dR|zT0%OsBo&>?+DwR@a^i$PB{j{W70nXbn02cn zBppfBYSM5@vg$R!c@G+dIIysfz(oxlN`zmnOxM@cDnxm;4n6s+!8z@zr=D8S@<0D0 zejq-%Y~wOgm(*qPl2>{t3#RF0?BnK?DPDi*UD-4Fq#&8W{hy>DqK9 zqkyxgrKg3X;|oCpWIg!K22;@;m$jQj(LnLB)DmkEl^OCGO3dR?0+Wh(lz{Q4QEFgM zNKF$J%zH|TIZeY%qBcD}Qs@+oWX35mQfTnMr`zmlS=zE0RS5+yEZh+YG&j>Gb}ZPi zK-|;$3+&GRp5DLWaY8d&A9~-^?&Ca8y2Vg;<8hJ`@i@7L+d;|JK0!?}*2b9X_7r-9 zo`jxM*o__38?l2@pDyEVl;@GR`zzG8eNE>r(#L$(=1-$*>{rot4{w^-vV9O&ww!`w zDXk#6Y}xMYus2O|**=P~cBZ<$m}3(?=XxZU%~RqD+jzNu4j!o@@Bly^scM}2oJ_n)*>BYu9@>-tBv??K`j zD9Fm&D+#Y=2MKP3PA1!JiR#l+!cmliBmM{6rW?v{(vbgie!eRoUVl!Qq!tPJ7|YxB z8jS{L%&Fx6tRZJUyIqq{3tA#Jg8#GKYPYuKYXFdR*wd^uumua@AOt!FtVK$NLqVa> zQOpyCv^HjsVbZ=`C?N-Tn;w`<@>w=J&1nv&(^bM($=qkT)^D!Ghl35PYk}6!*TA#@O&`KtTb-H5F)|D_iX$wB$&@}J~acjwhqSBO3PEX}(aZ4U2TUyBi}Oz4l8 z?>m(@6d{U6%y-r5t`J3=RHA6ad{?dR3Lky3igV4w9=!C_$rS6mP>fC%EQMI!-tNik z>nc_;ui1HsAX+*E_||rhu~#lBCjf<|knqbw7E7^;Z?z1w3~MVUd~4mXG@>&HzIDhj z0+kLK2DE6Me;5Z&wh6;1a$_jXZ{gnwRB9pGr(lK!x6x?=l`1#Uk2t6_*rSIM7Edw! zTYFGWD_mznH8mz;iv~-P)n8-9hrj9uD{!S=t+fW1>W+e&=cYEZ$)uo18JtQ3*-`fD z>m(0511^_pTim~Xots-1O1uC8KtxQ**w!gfASMS#cWFLh0vd*^Q-DB zoCE) zuFT9R-1g=h@+a4Adt>K)_wC$y-{i^CQGhzH-!*kunr7UA+xn1ceDjGXRYx=0A*=6^BJ#NoqKnQKZ?kllDALalFL@rzv|p&`-sJst#5yCux~<& z{gca9*{_n94U&6D*IMBsHs(9l|N9d43k{KF{O_a7c&fj=&i$Hac-!trwT5+kFY$c2 z2aaHyK;EwZ*XJ#2n@G+Z=YRJJ)6~3`vtg7s$~RVe&+vB3^Nm%&2jZH@xM4_lAnK7L z;Z3+ecOKRB@OGP#onaxbN2I6&+x+jf$I+6eb$viJA$%e>OVDND8uu;bGH)&A1W)@- zoF{0vn1*CeP*=7q?6s-jmq_LfV}ss^LmPFw=14{x6?u!oXd|9SJw>7k=W%bGZ)vxY zXkzmig8+%kCHImE=kiz_g2{*RCK0n9huaFLSyY;kPm+h#e6pOle0IBj>n@)$KCq}w zjhl}-YCa)_5*qBEL0a7nVfjUW&0?W6F-?ppbK0`f0H&eX`VX9;wav1iJ=Q zwZMTOK5knvD?EPuL@*y=9s)Thf_e%r&y=6dbp;A$1ak^0xtu~ImlJbd;WVn0L=mn^ zwV5QF+Ce>npxJO#0`Gwys~TeYgcm;)i+;zx%qi-);R1iI zs4tp4ZlWjcFHG`wu9DxE-z&INoBcFP7su_~`->L~?nMNOt&dOM3G`7J;`U#Txc!DO zn@@y-2oWVz>!?CO1o~ON&P<6|-#O*)*G0u+;i!15T?LtF-&Dimb@TKHmr(TS3jc;q z;=q;`1}fpcN& zKzkK~tz%2$&~=)p!tgrE!+V-vvOBc*p_Kn@O#C?eBQg8E2S&g9UPRy9uh}EobS54P z?+?mqI8xFmJA+bEqgv7^JA=Ye@E;dBPoW;`E^;25mgs`3*^}&pi+8hFt`^`u{L>TLOY}`Dc{(R! zd3PZwrE`X$)SuInUGuB33wwBvwg#z28pN4K{`HzaVj&^T=75OePoX2>noYd7)m^{W z+$%jNc!f)qnJS`VO<=JXR60i!D7C^bR?k5v`W%F%uzy*P6kR3qJu96G*s`=*)p9_Z-6qxR$D0X zNT0QKnlV;iaM#UjQu9xHyXBV)bUmEzdjGKU+CA%S{RTETd@s*#S~XQ{zjGPWYVLhV z{%L5z$c<~}u#9&**Do2}e4)6!9>N{0OYD!Dwc-`u!HC(YVyDwFVrtfYwM)EWkCMJY z_|DTe2%jTSO8*+^Y1XX$IYa?9s^qt|7XxbsbMcvg=Z;Ce?S5>j* zWlQI;tIx7a;xbL&6^9SpmODFjVEHWMVujSs8nnlvxrnrysMY%KBi{5=#Jc*kJQo@e z=0tLlU{xD^gRbmXTy(gdO_z<<&p zCca8-;t5t5tyVTEdK~+nQP{{UM`>%Mc5xmO%3F7+V|}!=4jL+sWKFTx z@i%{_`_ekNZANfuVoU4D^{2;|){&m3Ayu(b+N35^ocIQ7q6LgCY;1dq6c$YjWtCqP zXY!Wi5w<1FYu~V}{uI30f+pU?(8BOP=B@riFkQ9*j4hSPsx{S zEbNE!#Qk42X*y!^?NXVv82pQr2pv%$NC&qqnF9_k^C$Q~BDPgJ<=bG)ftOE^5kFOn5zGx}Iw2m@|%c_=_U4b%t*zNLyJ3BADO`gvV-X?BC$&1*J4a#isE-zKssI zJ&vjo&On`(A-skoiXC`iLa&HET^RvV)Da;1bRHSys9kV(uhnyDKb7QGW}3hrk@-cpNGde$kn%)7YX%6tv`6a7Yu~*}th?z0M93ukg?ugeQgV$Pjep z2J>==^ymlpY|Yx%Y6+M?QO-ym#7;cLFq%4+3WC60Xc)s>~b@HMxm9Q6q& zyz)wSWw~bvHh1`%r>Lx4xmJcF&*1X1B2W0*kfGxc%LTt1H*v_saYF}T8xp=Yw6STN za;*tRL+i&iH4Y75>pyT5-VJ^?s;Pg|sDVSUfpbhL!@wcKM=95a<7nXEQNxD}#I<;C zD(+yb*xX7AC050XV*?q*U5#5{#a|hB)v;q&UG?OXSB+n{Zv2FG>n3cIidUZfjd&pO zy!b}q8Oe6+N;(J2aQY?Fre1Q%)M=M|`MCDSUk=GlzR>we@;Qtn?ZO-EZ=AiknR;?O zcXL#Ap(swIB*h6IN>uLV7}93eF2H|?Sl<-pz%VN_`&$AV^Eq-55h$WLP==E*z#~C< zqVjW54pd?;#*YQE8S@c0%)Hf}z_^7`@c)o$>%Z(R|6%McXs<%oYnmp^E0yeaW$ug2 zynQ2ci8AwIR^>8BRb(zvXje7k5znNz5IPeCXyrsOqd&gp35U)EeGnyDLp={d&ao)FW$+KeFe5BCt^;>zwEP^+vmkhn>QV+k+p9&67iYrB* z88Z;6%4()dbfJ*{iY`Qc*WE?E-t({fL&qoX zlL3$?V2>})=HZ(`{FO=d+ z&8Bf1kp?&_moQZjHwd6hY!CvTi_(fXv_ag*VFn6*6nI^K$ZC_cV_LRvAJejPQbU^| zkJcjXS_$5))?w2JxlYVb)*D*ZX3libHLK04v&CO)%dt_6pd+c-DSlUWjv>c-^3@z< zN8wy23(}Id6hm&A@;wF1h9zyT-0=4-bX|~On8V*w;f21WEjuf>3`UbIaOyd-M+4R| zdS0MlK|wIvwY`6#YAFFLJI4vzD-qKj?n6pQ*Gh(Xx0UjbM~_*%?&A;Ft{Xi@{-U;4 z{z6M3j)G~ z>8$}LXw;cAy#}o7@QV=OXQFvjyA8eY0{&(p=Blfx=!j;fRg?f|GcB)75O%`4KReg` zS>Z+;%GT$DAN zWXUYAd}RCfhiiLe&1Cyc+Z&|8C#NiZx#`8PG_w5c_|waWoSe+g-)utQ@k!-l_AoZ% z?iV$hpWi)Be*3EPmB7T%cdMOt_!bHs`Ru(D0;&A71 zLHCJZ6Q|lNMhD89gr=gvy|o1AqUtF0iW6C8Tc3IT9ZgenhuZUc_83%?SMa?t04U0DytC)nlhggn zZy7gk+0E>V57~+W?~`xWmDc;_*A1B_{}i8{a><7mtv$$EIxoCX4pNDWVbw0xuELR4 zXi-WWiTw^#tg4mgvEN~$RNuHhQexF$R9D}w)i4Sj@%nMlih@9)og73&^qs*73Lnxk zjaYH2;uHRQqh$%@kWjPNbdSaAXcI(1s9 zd*-q0(d7#19blS6S7Udwk7T!IPWg!Re!;ut6>P0MSTt?FyU{sfgZ#8KLTZ<9O!Mv9 z(3#t5T6jfIhg^gAw3a=p86b|sx_^Z7o+9J_x3Mv<4@x45GE~XX@2r(hp6xG=+l%!7 z9gNrg$DKCas%fA9UaDCFEedl9H|u^a{SID| zg+PR?NXSi}gxsja+$L5T$&CWMl^hs%dt0D%!rKn{j2?*}l)5DcO|2|cWXi(mV?7lr z1)~a`qV7mx!ZGmBUm{tC1y&h;l&Bme9qF_*@aXhB=m~Ru@W4gE^T*sj`_`ZQ_{JrN zs+Qh$*$@7-YJ1Cy2b$_{Ub^m}=C_;XPq}1J&+BK*UoX~${m~bK@OrT5nx3Z&T~WmgQ?{jQEq>N6T~`p_g!NLZ63+vpW7LeI9BbKH-!{3hv|+ zj=dB#*x4Stlv2uPm{aUXkuTS2w(3w^-uREV6_|BgaP^V297JSU2(tybgnnq-qMRdN zVI|TkuM&q|=fq?LxV29N{93IX!q?I$Y`57kTcNhZKh>5L_BFCJQ*~i>QG>~|dT+zb zii*-wt7tKwr)%t&>&aX+VdQ-IsnWq!MT3lH7P$6D?DWm-A+~?YplZx;DMJh<`73#! z{BGygA6|Ij6pYI3pzyx#25BBd-YQ{MLakIM)JoW1Sre%h9?{KjD#Qf5dJkaJ< zaMC=`=7Bnog?#25ZX=-g&)A7W9jrS!z4pn?jkot6bVIM= zK7ob#d&b^;@}HP_Lr=ZGqHdD1D2iu=0m(e2b6!l?L& z(i0-{QFK&P<|EY^%EY3>A~&D}Q$P_UOsMz6T}i7mr5g0c6e8hPCIP?2j>ZpSWs642 z&}HZ(igOp4AXtZ8&?DdpO&Py9*(U-#p%yC9QYQWb#XwYGsB`wvCn8ExT zYEsMK>}|&efe2k+qVCW3vmx@2SjU;pITC!h@0Fis{pG1=#C>N!m47^ot>4Nrwz5_7 z_qNJsw#qx{&EF*mx|=jFLxN2a)+%ovB09Bu1B;GJDO`_j45aY>5FM5j%z-|o;#4~m zBuxs;s}(2PMXin-g6itp3UpdRpzGi$h9%n!)*0-rku^!pr&dI!X0NfNo$Zk-ntmGOgOw zOe{6hnSlBNm5fjayMUyDfO87VDva_NV~k+u)@uYUtO2KJL#72}`Uv!3R%R8fs9>sN zs?{onnSlO@wv}v;ynLm+oc&C`hs|QMw7Wa|ch-r|imjcub>1p2!tD2+u9e-P?}ynn z5vvfLU6m10R7b=pf`@0fd*6B$+oJFHBA!vMn<(C=U;J;z@aQJSVtBxzCSI@m8Or&c zBb9VN15yd73f)9$kbVuG@PC1-&`k_GRAHX`|A>kaX3ABf4fW&-OQ5Q7VPH|4nZhfq z3z+y=6SbLfq}sQoJ}mw_)Q9ZaZ_a&)Jnz?jF52{~|BKF2++)ElT<&%?A8g-wiJhq1?VCgq!*Z30fUN+6>0090VNd zA^eY0qKL-6XG9iIH1-}=GJswC0 zDmz$Oe3WGgE82WMTx1nUi3E|bCJ`L$mBbL0>F{Cra8y<~>`Gt)P-;nNg{-x?dC#7m zJI9Z#?jL%HLgI^$J}TXMc9C@J&4)K#DBUd$8*=5%XBSbReC?B`Xs)H&-K5s;PZH|3 z_#Aaxa_+jN+T-;bO0~Lc>f@@oLqJ7RC6m+|%+06p1rjoQze%U~1iT}30|F*xR?)z_Re>BDl@NCl5 zn_j4zV;S_RkZQmQK?oLJJ~zBS`=`!Z3<4x(^e}!uUBK`3%jKbh!H_RMB0qv(QA#oY z*cIaK%2%-tLqVPJsQ4>kw`QGChpj=MCrr>z6PnppVYB$4umjr&3FkBq3lnf}8@@O5 zeO=d^xPL6R7qHb}8-c9~TLZRn*ytMVrz!UfZAfE2rNj5@gms2|)*H{t!*+}IS)m!* zeK-zi-Vp-2I$=J(AH;7T(+EN#&L2Qp2Xqe#dvX2%u2Vkgz6to=j^h^mE)U0dNUCit-B;d}+sxk5yqv9FV!>UtMx2`Eb*_G7SZ!ZQYAKMLC<+(YLlVY`MsD_qB( z?Rp#6u@|nx^RJ=v*y!F(v`0DzalJwP-3}aEk&is=Gu5pghMs9+hVT^aQT|JDFFpS+ z`2AvRjo7IEb#xE*({XK*Ae{Y^AjlK7?{&Q^{!OUF`DdyAa1Bsse(eaq_MngEapwj-F-HJb+7JCwcrin@PL-FK?j>u8TQXT@(mMIAKbdmwhhJ9r=Yc?|ncv3KDf0ULjx z^e$sJDv$bp@5VjRO=&aKP7p0geh%dvgS;J5_VO$0{$6$8sa~H=d$e!dD}RdjLhqa2 zab!b&exl}&_B?;MPVYJVzUer+QCoh8zqcJYAKmD^D{U$h&pN5pjeHX4=-DUK?| zJ?e43df!<(#(lTbca-xF*j`839oSN_J%-;Lz%#GI^#j!AWA|tm&*Qs*K0*1TK6y>z zp57VGk3!x*q+{%VN8iyd#-Lx0!u{gw)e2@$F^8}usC`Q4kMA!1XX2l*KZtD~w!3K4+$3CvbSa;dU&{9YY{)O~-;{Th ziRXQia?J1f5@o0HD6%0lu>Rw76L4fdkcMkkXysRR?GAuDX zW*lxjoAN?x$aII;|6}Z>GO)yT*P;#(GDtOLQ^e88~*#|Bmn8a23h@C`$@4tcTuhxMQNw+t;BI;-KrhF=b|4jVe`r;QsLpJ+TY ze8BLBMpTb@dt~~^U84#|O&t{)tsVXBn4&TB$Fw(H-*iXQpT}M_cISEh$EA-e9Cv8k z)8jrK?;byL{Oa+yjz2xVb3(<0857=^sGs*edS|j}a?Rw~lebNo zHRakVKb-RFR5rDA>XfMmrfr^f_q3l)`|J7c^GBZl^XXSlziawWr++!4aK_jf8)gJ% zyw;rBT+@8C`JI^uX1+V~FSA^;8fUGTwQttjvyHR+&c0ywHFE~eX_<5VoWpb8nroQb zckY3CtLNQ1@5l2#n(vrDWPxWv(}INyb}x8v!OIIeTS{6cv}|s9q~-CJPg*)#OIpXb zZfd==^yVShYwY2BbNlPzYdhgQbmzkHj zmd#kUXxREB3DV(TYE;_}fa&%HAud zt-O5Yp_L!6%33vR)z;PLueo&1eQRD=^TwKw)@H07ymsN*-D_`O`_S6cYdhBsUe~m4 z$GThB{dnCk)_t+wyMFBYjqC4R|I_uau77`nbwk~T`5Sg@Xy5S0M%~6IHh#5fz@}xJ z9@+HPg%@A=^k&EA3pT%ek#tejMH??VdeIjbufF*GOPVh^d}-CCt1mrq`T3XMa`|gp zGPnHcO5K&quNr*SjH_O~dg9f$Tw}Rr`85Zw`T4c>Yj50|yLJDz>}@Ny{rEc9b?05T z`?}-Xb=xOw|G|ziJFeUD+D^~TxjXmneD?Z{yY#yb?;c5#5xW9h4SGbD(qL6YqfQgX zRYBZ{6#&Iv?kDbw$WP=+uEhNr6u(#Q(cm8PZl!-(j9oXY$2y#E zL+VUJPcR9OtH&C_COpfJwK#9(IBllUA~QDO1Abn|@Bd0Y#(8+PQ#yLOpJl7ZbiOA) zhV?_($jG;pY2X}#^MK&+V^K(BKT(e*;Y#+RdW_$SlhtD_#+6p}Scmg>@M9pl1(T?x z16u^nALHj!aXw9aK|QAPZ}4LizyA~Un9eKtPowjanV*N_i(r$K{97p?#~D1GRq8RFznC8*KCWPuu2+xg{O$bM$?yN6dQ9gZ=f~MNpC&!8 z9@F{X@Z&sw{~y(3I{yVf&c}I!CRIJgd5xVP_on+b#p*GgucTuGf39YjdQ9iX)3J%y zr)HjdOy^hdV_t8X%hh8#zhl^{)tlEYS+sb4L5~GJ3o7gC`u47?Eg03hX?g4V^}Q#| zTd-{2+LrQyq05&S@Y~iEtZiM_x^`o0OZlkQRzkUt@legb!@|& zwz|f;;e}<5NBT}0II6KuXD9_KX^ubrmX^J(hg$AyeYE9p>&cd9S`F*`+EFc^wTSEe zdS~k`E$yw+hQQF-ev7_UzfiwOe@GwF->AP`zb;^$J?AKN%q_zk9>e&wL|8d|J3fyJ zi-$i3jqGyf;@=;~B;O+R!flNW!9F!*)5~0*83YB#37%>Fp{0*5J-Jj`=Pw@FI&$I2 zMI#T542@i{uyx_Wg^Ly*S{PdRz{2)L3${M6wf+7F?r*mtU;CEtJGk#AX~P^Sf`>0D zoL{hg!S?M7pa*8I?SYXSmoyeGX+*@t#exmt^_K9zEBN1)_#1?0;^c Date: Fri, 29 Aug 2025 02:53:54 +0200 Subject: [PATCH 100/208] Update mcp4461.cpp (#10479) --- esphome/components/mcp4461/mcp4461.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/mcp4461/mcp4461.cpp b/esphome/components/mcp4461/mcp4461.cpp index 55ce9b7899..191fbae366 100644 --- a/esphome/components/mcp4461/mcp4461.cpp +++ b/esphome/components/mcp4461/mcp4461.cpp @@ -198,7 +198,7 @@ uint16_t Mcp4461Component::get_wiper_level_(Mcp4461WiperIdx wiper) { uint16_t Mcp4461Component::read_wiper_level_(uint8_t wiper_idx) { uint8_t addr = this->get_wiper_address_(wiper_idx); - uint8_t reg = addr | static_cast(Mcp4461Commands::INCREMENT); + uint8_t reg = addr | static_cast(Mcp4461Commands::READ); if (wiper_idx > 3) { if (!this->is_eeprom_ready_for_writing_(true)) { return 0; From ef98f67b41dab7bd50e71217b3afa607928f955c Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Fri, 29 Aug 2025 11:58:58 +1000 Subject: [PATCH 101/208] [lvgl] Replace spinbox step with selected_digit (#10349) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/lvgl/defines.py | 1 + esphome/components/lvgl/widgets/__init__.py | 1 - esphome/components/lvgl/widgets/spinbox.py | 31 +++++++++++++-------- tests/components/lvgl/lvgl-package.yaml | 2 +- 4 files changed, 22 insertions(+), 13 deletions(-) diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py index 8f09a3a6d0..baee403b57 100644 --- a/esphome/components/lvgl/defines.py +++ b/esphome/components/lvgl/defines.py @@ -451,6 +451,7 @@ CONF_GRID_ROWS = "grid_rows" CONF_HEADER_MODE = "header_mode" CONF_HOME = "home" CONF_INITIAL_FOCUS = "initial_focus" +CONF_SELECTED_DIGIT = "selected_digit" CONF_KEY_CODE = "key_code" CONF_KEYPADS = "keypads" CONF_LAYOUT = "layout" diff --git a/esphome/components/lvgl/widgets/__init__.py b/esphome/components/lvgl/widgets/__init__.py index bb6155234c..1f9cdde0a0 100644 --- a/esphome/components/lvgl/widgets/__init__.py +++ b/esphome/components/lvgl/widgets/__init__.py @@ -67,7 +67,6 @@ class Widget: self.type = wtype self.config = config self.scale = 1.0 - self.step = 1.0 self.range_from = -sys.maxsize self.range_to = sys.maxsize if wtype.is_compound(): diff --git a/esphome/components/lvgl/widgets/spinbox.py b/esphome/components/lvgl/widgets/spinbox.py index b84dc7cd23..26ad149c6f 100644 --- a/esphome/components/lvgl/widgets/spinbox.py +++ b/esphome/components/lvgl/widgets/spinbox.py @@ -11,6 +11,7 @@ from ..defines import ( CONF_ROLLOVER, CONF_SCROLLBAR, CONF_SELECTED, + CONF_SELECTED_DIGIT, CONF_TEXTAREA_PLACEHOLDER, ) from ..lv_validation import lv_bool, lv_float @@ -38,18 +39,24 @@ def validate_spinbox(config): min_val = -1 - max_val range_from = int(config[CONF_RANGE_FROM]) range_to = int(config[CONF_RANGE_TO]) - step = int(config[CONF_STEP]) + step = config[CONF_SELECTED_DIGIT] + digits = config[CONF_DIGITS] if ( range_from > max_val or range_from < min_val or range_to > max_val or range_to < min_val ): - raise cv.Invalid("Range outside allowed limits") - if step <= 0 or step >= (range_to - range_from) / 2: - raise cv.Invalid("Invalid step value") - if config[CONF_DIGITS] <= config[CONF_DECIMAL_PLACES]: - raise cv.Invalid("Number of digits must exceed number of decimal places") + raise cv.Invalid("Range outside allowed limits", path=[CONF_RANGE_FROM]) + if digits <= config[CONF_DECIMAL_PLACES]: + raise cv.Invalid( + "Number of digits must exceed number of decimal places", path=[CONF_DIGITS] + ) + if step >= digits: + raise cv.Invalid( + "Initial selected digit must be less than number of digits", + path=[CONF_SELECTED_DIGIT], + ) return config @@ -59,7 +66,10 @@ SPINBOX_SCHEMA = cv.Schema( cv.Optional(CONF_RANGE_FROM, default=0): cv.float_, cv.Optional(CONF_RANGE_TO, default=100): cv.float_, cv.Optional(CONF_DIGITS, default=4): cv.int_range(1, 10), - cv.Optional(CONF_STEP, default=1.0): cv.positive_float, + cv.Optional(CONF_STEP): cv.invalid( + f"{CONF_STEP} has been replaced by {CONF_SELECTED_DIGIT}" + ), + cv.Optional(CONF_SELECTED_DIGIT, default=0): cv.positive_int, cv.Optional(CONF_DECIMAL_PLACES, default=0): cv.int_range(0, 6), cv.Optional(CONF_ROLLOVER, default=False): lv_bool, } @@ -93,13 +103,12 @@ class SpinboxType(WidgetType): scale = 10 ** config[CONF_DECIMAL_PLACES] range_from = int(config[CONF_RANGE_FROM]) * scale range_to = int(config[CONF_RANGE_TO]) * scale - step = int(config[CONF_STEP]) * scale + step = config[CONF_SELECTED_DIGIT] w.scale = scale - w.step = step w.range_to = range_to w.range_from = range_from lv.spinbox_set_range(w.obj, range_from, range_to) - await w.set_property(CONF_STEP, step) + await w.set_property("step", 10**step) await w.set_property(CONF_ROLLOVER, config) lv.spinbox_set_digit_format( w.obj, digits, digits - config[CONF_DECIMAL_PLACES] @@ -120,7 +129,7 @@ class SpinboxType(WidgetType): return config[CONF_RANGE_FROM] def get_step(self, config: dict): - return config[CONF_STEP] + return 10 ** config[CONF_SELECTED_DIGIT] spinbox_spec = SpinboxType() diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index feee96672c..582531e943 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -684,7 +684,7 @@ lvgl: width: 120 range_from: -10 range_to: 1000 - step: 5.0 + selected_digit: 2 rollover: false digits: 6 decimal_places: 2 From dea68bebd8f41c9ab4d0ee3d3fa38fc7b679685e Mon Sep 17 00:00:00 2001 From: Ben Curtis Date: Thu, 28 Aug 2025 22:00:54 -0400 Subject: [PATCH 102/208] Adjust sen5x to match VOC/NOX datasheet (#9894) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/sen5x/sensor.py | 111 +++++++++++++++++------------ 1 file changed, 67 insertions(+), 44 deletions(-) diff --git a/esphome/components/sen5x/sensor.py b/esphome/components/sen5x/sensor.py index f52de5fe85..9668a253c0 100644 --- a/esphome/components/sen5x/sensor.py +++ b/esphome/components/sen5x/sensor.py @@ -65,26 +65,47 @@ ACCELERATION_MODES = { "high": RhtAccelerationMode.HIGH_ACCELERATION, } -GAS_SENSOR = cv.Schema( - { - cv.Optional(CONF_ALGORITHM_TUNING): cv.Schema( - { - cv.Optional(CONF_INDEX_OFFSET, default=100): cv.int_range(1, 250), - cv.Optional(CONF_LEARNING_TIME_OFFSET_HOURS, default=12): cv.int_range( - 1, 1000 - ), - cv.Optional(CONF_LEARNING_TIME_GAIN_HOURS, default=12): cv.int_range( - 1, 1000 - ), - cv.Optional( - CONF_GATING_MAX_DURATION_MINUTES, default=720 - ): cv.int_range(0, 3000), - cv.Optional(CONF_STD_INITIAL, default=50): cv.int_, - cv.Optional(CONF_GAIN_FACTOR, default=230): cv.int_range(1, 1000), - } - ) - } -) + +def _gas_sensor( + *, + index_offset: int, + learning_time_offset: int, + learning_time_gain: int, + gating_max_duration: int, + std_initial: int, + gain_factor: int, +) -> cv.Schema: + return sensor.sensor_schema( + icon=ICON_RADIATOR, + accuracy_decimals=0, + device_class=DEVICE_CLASS_AQI, + state_class=STATE_CLASS_MEASUREMENT, + ).extend( + { + cv.Optional(CONF_ALGORITHM_TUNING): cv.Schema( + { + cv.Optional(CONF_INDEX_OFFSET, default=index_offset): cv.int_range( + 1, 250 + ), + cv.Optional( + CONF_LEARNING_TIME_OFFSET_HOURS, default=learning_time_offset + ): cv.int_range(1, 1000), + cv.Optional( + CONF_LEARNING_TIME_GAIN_HOURS, default=learning_time_gain + ): cv.int_range(1, 1000), + cv.Optional( + CONF_GATING_MAX_DURATION_MINUTES, default=gating_max_duration + ): cv.int_range(0, 3000), + cv.Optional(CONF_STD_INITIAL, default=std_initial): cv.int_range( + 10, 5000 + ), + cv.Optional(CONF_GAIN_FACTOR, default=gain_factor): cv.int_range( + 1, 1000 + ), + } + ) + } + ) def float_previously_pct(value): @@ -127,18 +148,22 @@ CONFIG_SCHEMA = ( state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_AUTO_CLEANING_INTERVAL): cv.update_interval, - cv.Optional(CONF_VOC): sensor.sensor_schema( - icon=ICON_RADIATOR, - accuracy_decimals=0, - device_class=DEVICE_CLASS_AQI, - state_class=STATE_CLASS_MEASUREMENT, - ).extend(GAS_SENSOR), - cv.Optional(CONF_NOX): sensor.sensor_schema( - icon=ICON_RADIATOR, - accuracy_decimals=0, - device_class=DEVICE_CLASS_AQI, - state_class=STATE_CLASS_MEASUREMENT, - ).extend(GAS_SENSOR), + cv.Optional(CONF_VOC): _gas_sensor( + index_offset=100, + learning_time_offset=12, + learning_time_gain=12, + gating_max_duration=180, + std_initial=50, + gain_factor=230, + ), + cv.Optional(CONF_NOX): _gas_sensor( + index_offset=1, + learning_time_offset=12, + learning_time_gain=12, + gating_max_duration=720, + std_initial=50, + gain_factor=230, + ), cv.Optional(CONF_STORE_BASELINE, default=True): cv.boolean, cv.Optional(CONF_VOC_BASELINE): cv.hex_uint16_t, cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( @@ -194,16 +219,15 @@ async def to_code(config): await i2c.register_i2c_device(var, config) for key, funcName in SETTING_MAP.items(): - if key in config: - cg.add(getattr(var, funcName)(config[key])) + if cfg := config.get(key): + cg.add(getattr(var, funcName)(cfg)) for key, funcName in SENSOR_MAP.items(): - if key in config: - sens = await sensor.new_sensor(config[key]) + if cfg := config.get(key): + sens = await sensor.new_sensor(cfg) cg.add(getattr(var, funcName)(sens)) - if CONF_VOC in config and CONF_ALGORITHM_TUNING in config[CONF_VOC]: - cfg = config[CONF_VOC][CONF_ALGORITHM_TUNING] + if cfg := config.get(CONF_VOC, {}).get(CONF_ALGORITHM_TUNING): cg.add( var.set_voc_algorithm_tuning( cfg[CONF_INDEX_OFFSET], @@ -214,8 +238,7 @@ async def to_code(config): cfg[CONF_GAIN_FACTOR], ) ) - if CONF_NOX in config and CONF_ALGORITHM_TUNING in config[CONF_NOX]: - cfg = config[CONF_NOX][CONF_ALGORITHM_TUNING] + if cfg := config.get(CONF_NOX, {}).get(CONF_ALGORITHM_TUNING): cg.add( var.set_nox_algorithm_tuning( cfg[CONF_INDEX_OFFSET], @@ -225,12 +248,12 @@ async def to_code(config): cfg[CONF_GAIN_FACTOR], ) ) - if CONF_TEMPERATURE_COMPENSATION in config: + if cfg := config.get(CONF_TEMPERATURE_COMPENSATION): cg.add( var.set_temperature_compensation( - config[CONF_TEMPERATURE_COMPENSATION][CONF_OFFSET], - config[CONF_TEMPERATURE_COMPENSATION][CONF_NORMALIZED_OFFSET_SLOPE], - config[CONF_TEMPERATURE_COMPENSATION][CONF_TIME_CONSTANT], + cfg[CONF_OFFSET], + cfg[CONF_NORMALIZED_OFFSET_SLOPE], + cfg[CONF_TIME_CONSTANT], ) ) From ca72286386aa0f47e609bd3774cab65ccc4047c5 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Fri, 29 Aug 2025 15:42:39 +1000 Subject: [PATCH 103/208] [lvgl] Update hello world (#10469) --- esphome/components/lvgl/hello_world.py | 129 ++++++++++++++++++------- 1 file changed, 96 insertions(+), 33 deletions(-) diff --git a/esphome/components/lvgl/hello_world.py b/esphome/components/lvgl/hello_world.py index 2c2ec6732c..f85da9d8e4 100644 --- a/esphome/components/lvgl/hello_world.py +++ b/esphome/components/lvgl/hello_world.py @@ -4,49 +4,112 @@ from esphome.yaml_util import parse_yaml CONFIG = """ - obj: - radius: 0 + id: hello_world_card_ pad_all: 12 - bg_color: 0xFFFFFF + bg_color: white height: 100% width: 100% + scrollable: false widgets: - - spinner: - id: hello_world_spinner_ - align: center - indicator: - arc_color: tomato - height: 100 - width: 100 - spin_time: 2s - arc_length: 60deg - - label: - id: hello_world_label_ - text: "Hello World!" + - obj: + align: top_mid + outline_width: 0 + border_width: 0 + pad_all: 4 + scrollable: false + height: size_content + width: 100% + layout: + type: flex + flex_flow: row + flex_align_cross: center + flex_align_track: start + flex_align_main: space_between + widgets: + - button: + checkable: true + radius: 4 + text_font: montserrat_20 + on_click: + lvgl.label.update: + id: hello_world_label_ + text: "Clicked!" + widgets: + - label: + text: "Button" + - label: + id: hello_world_title_ + text: ESPHome + text_font: montserrat_20 + width: 100% + text_align: center + on_boot: + lvgl.widget.refresh: hello_world_title_ + hidden: !lambda |- + return lv_obj_get_width(lv_scr_act()) < 400; + - checkbox: + text: Checkbox + id: hello_world_checkbox_ + on_boot: + lvgl.widget.refresh: hello_world_checkbox_ + hidden: !lambda |- + return lv_obj_get_width(lv_scr_act()) < 240; + on_click: + lvgl.label.update: + id: hello_world_label_ + text: "Checked!" + - obj: + id: hello_world_container_ align: center + y: 14 + pad_all: 0 + outline_width: 0 + border_width: 0 + width: 100% + height: size_content + scrollable: false on_click: lvgl.spinner.update: id: hello_world_spinner_ arc_color: springgreen - - checkbox: - pad_all: 8 - text: Checkbox - align: top_right - on_click: - lvgl.label.update: - id: hello_world_label_ - text: "Checked!" - - button: - pad_all: 8 - checkable: true - align: top_left - text_font: montserrat_20 - on_click: - lvgl.label.update: - id: hello_world_label_ - text: "Clicked!" + layout: + type: flex + flex_flow: row_wrap + flex_align_cross: center + flex_align_track: center + flex_align_main: space_evenly widgets: - - label: - text: "Button" + - spinner: + id: hello_world_spinner_ + indicator: + arc_color: tomato + height: 100 + width: 100 + spin_time: 2s + arc_length: 60deg + widgets: + - label: + id: hello_world_label_ + text: "Hello World!" + align: center + - obj: + id: hello_world_qrcode_ + outline_width: 0 + border_width: 0 + hidden: !lambda |- + return lv_obj_get_width(lv_scr_act()) < 300 && lv_obj_get_height(lv_scr_act()) < 400; + widgets: + - label: + text_font: montserrat_14 + text: esphome.io + align: top_mid + - qrcode: + text: "https://esphome.io" + size: 80 + align: bottom_mid + on_boot: + lvgl.widget.refresh: hello_world_qrcode_ + - slider: width: 80% align: bottom_mid From fd568d9af36a8d17fd795c2b5b89a674bb502070 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 29 Aug 2025 10:14:15 -0500 Subject: [PATCH 104/208] Bump aioesphomeapi from 39.0.0 to 39.0.1 (#10491) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 910f70fe45..32fdfabcda 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile esptool==5.0.2 click==8.1.7 esphome-dashboard==20250828.0 -aioesphomeapi==39.0.0 +aioesphomeapi==39.0.1 zeroconf==0.147.0 puremagic==1.30 ruamel.yaml==0.18.15 # dashboard_import From 983b3cb87924f9145c9f0f895e78f927b81d9ede Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Sat, 30 Aug 2025 18:43:26 +1200 Subject: [PATCH 105/208] [mipi] Add type to models for better type hinting downstream (#10475) --- esphome/components/mipi/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/mipi/__init__.py b/esphome/components/mipi/__init__.py index 570a021cff..a9ecb9d79a 100644 --- a/esphome/components/mipi/__init__.py +++ b/esphome/components/mipi/__init__.py @@ -2,7 +2,7 @@ # Various configuration constants for MIPI displays # Various utility functions for MIPI DBI configuration -from typing import Any +from typing import Any, Self from esphome.components.const import CONF_COLOR_DEPTH from esphome.components.display import CONF_SHOW_TEST_CARD, display_ns @@ -222,7 +222,7 @@ def delay(ms): class DriverChip: - models = {} + models: dict[str, Self] = {} def __init__( self, From e29f0ee7f860e513adaad75b9b9ea890bacf60ea Mon Sep 17 00:00:00 2001 From: DT-art1 <81360462+DT-art1@users.noreply.github.com> Date: Sun, 31 Aug 2025 16:26:15 +0200 Subject: [PATCH 106/208] Add JPEG encoder support via new camera_encoder component (#9459) Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston --- CODEOWNERS | 1 + esphome/components/camera/buffer.h | 18 ++++ esphome/components/camera/buffer_impl.cpp | 20 +++++ esphome/components/camera/buffer_impl.h | 26 ++++++ esphome/components/camera/camera.h | 43 ++++++++++ esphome/components/camera/encoder.h | 69 ++++++++++++++++ esphome/components/camera_encoder/__init__.py | 62 ++++++++++++++ .../camera_encoder/encoder_buffer_impl.cpp | 23 ++++++ .../camera_encoder/encoder_buffer_impl.h | 25 ++++++ .../esp32_camera_jpeg_encoder.cpp | 82 +++++++++++++++++++ .../esp32_camera_jpeg_encoder.h | 39 +++++++++ tests/components/camera_encoder/common.yaml | 5 ++ .../camera_encoder/test.esp32-ard.yaml | 1 + .../camera_encoder/test.esp32-idf.yaml | 1 + 14 files changed, 415 insertions(+) create mode 100644 esphome/components/camera/buffer.h create mode 100644 esphome/components/camera/buffer_impl.cpp create mode 100644 esphome/components/camera/buffer_impl.h create mode 100644 esphome/components/camera/encoder.h create mode 100644 esphome/components/camera_encoder/__init__.py create mode 100644 esphome/components/camera_encoder/encoder_buffer_impl.cpp create mode 100644 esphome/components/camera_encoder/encoder_buffer_impl.h create mode 100644 esphome/components/camera_encoder/esp32_camera_jpeg_encoder.cpp create mode 100644 esphome/components/camera_encoder/esp32_camera_jpeg_encoder.h create mode 100644 tests/components/camera_encoder/common.yaml create mode 100644 tests/components/camera_encoder/test.esp32-ard.yaml create mode 100644 tests/components/camera_encoder/test.esp32-idf.yaml diff --git a/CODEOWNERS b/CODEOWNERS index a1a74e9c99..116f35f3b6 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -89,6 +89,7 @@ esphome/components/bp5758d/* @Cossid esphome/components/button/* @esphome/core esphome/components/bytebuffer/* @clydebarrow esphome/components/camera/* @DT-art1 @bdraco +esphome/components/camera_encoder/* @DT-art1 esphome/components/canbus/* @danielschramm @mvturnho esphome/components/cap1188/* @mreditor97 esphome/components/captive_portal/* @esphome/core diff --git a/esphome/components/camera/buffer.h b/esphome/components/camera/buffer.h new file mode 100644 index 0000000000..f860877b94 --- /dev/null +++ b/esphome/components/camera/buffer.h @@ -0,0 +1,18 @@ +#pragma once + +#include +#include + +namespace esphome::camera { + +/// Interface for a generic buffer that stores image data. +class Buffer { + public: + /// Returns a pointer to the buffer's data. + virtual uint8_t *get_data_buffer() = 0; + /// Returns the length of the buffer in bytes. + virtual size_t get_data_length() = 0; + virtual ~Buffer() = default; +}; + +} // namespace esphome::camera diff --git a/esphome/components/camera/buffer_impl.cpp b/esphome/components/camera/buffer_impl.cpp new file mode 100644 index 0000000000..d17a4e2707 --- /dev/null +++ b/esphome/components/camera/buffer_impl.cpp @@ -0,0 +1,20 @@ +#include "buffer_impl.h" + +namespace esphome::camera { + +BufferImpl::BufferImpl(size_t size) { + this->data_ = this->allocator_.allocate(size); + this->size_ = size; +} + +BufferImpl::BufferImpl(CameraImageSpec *spec) { + this->data_ = this->allocator_.allocate(spec->bytes_per_image()); + this->size_ = spec->bytes_per_image(); +} + +BufferImpl::~BufferImpl() { + if (this->data_ != nullptr) + this->allocator_.deallocate(this->data_, this->size_); +} + +} // namespace esphome::camera diff --git a/esphome/components/camera/buffer_impl.h b/esphome/components/camera/buffer_impl.h new file mode 100644 index 0000000000..46398295fa --- /dev/null +++ b/esphome/components/camera/buffer_impl.h @@ -0,0 +1,26 @@ +#pragma once + +#include "buffer.h" +#include "camera.h" + +namespace esphome::camera { + +/// Default implementation of Buffer Interface. +/// Uses a RAMAllocator for memory reservation. +class BufferImpl : public Buffer { + public: + explicit BufferImpl(size_t size); + explicit BufferImpl(CameraImageSpec *spec); + // -------- Buffer -------- + uint8_t *get_data_buffer() override { return data_; } + size_t get_data_length() override { return size_; } + // ------------------------ + ~BufferImpl() override; + + protected: + RAMAllocator allocator_; + size_t size_{}; + uint8_t *data_{}; +}; + +} // namespace esphome::camera diff --git a/esphome/components/camera/camera.h b/esphome/components/camera/camera.h index fb9da58cc1..c28a756a06 100644 --- a/esphome/components/camera/camera.h +++ b/esphome/components/camera/camera.h @@ -15,6 +15,26 @@ namespace camera { */ enum CameraRequester : uint8_t { IDLE, API_REQUESTER, WEB_REQUESTER }; +/// Enumeration of different pixel formats. +enum PixelFormat : uint8_t { + PIXEL_FORMAT_GRAYSCALE = 0, ///< 8-bit grayscale. + PIXEL_FORMAT_RGB565, ///< 16-bit RGB (5-6-5). + PIXEL_FORMAT_BGR888, ///< RGB pixel data in 8-bit format, stored as B, G, R (1 byte each). +}; + +/// Returns string name for a given PixelFormat. +inline const char *to_string(PixelFormat format) { + switch (format) { + case PIXEL_FORMAT_GRAYSCALE: + return "PIXEL_FORMAT_GRAYSCALE"; + case PIXEL_FORMAT_RGB565: + return "PIXEL_FORMAT_RGB565"; + case PIXEL_FORMAT_BGR888: + return "PIXEL_FORMAT_BGR888"; + } + return "PIXEL_FORMAT_UNKNOWN"; +} + /** Abstract camera image base class. * Encapsulates the JPEG encoded data and it is shared among * all connected clients. @@ -43,6 +63,29 @@ class CameraImageReader { virtual ~CameraImageReader() {} }; +/// Specification of a caputured camera image. +/// This struct defines the format and size details for images captured +/// or processed by a camera component. +struct CameraImageSpec { + uint16_t width; + uint16_t height; + PixelFormat format; + size_t bytes_per_pixel() { + switch (format) { + case PIXEL_FORMAT_GRAYSCALE: + return 1; + case PIXEL_FORMAT_RGB565: + return 2; + case PIXEL_FORMAT_BGR888: + return 3; + } + + return 1; + } + size_t bytes_per_row() { return bytes_per_pixel() * width; } + size_t bytes_per_image() { return bytes_per_pixel() * width * height; } +}; + /** Abstract camera base class. Collaborates with API. * 1) API server starts and installs callback (add_image_callback) * which is called by the camera when a new image is available. diff --git a/esphome/components/camera/encoder.h b/esphome/components/camera/encoder.h new file mode 100644 index 0000000000..17ce828d23 --- /dev/null +++ b/esphome/components/camera/encoder.h @@ -0,0 +1,69 @@ +#pragma once + +#include "buffer.h" +#include "camera.h" + +namespace esphome::camera { + +/// Result codes from the encoder used to control camera pipeline flow. +enum EncoderError : uint8_t { + ENCODER_ERROR_SUCCESS = 0, ///< Encoding succeeded, continue pipeline normally. + ENCODER_ERROR_SKIP_FRAME, ///< Skip current frame, try again on next frame. + ENCODER_ERROR_RETRY_FRAME, ///< Retry current frame, after buffer growth or for incremental encoding. + ENCODER_ERROR_CONFIGURATION ///< Fatal config error, shut down pipeline. +}; + +/// Converts EncoderError to string. +inline const char *to_string(EncoderError error) { + switch (error) { + case ENCODER_ERROR_SUCCESS: + return "ENCODER_ERROR_SUCCESS"; + case ENCODER_ERROR_SKIP_FRAME: + return "ENCODER_ERROR_SKIP_FRAME"; + case ENCODER_ERROR_RETRY_FRAME: + return "ENCODER_ERROR_RETRY_FRAME"; + case ENCODER_ERROR_CONFIGURATION: + return "ENCODER_ERROR_CONFIGURATION"; + } + return "ENCODER_ERROR_INVALID"; +} + +/// Interface for an encoder buffer supporting resizing and variable-length data. +class EncoderBuffer { + public: + /// Sets logical buffer size, reallocates if needed. + /// @param size Required size in bytes. + /// @return true on success, false on allocation failure. + virtual bool set_buffer_size(size_t size) = 0; + + /// Returns a pointer to the buffer data. + virtual uint8_t *get_data() const = 0; + + /// Returns number of bytes currently used. + virtual size_t get_size() const = 0; + + /// Returns total allocated buffer size. + virtual size_t get_max_size() const = 0; + + virtual ~EncoderBuffer() = default; +}; + +/// Interface for image encoders used in a camera pipeline. +class Encoder { + public: + /// Encodes pixel data from a previous camera pipeline stage. + /// @param spec Specification of the input pixel data. + /// @param pixels Image pixels in RGB or grayscale format, as specified in @p spec. + /// @return EncoderError Indicating the result of the encoding operation. + virtual EncoderError encode_pixels(CameraImageSpec *spec, Buffer *pixels) = 0; + + /// Returns the encoder's output buffer. + /// @return Pointer to an EncoderBuffer containing encoded data. + virtual EncoderBuffer *get_output_buffer() = 0; + + /// Prints the encoder's configuration to the log. + virtual void dump_config() = 0; + virtual ~Encoder() = default; +}; + +} // namespace esphome::camera diff --git a/esphome/components/camera_encoder/__init__.py b/esphome/components/camera_encoder/__init__.py new file mode 100644 index 0000000000..c0f0ca2fe0 --- /dev/null +++ b/esphome/components/camera_encoder/__init__.py @@ -0,0 +1,62 @@ +import esphome.codegen as cg +from esphome.components.esp32 import add_idf_component +import esphome.config_validation as cv +from esphome.const import CONF_BUFFER_SIZE, CONF_ID, CONF_TYPE +from esphome.core import CORE +from esphome.types import ConfigType + +CODEOWNERS = ["@DT-art1"] + +AUTO_LOAD = ["camera"] + +CONF_BUFFER_EXPAND_SIZE = "buffer_expand_size" +CONF_ENCODER_BUFFER_ID = "encoder_buffer_id" +CONF_QUALITY = "quality" + +ESP32_CAMERA_ENCODER = "esp32_camera" + +camera_ns = cg.esphome_ns.namespace("camera") +camera_encoder_ns = cg.esphome_ns.namespace("camera_encoder") + +Encoder = camera_ns.class_("Encoder") +EncoderBufferImpl = camera_encoder_ns.class_("EncoderBufferImpl") + +ESP32CameraJPEGEncoder = camera_encoder_ns.class_("ESP32CameraJPEGEncoder", Encoder) + +MAX_JPEG_BUFFER_SIZE_2MB = 2 * 1024 * 1024 + +ESP32_CAMERA_ENCODER_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(ESP32CameraJPEGEncoder), + cv.Optional(CONF_QUALITY, default=80): cv.int_range(1, 100), + cv.Optional(CONF_BUFFER_SIZE, default=4096): cv.int_range( + 1024, MAX_JPEG_BUFFER_SIZE_2MB + ), + cv.Optional(CONF_BUFFER_EXPAND_SIZE, default=1024): cv.int_range( + 0, MAX_JPEG_BUFFER_SIZE_2MB + ), + cv.GenerateID(CONF_ENCODER_BUFFER_ID): cv.declare_id(EncoderBufferImpl), + } +) + +CONFIG_SCHEMA = cv.typed_schema( + { + ESP32_CAMERA_ENCODER: ESP32_CAMERA_ENCODER_SCHEMA, + }, + default_type=ESP32_CAMERA_ENCODER, +) + + +async def to_code(config: ConfigType) -> None: + buffer = cg.new_Pvariable(config[CONF_ENCODER_BUFFER_ID]) + cg.add(buffer.set_buffer_size(config[CONF_BUFFER_SIZE])) + if config[CONF_TYPE] == ESP32_CAMERA_ENCODER: + if CORE.using_esp_idf: + add_idf_component(name="espressif/esp32-camera", ref="2.1.0") + cg.add_build_flag("-DUSE_ESP32_CAMERA_JPEG_ENCODER") + var = cg.new_Pvariable( + config[CONF_ID], + config[CONF_QUALITY], + buffer, + ) + cg.add(var.set_buffer_expand_size(config[CONF_BUFFER_EXPAND_SIZE])) diff --git a/esphome/components/camera_encoder/encoder_buffer_impl.cpp b/esphome/components/camera_encoder/encoder_buffer_impl.cpp new file mode 100644 index 0000000000..db84026496 --- /dev/null +++ b/esphome/components/camera_encoder/encoder_buffer_impl.cpp @@ -0,0 +1,23 @@ +#include "encoder_buffer_impl.h" + +namespace esphome::camera_encoder { + +bool EncoderBufferImpl::set_buffer_size(size_t size) { + if (size > this->capacity_) { + uint8_t *p = this->allocator_.reallocate(this->data_, size); + if (p == nullptr) + return false; + + this->data_ = p; + this->capacity_ = size; + } + this->size_ = size; + return true; +} + +EncoderBufferImpl::~EncoderBufferImpl() { + if (this->data_ != nullptr) + this->allocator_.deallocate(this->data_, this->capacity_); +} + +} // namespace esphome::camera_encoder diff --git a/esphome/components/camera_encoder/encoder_buffer_impl.h b/esphome/components/camera_encoder/encoder_buffer_impl.h new file mode 100644 index 0000000000..13eccb7d56 --- /dev/null +++ b/esphome/components/camera_encoder/encoder_buffer_impl.h @@ -0,0 +1,25 @@ +#pragma once + +#include "esphome/components/camera/encoder.h" +#include "esphome/core/helpers.h" + +namespace esphome::camera_encoder { + +class EncoderBufferImpl : public camera::EncoderBuffer { + public: + // --- EncoderBuffer --- + bool set_buffer_size(size_t size) override; + uint8_t *get_data() const override { return this->data_; } + size_t get_size() const override { return this->size_; } + size_t get_max_size() const override { return this->capacity_; } + // ---------------------- + ~EncoderBufferImpl() override; + + protected: + RAMAllocator allocator_; + size_t capacity_{}; + size_t size_{}; + uint8_t *data_{}; +}; + +} // namespace esphome::camera_encoder diff --git a/esphome/components/camera_encoder/esp32_camera_jpeg_encoder.cpp b/esphome/components/camera_encoder/esp32_camera_jpeg_encoder.cpp new file mode 100644 index 0000000000..7e21122087 --- /dev/null +++ b/esphome/components/camera_encoder/esp32_camera_jpeg_encoder.cpp @@ -0,0 +1,82 @@ +#ifdef USE_ESP32_CAMERA_JPEG_ENCODER + +#include "esp32_camera_jpeg_encoder.h" + +namespace esphome::camera_encoder { + +static const char *const TAG = "camera_encoder"; + +ESP32CameraJPEGEncoder::ESP32CameraJPEGEncoder(uint8_t quality, camera::EncoderBuffer *output) { + this->quality_ = quality; + this->output_ = output; +} + +camera::EncoderError ESP32CameraJPEGEncoder::encode_pixels(camera::CameraImageSpec *spec, camera::Buffer *pixels) { + this->bytes_written_ = 0; + this->out_of_output_memory_ = false; + bool success = fmt2jpg_cb(pixels->get_data_buffer(), pixels->get_data_length(), spec->width, spec->height, + to_internal_(spec->format), this->quality_, callback_, this); + + if (!success) + return camera::ENCODER_ERROR_CONFIGURATION; + + if (this->out_of_output_memory_) { + if (this->buffer_expand_size_ <= 0) + return camera::ENCODER_ERROR_SKIP_FRAME; + + size_t current_size = this->output_->get_max_size(); + size_t new_size = this->output_->get_max_size() + this->buffer_expand_size_; + if (!this->output_->set_buffer_size(new_size)) { + ESP_LOGE(TAG, "Failed to expand output buffer."); + this->buffer_expand_size_ = 0; + return camera::ENCODER_ERROR_SKIP_FRAME; + } + + ESP_LOGD(TAG, "Output buffer expanded (%u -> %u).", current_size, this->output_->get_max_size()); + return camera::ENCODER_ERROR_RETRY_FRAME; + } + + this->output_->set_buffer_size(this->bytes_written_); + return camera::ENCODER_ERROR_SUCCESS; +} + +void ESP32CameraJPEGEncoder::dump_config() { + ESP_LOGCONFIG(TAG, + "ESP32 Camera JPEG Encoder:\n" + " Size: %zu\n" + " Quality: %d\n" + " Expand: %d\n", + this->output_->get_max_size(), this->quality_, this->buffer_expand_size_); +} + +size_t ESP32CameraJPEGEncoder::callback_(void *arg, size_t index, const void *data, size_t len) { + ESP32CameraJPEGEncoder *that = reinterpret_cast(arg); + uint8_t *buffer = that->output_->get_data(); + size_t buffer_length = that->output_->get_max_size(); + if (index + len > buffer_length) { + that->out_of_output_memory_ = true; + return 0; + } + + std::memcpy(&buffer[index], data, len); + that->bytes_written_ += len; + return len; +} + +pixformat_t ESP32CameraJPEGEncoder::to_internal_(camera::PixelFormat format) { + switch (format) { + case camera::PIXEL_FORMAT_GRAYSCALE: + return PIXFORMAT_GRAYSCALE; + case camera::PIXEL_FORMAT_RGB565: + return PIXFORMAT_RGB565; + // Internal representation for RGB is in byte order: B, G, R + case camera::PIXEL_FORMAT_BGR888: + return PIXFORMAT_RGB888; + } + + return PIXFORMAT_GRAYSCALE; +} + +} // namespace esphome::camera_encoder + +#endif diff --git a/esphome/components/camera_encoder/esp32_camera_jpeg_encoder.h b/esphome/components/camera_encoder/esp32_camera_jpeg_encoder.h new file mode 100644 index 0000000000..b585252584 --- /dev/null +++ b/esphome/components/camera_encoder/esp32_camera_jpeg_encoder.h @@ -0,0 +1,39 @@ +#pragma once + +#ifdef USE_ESP32_CAMERA_JPEG_ENCODER + +#include + +#include "esphome/components/camera/encoder.h" + +namespace esphome::camera_encoder { + +/// Encoder that uses the software-based JPEG implementation from Espressif's esp32-camera component. +class ESP32CameraJPEGEncoder : public camera::Encoder { + public: + /// Constructs a ESP32CameraJPEGEncoder instance. + /// @param quality Sets the quality of the encoded image (1-100). + /// @param output Pointer to preallocated output buffer. + ESP32CameraJPEGEncoder(uint8_t quality, camera::EncoderBuffer *output); + /// Sets the number of bytes to expand the output buffer on underflow during encoding. + /// @param buffer_expand_size Number of bytes to expand the buffer. + void set_buffer_expand_size(size_t buffer_expand_size) { this->buffer_expand_size_ = buffer_expand_size; } + // -------- Encoder -------- + camera::EncoderError encode_pixels(camera::CameraImageSpec *spec, camera::Buffer *pixels) override; + camera::EncoderBuffer *get_output_buffer() override { return output_; } + void dump_config() override; + // ------------------------- + protected: + static size_t callback_(void *arg, size_t index, const void *data, size_t len); + pixformat_t to_internal_(camera::PixelFormat format); + + camera::EncoderBuffer *output_{}; + size_t buffer_expand_size_{}; + size_t bytes_written_{}; + uint8_t quality_{}; + bool out_of_output_memory_{}; +}; + +} // namespace esphome::camera_encoder + +#endif diff --git a/tests/components/camera_encoder/common.yaml b/tests/components/camera_encoder/common.yaml new file mode 100644 index 0000000000..8fd7a8ce47 --- /dev/null +++ b/tests/components/camera_encoder/common.yaml @@ -0,0 +1,5 @@ +camera_encoder: + id: jpeg_encoder + quality: 80 + buffer_size: 4096 + buffer_expand_size: 1024 diff --git a/tests/components/camera_encoder/test.esp32-ard.yaml b/tests/components/camera_encoder/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/camera_encoder/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/camera_encoder/test.esp32-idf.yaml b/tests/components/camera_encoder/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/camera_encoder/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml From da21174c6d2181e27fec5c6ba6f14a4bb59de318 Mon Sep 17 00:00:00 2001 From: Felix Kaechele Date: Sun, 31 Aug 2025 17:02:56 -0400 Subject: [PATCH 107/208] [sntp] Use callbacks to trigger `on_time_sync` for ESP32 and ESP8266 (#10390) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/sntp/sntp_component.cpp | 45 +++++++++++++++++----- esphome/components/sntp/sntp_component.h | 7 ++++ 2 files changed, 43 insertions(+), 9 deletions(-) diff --git a/esphome/components/sntp/sntp_component.cpp b/esphome/components/sntp/sntp_component.cpp index ccd9af3153..1cca5e8043 100644 --- a/esphome/components/sntp/sntp_component.cpp +++ b/esphome/components/sntp/sntp_component.cpp @@ -14,8 +14,13 @@ namespace sntp { static const char *const TAG = "sntp"; +#if defined(USE_ESP32) +SNTPComponent *SNTPComponent::instance = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +#endif + void SNTPComponent::setup() { #if defined(USE_ESP32) + SNTPComponent::instance = this; if (esp_sntp_enabled()) { esp_sntp_stop(); } @@ -25,6 +30,11 @@ void SNTPComponent::setup() { esp_sntp_setservername(i++, server.c_str()); } esp_sntp_set_sync_interval(this->get_update_interval()); + esp_sntp_set_time_sync_notification_cb([](struct timeval *tv) { + if (SNTPComponent::instance != nullptr) { + SNTPComponent::instance->defer([]() { SNTPComponent::instance->time_synced(); }); + } + }); esp_sntp_init(); #else sntp_stop(); @@ -34,6 +44,14 @@ void SNTPComponent::setup() { for (auto &server : this->servers_) { sntp_setservername(i++, server.c_str()); } + +#if defined(USE_ESP8266) + settimeofday_cb([this](bool from_sntp) { + if (from_sntp) + this->time_synced(); + }); +#endif + sntp_init(); #endif } @@ -46,7 +64,8 @@ void SNTPComponent::dump_config() { } void SNTPComponent::update() { #if !defined(USE_ESP32) - // force resync + // Some platforms currently cannot set the sync interval at runtime so we need + // to do the re-sync by hand for now. if (sntp_enabled()) { sntp_stop(); this->has_time_ = false; @@ -55,23 +74,31 @@ void SNTPComponent::update() { #endif } void SNTPComponent::loop() { +// The loop is used to infer whether we have valid time on platforms where we +// cannot tell whether SNTP has succeeded. +// One limitation of this approach is that we cannot tell if it was the SNTP +// component that set the time. +// ESP-IDF and ESP8266 use callbacks from the SNTP task to trigger the +// `on_time_sync` trigger on successful sync events. +#if defined(USE_ESP32) || defined(USE_ESP8266) + this->disable_loop(); +#endif + if (this->has_time_) return; + this->time_synced(); +} + +void SNTPComponent::time_synced() { auto time = this->now(); - if (!time.is_valid()) + this->has_time_ = time.is_valid(); + if (!this->has_time_) return; ESP_LOGD(TAG, "Synchronized time: %04d-%02d-%02d %02d:%02d:%02d", time.year, time.month, time.day_of_month, time.hour, time.minute, time.second); this->time_sync_callback_.call(); - this->has_time_ = true; - -#ifdef USE_ESP_IDF - // On ESP-IDF, time sync is permanent and update() doesn't force resync - // Time is now synchronized, no need to check anymore - this->disable_loop(); -#endif } } // namespace sntp diff --git a/esphome/components/sntp/sntp_component.h b/esphome/components/sntp/sntp_component.h index a4e8267383..dd4c71e082 100644 --- a/esphome/components/sntp/sntp_component.h +++ b/esphome/components/sntp/sntp_component.h @@ -26,9 +26,16 @@ class SNTPComponent : public time::RealTimeClock { void update() override; void loop() override; + void time_synced(); + protected: std::vector servers_; bool has_time_{false}; + +#if defined(USE_ESP32) + private: + static SNTPComponent *instance; +#endif }; } // namespace sntp From a25b544c3bdcc8233acc199830a1ce2f9cae64c6 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 1 Sep 2025 09:22:11 +1200 Subject: [PATCH 108/208] [display] Allow page actions to have auto generated display id (#10460) --- esphome/components/display/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/display/__init__.py b/esphome/components/display/__init__.py index 8021a8f9b1..e55afcebbf 100644 --- a/esphome/components/display/__init__.py +++ b/esphome/components/display/__init__.py @@ -176,7 +176,7 @@ async def display_page_show_to_code(config, action_id, template_arg, args): DisplayPageShowNextAction, maybe_simple_id( { - cv.Required(CONF_ID): cv.templatable(cv.use_id(Display)), + cv.GenerateID(CONF_ID): cv.templatable(cv.use_id(Display)), } ), ) @@ -190,7 +190,7 @@ async def display_page_show_next_to_code(config, action_id, template_arg, args): DisplayPageShowPrevAction, maybe_simple_id( { - cv.Required(CONF_ID): cv.templatable(cv.use_id(Display)), + cv.GenerateID(CONF_ID): cv.templatable(cv.use_id(Display)), } ), ) From 905e2906fec68b46fe13b6d8ced986dd2bd3ff19 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Mon, 1 Sep 2025 00:54:35 +0200 Subject: [PATCH 109/208] [nrf52] add dfu (#9319) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/nrf52/__init__.py | 45 +++++++++++++++- esphome/components/nrf52/const.py | 1 + esphome/components/nrf52/dfu.cpp | 51 +++++++++++++++++++ esphome/components/nrf52/dfu.h | 24 +++++++++ esphome/core/defines.h | 4 ++ .../components/nrf52/test.nrf52-adafruit.yaml | 7 +++ 6 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 esphome/components/nrf52/dfu.cpp create mode 100644 esphome/components/nrf52/dfu.h create mode 100644 tests/components/nrf52/test.nrf52-adafruit.yaml diff --git a/esphome/components/nrf52/__init__.py b/esphome/components/nrf52/__init__.py index e75bf8192c..c388bb0f5e 100644 --- a/esphome/components/nrf52/__init__.py +++ b/esphome/components/nrf52/__init__.py @@ -2,10 +2,13 @@ from __future__ import annotations from pathlib import Path +from esphome import pins import esphome.codegen as cg from esphome.components.zephyr import ( copy_files as zephyr_copy_files, zephyr_add_pm_static, + zephyr_add_prj_conf, + zephyr_data, zephyr_set_core_data, zephyr_to_code, ) @@ -18,6 +21,8 @@ import esphome.config_validation as cv from esphome.const import ( CONF_BOARD, CONF_FRAMEWORK, + CONF_ID, + CONF_RESET_PIN, KEY_CORE, KEY_FRAMEWORK_VERSION, KEY_TARGET_FRAMEWORK, @@ -90,18 +95,43 @@ def _detect_bootloader(config: ConfigType) -> ConfigType: return config +nrf52_ns = cg.esphome_ns.namespace("nrf52") +DeviceFirmwareUpdate = nrf52_ns.class_("DeviceFirmwareUpdate", cg.Component) + +CONF_DFU = "dfu" + CONFIG_SCHEMA = cv.All( + set_core_data, cv.Schema( { cv.Required(CONF_BOARD): cv.string_strict, cv.Optional(KEY_BOOTLOADER): cv.one_of(*BOOTLOADERS, lower=True), + cv.Optional(CONF_DFU): cv.Schema( + { + cv.GenerateID(): cv.declare_id(DeviceFirmwareUpdate), + cv.Required(CONF_RESET_PIN): pins.gpio_output_pin_schema, + } + ), } ), _detect_bootloader, - set_core_data, ) +def _validate_mcumgr(config): + bootloader = zephyr_data()[KEY_BOOTLOADER] + if bootloader == BOOTLOADER_MCUBOOT: + raise cv.Invalid(f"'{bootloader}' bootloader does not support DFU") + + +def _final_validate(config): + if CONF_DFU in config: + _validate_mcumgr(config) + + +FINAL_VALIDATE_SCHEMA = _final_validate + + @coroutine_with_priority(1000) async def to_code(config: ConfigType) -> None: """Convert the configuration to code.""" @@ -136,6 +166,19 @@ async def to_code(config: ConfigType) -> None: zephyr_to_code(config) + if dfu_config := config.get(CONF_DFU): + CORE.add_job(_dfu_to_code, dfu_config) + + +@coroutine_with_priority(90) +async def _dfu_to_code(dfu_config): + cg.add_define("USE_NRF52_DFU") + var = cg.new_Pvariable(dfu_config[CONF_ID]) + pin = await cg.gpio_pin_expression(dfu_config[CONF_RESET_PIN]) + cg.add(var.set_reset_pin(pin)) + zephyr_add_prj_conf("CDC_ACM_DTE_RATE_CALLBACK_SUPPORT", True) + await cg.register_component(var, dfu_config) + def copy_files() -> None: """Copy files to the build directory.""" diff --git a/esphome/components/nrf52/const.py b/esphome/components/nrf52/const.py index 715d527a66..977ca2252a 100644 --- a/esphome/components/nrf52/const.py +++ b/esphome/components/nrf52/const.py @@ -2,6 +2,7 @@ BOOTLOADER_ADAFRUIT = "adafruit" BOOTLOADER_ADAFRUIT_NRF52_SD132 = "adafruit_nrf52_sd132" BOOTLOADER_ADAFRUIT_NRF52_SD140_V6 = "adafruit_nrf52_sd140_v6" BOOTLOADER_ADAFRUIT_NRF52_SD140_V7 = "adafruit_nrf52_sd140_v7" + EXTRA_ADC = [ "VDD", "VDDHDIV5", diff --git a/esphome/components/nrf52/dfu.cpp b/esphome/components/nrf52/dfu.cpp new file mode 100644 index 0000000000..9e49373467 --- /dev/null +++ b/esphome/components/nrf52/dfu.cpp @@ -0,0 +1,51 @@ +#include "dfu.h" + +#ifdef USE_NRF52_DFU + +#include +#include +#include +#include "esphome/core/log.h" + +namespace esphome { +namespace nrf52 { + +static const char *const TAG = "dfu"; + +volatile bool goto_dfu = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +static const uint32_t DFU_DBL_RESET_MAGIC = 0x5A1AD5; // SALADS + +#define DEVICE_AND_COMMA(node_id) DEVICE_DT_GET(node_id), + +static void cdc_dte_rate_callback(const struct device * /*unused*/, uint32_t rate) { + if (rate == 1200) { + goto_dfu = true; + } +} +void DeviceFirmwareUpdate::setup() { + this->reset_pin_->setup(); + const struct device *cdc_dev[] = {DT_FOREACH_STATUS_OKAY(zephyr_cdc_acm_uart, DEVICE_AND_COMMA)}; + for (auto &idx : cdc_dev) { + cdc_acm_dte_rate_callback_set(idx, cdc_dte_rate_callback); + } +} + +void DeviceFirmwareUpdate::loop() { + if (goto_dfu) { + goto_dfu = false; + volatile uint32_t *dbl_reset_mem = (volatile uint32_t *) 0x20007F7C; + (*dbl_reset_mem) = DFU_DBL_RESET_MAGIC; + this->reset_pin_->digital_write(true); + } +} + +void DeviceFirmwareUpdate::dump_config() { + ESP_LOGCONFIG(TAG, "DFU:"); + LOG_PIN(" RESET Pin: ", this->reset_pin_); +} + +} // namespace nrf52 +} // namespace esphome + +#endif diff --git a/esphome/components/nrf52/dfu.h b/esphome/components/nrf52/dfu.h new file mode 100644 index 0000000000..979a4567cf --- /dev/null +++ b/esphome/components/nrf52/dfu.h @@ -0,0 +1,24 @@ +#pragma once + +#include "esphome/core/defines.h" +#ifdef USE_NRF52_DFU +#include "esphome/core/component.h" +#include "esphome/core/gpio.h" + +namespace esphome { +namespace nrf52 { +class DeviceFirmwareUpdate : public Component { + public: + void setup() override; + void loop() override; + void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; } + void dump_config() override; + + protected: + GPIOPin *reset_pin_; +}; + +} // namespace nrf52 +} // namespace esphome + +#endif diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 5df3bcf475..9a7e090b83 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -240,6 +240,10 @@ #define USE_SOCKET_SELECT_SUPPORT #endif +#ifdef USE_NRF52 +#define USE_NRF52_DFU +#endif + // Disabled feature flags // #define USE_BSEC // Requires a library with proprietary license // #define USE_BSEC2 // Requires a library with proprietary license diff --git a/tests/components/nrf52/test.nrf52-adafruit.yaml b/tests/components/nrf52/test.nrf52-adafruit.yaml new file mode 100644 index 0000000000..3fe80209b6 --- /dev/null +++ b/tests/components/nrf52/test.nrf52-adafruit.yaml @@ -0,0 +1,7 @@ +nrf52: + dfu: + reset_pin: + number: 14 + inverted: true + mode: + output: true From 6d834c019d710117eaf414b28f49bbb8eca8da77 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 31 Aug 2025 20:01:15 -0500 Subject: [PATCH 110/208] Fix incorrect entity count due to undefined execution order with globals (#10497) --- esphome/core/config.py | 2 +- tests/unit_tests/core/test_config.py | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/esphome/core/config.py b/esphome/core/config.py index 90768a4b09..b6ff1d8afd 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -423,7 +423,7 @@ async def _add_automations(config): DATETIME_SUBTYPES = {"date", "time", "datetime"} -@coroutine_with_priority(-100.0) +@coroutine_with_priority(-1000.0) async def _add_platform_defines() -> None: # Generate compile-time defines for platforms that have actual entities # Only add USE_* and count defines when there are entities diff --git a/tests/unit_tests/core/test_config.py b/tests/unit_tests/core/test_config.py index 46e3b513d7..f5ba5221ed 100644 --- a/tests/unit_tests/core/test_config.py +++ b/tests/unit_tests/core/test_config.py @@ -8,6 +8,7 @@ import pytest from esphome import config_validation as cv, core from esphome.const import CONF_AREA, CONF_AREAS, CONF_DEVICES +from esphome.core import config from esphome.core.config import Area, validate_area_config from .common import load_config_from_fixture @@ -223,3 +224,24 @@ def test_device_duplicate_id( # Check for the specific error message from IDPassValidationStep captured = capsys.readouterr() assert "ID duplicate_device redefined!" in captured.out + + +def test_add_platform_defines_priority() -> None: + """Test that _add_platform_defines runs after globals. + + This ensures the fix for issue #10431 where sensor counts were incorrect + when lambdas were present. The function must run at a lower priority than + globals (-100.0) to ensure all components (including those using globals + in lambdas) have registered their entities before the count defines are + generated. + + Regression test for https://github.com/esphome/esphome/issues/10431 + """ + # Import globals to check its priority + from esphome.components.globals import to_code as globals_to_code + + # _add_platform_defines must run AFTER globals (lower priority number = runs later) + assert config._add_platform_defines.priority < globals_to_code.priority, ( + f"_add_platform_defines priority ({config._add_platform_defines.priority}) must be lower than " + f"globals priority ({globals_to_code.priority}) to fix issue #10431 (sensor count bug with lambdas)" + ) From 6daeffcefda4f91eb1e0ee1e720073dce496068e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 31 Aug 2025 20:07:29 -0500 Subject: [PATCH 111/208] [bluetooth_proxy] Expose configured scanning mode in API responses (#10490) --- esphome/components/api/api.proto | 1 + esphome/components/api/api_pb2.cpp | 2 ++ esphome/components/api/api_pb2.h | 3 ++- esphome/components/api/api_pb2_dump.cpp | 1 + esphome/components/bluetooth_proxy/bluetooth_proxy.cpp | 6 ++++++ esphome/components/bluetooth_proxy/bluetooth_proxy.h | 3 ++- 6 files changed, 14 insertions(+), 2 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 6b19f2026a..9707e714e7 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -1712,6 +1712,7 @@ message BluetoothScannerStateResponse { BluetoothScannerState state = 1; BluetoothScannerMode mode = 2; + BluetoothScannerMode configured_mode = 3; } message BluetoothScannerSetModeRequest { diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 476e3c88d0..de60ed3fdb 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -2153,10 +2153,12 @@ void BluetoothDeviceClearCacheResponse::calculate_size(ProtoSize &size) const { void BluetoothScannerStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_uint32(1, static_cast(this->state)); buffer.encode_uint32(2, static_cast(this->mode)); + buffer.encode_uint32(3, static_cast(this->configured_mode)); } void BluetoothScannerStateResponse::calculate_size(ProtoSize &size) const { size.add_uint32(1, static_cast(this->state)); size.add_uint32(1, static_cast(this->mode)); + size.add_uint32(1, static_cast(this->configured_mode)); } bool BluetoothScannerSetModeRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index abdf0e6121..3f2c2ea763 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -2214,12 +2214,13 @@ class BluetoothDeviceClearCacheResponse final : public ProtoMessage { class BluetoothScannerStateResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 126; - static constexpr uint8_t ESTIMATED_SIZE = 4; + static constexpr uint8_t ESTIMATED_SIZE = 6; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "bluetooth_scanner_state_response"; } #endif enums::BluetoothScannerState state{}; enums::BluetoothScannerMode mode{}; + enums::BluetoothScannerMode configured_mode{}; void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index 7af322f96d..3e7df9195b 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -1704,6 +1704,7 @@ void BluetoothScannerStateResponse::dump_to(std::string &out) const { MessageDumpHelper helper(out, "BluetoothScannerStateResponse"); dump_field(out, "state", static_cast(this->state)); dump_field(out, "mode", static_cast(this->mode)); + dump_field(out, "configured_mode", static_cast(this->configured_mode)); } void BluetoothScannerSetModeRequest::dump_to(std::string &out) const { MessageDumpHelper helper(out, "BluetoothScannerSetModeRequest"); diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index 80b7fbe960..532aff550e 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -24,6 +24,9 @@ void BluetoothProxy::setup() { this->connections_free_response_.limit = BLUETOOTH_PROXY_MAX_CONNECTIONS; this->connections_free_response_.free = BLUETOOTH_PROXY_MAX_CONNECTIONS; + // Capture the configured scan mode from YAML before any API changes + this->configured_scan_active_ = this->parent_->get_scan_active(); + this->parent_->add_scanner_state_callback([this](esp32_ble_tracker::ScannerState state) { if (this->api_connection_ != nullptr) { this->send_bluetooth_scanner_state_(state); @@ -36,6 +39,9 @@ void BluetoothProxy::send_bluetooth_scanner_state_(esp32_ble_tracker::ScannerSta resp.state = static_cast(state); resp.mode = this->parent_->get_scan_active() ? api::enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_ACTIVE : api::enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_PASSIVE; + resp.configured_mode = this->configured_scan_active_ + ? api::enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_ACTIVE + : api::enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_PASSIVE; this->api_connection_->send_message(resp, api::BluetoothScannerStateResponse::MESSAGE_TYPE); } diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.h b/esphome/components/bluetooth_proxy/bluetooth_proxy.h index c81c8c9532..4b262dbe86 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.h +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.h @@ -161,7 +161,8 @@ class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener, publ // Group 4: 1-byte types grouped together bool active_; uint8_t connection_count_{0}; - // 2 bytes used, 2 bytes padding + bool configured_scan_active_{false}; // Configured scan mode from YAML + // 3 bytes used, 1 byte padding }; extern BluetoothProxy *global_bluetooth_proxy; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) From 77dbe7711752f4e273cd432cb2436a051c83a373 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Mon, 1 Sep 2025 19:30:02 +0200 Subject: [PATCH 112/208] [nrf52] fix missing bootloader (#10519) --- esphome/components/nrf52/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/nrf52/__init__.py b/esphome/components/nrf52/__init__.py index c388bb0f5e..a4e387b77a 100644 --- a/esphome/components/nrf52/__init__.py +++ b/esphome/components/nrf52/__init__.py @@ -101,6 +101,7 @@ DeviceFirmwareUpdate = nrf52_ns.class_("DeviceFirmwareUpdate", cg.Component) CONF_DFU = "dfu" CONFIG_SCHEMA = cv.All( + _detect_bootloader, set_core_data, cv.Schema( { @@ -114,7 +115,6 @@ CONFIG_SCHEMA = cv.All( ), } ), - _detect_bootloader, ) From d0b4bc48e465419a2070a83389113a9cc35dc736 Mon Sep 17 00:00:00 2001 From: Mischa Siekmann <45062894+gnumpi@users.noreply.github.com> Date: Mon, 1 Sep 2025 22:51:03 +0200 Subject: [PATCH 113/208] [wifi] Guard wifi error cases introduced in IDF5.2 by a version check (#10466) --- esphome/components/wifi/wifi_component_esp_idf.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index d465b346b3..31ee712a48 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -654,12 +654,14 @@ const char *get_disconnect_reason_str(uint8_t reason) { return "Association comeback time too long"; case WIFI_REASON_SA_QUERY_TIMEOUT: return "SA query timeout"; +#if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 2) case WIFI_REASON_NO_AP_FOUND_W_COMPATIBLE_SECURITY: return "No AP found with compatible security"; case WIFI_REASON_NO_AP_FOUND_IN_AUTHMODE_THRESHOLD: return "No AP found in auth mode threshold"; case WIFI_REASON_NO_AP_FOUND_IN_RSSI_THRESHOLD: return "No AP found in RSSI threshold"; +#endif case WIFI_REASON_UNSPECIFIED: default: return "Unspecified"; From 2ddd8c72d6ddab304e872f615cc876c629f27d15 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 2 Sep 2025 06:51:31 +1000 Subject: [PATCH 114/208] [mipi_dsi] Fix config for Guition screen (#10464) --- esphome/components/mipi_dsi/models/guition.py | 1 - 1 file changed, 1 deletion(-) diff --git a/esphome/components/mipi_dsi/models/guition.py b/esphome/components/mipi_dsi/models/guition.py index fd3fbf6160..5f7db4ebda 100644 --- a/esphome/components/mipi_dsi/models/guition.py +++ b/esphome/components/mipi_dsi/models/guition.py @@ -16,7 +16,6 @@ DriverChip( lane_bit_rate="750Mbps", swap_xy=cv.UNDEFINED, color_order="RGB", - reset_pin=27, initsequence=[ (0x30, 0x00), (0xF7, 0x49, 0x61, 0x02, 0x00), (0x30, 0x01), (0x04, 0x0C), (0x05, 0x00), (0x06, 0x00), (0x0B, 0x11), (0x17, 0x00), (0x20, 0x04), (0x1F, 0x05), (0x23, 0x00), (0x25, 0x19), (0x28, 0x18), (0x29, 0x04), (0x2A, 0x01), From ed48282d094b082ea6b8801e8976f7116346b986 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 2 Sep 2025 06:53:03 +1000 Subject: [PATCH 115/208] [mcp4461] Fix read transaction (#10465) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/mcp4461/mcp4461.cpp | 22 ++++++++++++++++++---- esphome/components/mcp4461/mcp4461.h | 1 + 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/esphome/components/mcp4461/mcp4461.cpp b/esphome/components/mcp4461/mcp4461.cpp index 191fbae366..2f2c75e05a 100644 --- a/esphome/components/mcp4461/mcp4461.cpp +++ b/esphome/components/mcp4461/mcp4461.cpp @@ -122,7 +122,7 @@ uint8_t Mcp4461Component::get_status_register_() { uint8_t addr = static_cast(Mcp4461Addresses::MCP4461_STATUS); uint8_t reg = addr | static_cast(Mcp4461Commands::READ); uint16_t buf; - if (!this->read_byte_16(reg, &buf)) { + if (!this->read_16_(reg, &buf)) { this->error_code_ = MCP4461_STATUS_REGISTER_ERROR; this->mark_failed(); return 0; @@ -148,6 +148,20 @@ void Mcp4461Component::read_status_register_to_log() { ((status_register_value >> 3) & 0x01), ((status_register_value >> 2) & 0x01), ((status_register_value >> 1) & 0x01), ((status_register_value >> 0) & 0x01)); } +bool Mcp4461Component::read_16_(uint8_t address, uint16_t *buf) { + // read 16 bits and convert from big endian to host, + // Do this as two separate operations to ensure a stop condition between the write and read + i2c::ErrorCode err = this->write(&address, 1); + if (err != i2c::ERROR_OK) { + return false; + } + err = this->read(reinterpret_cast(buf), 2); + if (err != i2c::ERROR_OK) { + return false; + } + *buf = convert_big_endian(*buf); + return true; +} uint8_t Mcp4461Component::get_wiper_address_(uint8_t wiper) { uint8_t addr; @@ -205,7 +219,7 @@ uint16_t Mcp4461Component::read_wiper_level_(uint8_t wiper_idx) { } } uint16_t buf = 0; - if (!(this->read_byte_16(reg, &buf))) { + if (!(this->read_16_(reg, &buf))) { this->error_code_ = MCP4461_STATUS_I2C_ERROR; this->status_set_warning(); ESP_LOGW(TAG, "Error fetching %swiper %u value", (wiper_idx > 3) ? "nonvolatile " : "", wiper_idx); @@ -392,7 +406,7 @@ uint8_t Mcp4461Component::get_terminal_register_(Mcp4461TerminalIdx terminal_con : static_cast(Mcp4461Addresses::MCP4461_TCON1); reg |= static_cast(Mcp4461Commands::READ); uint16_t buf; - if (this->read_byte_16(reg, &buf)) { + if (this->read_16_(reg, &buf)) { return static_cast(buf & 0x00ff); } else { this->error_code_ = MCP4461_STATUS_I2C_ERROR; @@ -517,7 +531,7 @@ uint16_t Mcp4461Component::get_eeprom_value(Mcp4461EepromLocation location) { if (!this->is_eeprom_ready_for_writing_(true)) { return 0; } - if (!this->read_byte_16(reg, &buf)) { + if (!this->read_16_(reg, &buf)) { this->error_code_ = MCP4461_STATUS_I2C_ERROR; this->status_set_warning(); ESP_LOGW(TAG, "Error fetching EEPROM location value"); diff --git a/esphome/components/mcp4461/mcp4461.h b/esphome/components/mcp4461/mcp4461.h index 9b7f60f201..59f6358a56 100644 --- a/esphome/components/mcp4461/mcp4461.h +++ b/esphome/components/mcp4461/mcp4461.h @@ -96,6 +96,7 @@ class Mcp4461Component : public Component, public i2c::I2CDevice { protected: friend class Mcp4461Wiper; + bool read_16_(uint8_t address, uint16_t *buf); void update_write_protection_status_(); uint8_t get_wiper_address_(uint8_t wiper); uint16_t read_wiper_level_(uint8_t wiper); From f286bc57f3fda499f270c37e4390c2189a3980ec Mon Sep 17 00:00:00 2001 From: Eyal <109809+eyal0@users.noreply.github.com> Date: Mon, 1 Sep 2025 22:45:25 -0600 Subject: [PATCH 116/208] [core] Fix timezone offset calculation (#10426) --- esphome/core/time.cpp | 26 ++++++-------------------- 1 file changed, 6 insertions(+), 20 deletions(-) diff --git a/esphome/core/time.cpp b/esphome/core/time.cpp index f9652b5329..fe6f50158c 100644 --- a/esphome/core/time.cpp +++ b/esphome/core/time.cpp @@ -203,27 +203,13 @@ void ESPTime::recalc_timestamp_local() { } int32_t ESPTime::timezone_offset() { - int32_t offset = 0; time_t now = ::time(nullptr); - auto local = ESPTime::from_epoch_local(now); - auto utc = ESPTime::from_epoch_utc(now); - bool negative = utc.hour > local.hour && local.day_of_year <= utc.day_of_year; - - if (utc.minute > local.minute) { - local.minute += 60; - local.hour -= 1; - } - offset += (local.minute - utc.minute) * 60; - - if (negative) { - offset -= (utc.hour - local.hour) * 3600; - } else { - if (utc.hour > local.hour) { - local.hour += 24; - } - offset += (local.hour - utc.hour) * 3600; - } - return offset; + struct tm local_tm = *::localtime(&now); + local_tm.tm_isdst = 0; // Cause mktime to ignore daylight saving time because we want to include it in the offset. + time_t local_time = mktime(&local_tm); + struct tm utc_tm = *::gmtime(&now); + time_t utc_time = mktime(&utc_tm); + return static_cast(local_time - utc_time); } bool ESPTime::operator<(const ESPTime &other) const { return this->timestamp < other.timestamp; } From d1276dc6df07f4af76cba228c063c3faea6c132b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 2 Sep 2025 16:41:50 -0500 Subject: [PATCH 117/208] [core] Replace magic coroutine priority numbers with self-documenting CoroPriority enum (#10518) --- .../alarm_control_panel/__init__.py | 4 +- esphome/components/api/__init__.py | 4 +- esphome/components/async_tcp/__init__.py | 4 +- esphome/components/audio_adc/__init__.py | 4 +- esphome/components/audio_dac/__init__.py | 4 +- esphome/components/binary_sensor/__init__.py | 4 +- esphome/components/button/__init__.py | 4 +- esphome/components/captive_portal/__init__.py | 4 +- esphome/components/climate/__init__.py | 4 +- esphome/components/cover/__init__.py | 4 +- esphome/components/datetime/__init__.py | 4 +- esphome/components/display/__init__.py | 4 +- .../components/esp32_ble_tracker/__init__.py | 4 +- esphome/components/esp8266/__init__.py | 4 +- esphome/components/esp8266/gpio.py | 4 +- esphome/components/esphome/ota/__init__.py | 4 +- esphome/components/ethernet/__init__.py | 9 +- esphome/components/event/__init__.py | 4 +- esphome/components/fan/__init__.py | 4 +- esphome/components/globals/__init__.py | 4 +- .../components/http_request/ota/__init__.py | 4 +- esphome/components/i2c/__init__.py | 4 +- esphome/components/json/__init__.py | 4 +- esphome/components/light/__init__.py | 4 +- esphome/components/lock/__init__.py | 4 +- esphome/components/logger/__init__.py | 4 +- esphome/components/mdns/__init__.py | 4 +- esphome/components/media_player/__init__.py | 4 +- esphome/components/microphone/__init__.py | 4 +- esphome/components/mqtt/__init__.py | 4 +- esphome/components/network/__init__.py | 4 +- esphome/components/nrf52/__init__.py | 6 +- esphome/components/number/__init__.py | 4 +- esphome/components/ota/__init__.py | 4 +- esphome/components/rp2040/__init__.py | 4 +- esphome/components/safe_mode/__init__.py | 4 +- esphome/components/select/__init__.py | 4 +- esphome/components/sensor/__init__.py | 4 +- esphome/components/speaker/__init__.py | 4 +- esphome/components/spi/__init__.py | 4 +- esphome/components/status_led/__init__.py | 4 +- esphome/components/stepper/__init__.py | 4 +- esphome/components/switch/__init__.py | 4 +- esphome/components/text/__init__.py | 4 +- esphome/components/text_sensor/__init__.py | 4 +- esphome/components/time/__init__.py | 4 +- esphome/components/touchscreen/__init__.py | 4 +- esphome/components/update/__init__.py | 4 +- esphome/components/valve/__init__.py | 4 +- esphome/components/web_server/__init__.py | 4 +- esphome/components/web_server/ota/__init__.py | 4 +- .../components/web_server_base/__init__.py | 4 +- esphome/components/wifi/__init__.py | 4 +- esphome/core/__init__.py | 1 + esphome/core/config.py | 14 +- esphome/coroutine.py | 83 ++++++- tests/unit_tests/test_coroutine.py | 204 ++++++++++++++++++ 57 files changed, 404 insertions(+), 117 deletions(-) create mode 100644 tests/unit_tests/test_coroutine.py diff --git a/esphome/components/alarm_control_panel/__init__.py b/esphome/components/alarm_control_panel/__init__.py index 058e061d1e..174a9d9e0a 100644 --- a/esphome/components/alarm_control_panel/__init__.py +++ b/esphome/components/alarm_control_panel/__init__.py @@ -13,7 +13,7 @@ from esphome.const import ( CONF_TRIGGER_ID, CONF_WEB_SERVER, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass @@ -345,6 +345,6 @@ async def alarm_control_panel_is_armed_to_code( return cg.new_Pvariable(condition_id, template_arg, paren) -@coroutine_with_priority(100.0) +@coroutine_with_priority(CoroPriority.CORE) async def to_code(config): cg.add_global(alarm_control_panel_ns.using) diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index 2672ea1edb..5fb84d3c21 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -24,7 +24,7 @@ from esphome.const import ( CONF_TRIGGER_ID, CONF_VARIABLES, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority DOMAIN = "api" DEPENDENCIES = ["network"] @@ -134,7 +134,7 @@ CONFIG_SCHEMA = cv.All( ) -@coroutine_with_priority(40.0) +@coroutine_with_priority(CoroPriority.WEB) async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) diff --git a/esphome/components/async_tcp/__init__.py b/esphome/components/async_tcp/__init__.py index 942d5bc8e5..f2d8895b39 100644 --- a/esphome/components/async_tcp/__init__.py +++ b/esphome/components/async_tcp/__init__.py @@ -8,7 +8,7 @@ from esphome.const import ( PLATFORM_LN882X, PLATFORM_RTL87XX, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority CODEOWNERS = ["@esphome/core"] @@ -27,7 +27,7 @@ CONFIG_SCHEMA = cv.All( ) -@coroutine_with_priority(200.0) +@coroutine_with_priority(CoroPriority.NETWORK_TRANSPORT) async def to_code(config): if CORE.is_esp32 or CORE.is_libretiny: # https://github.com/ESP32Async/AsyncTCP diff --git a/esphome/components/audio_adc/__init__.py b/esphome/components/audio_adc/__init__.py index dd3c958821..2f95a039f5 100644 --- a/esphome/components/audio_adc/__init__.py +++ b/esphome/components/audio_adc/__init__.py @@ -2,7 +2,7 @@ from esphome import automation import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_MIC_GAIN -from esphome.core import coroutine_with_priority +from esphome.core import CoroPriority, coroutine_with_priority CODEOWNERS = ["@kbx81"] IS_PLATFORM_COMPONENT = True @@ -35,7 +35,7 @@ async def audio_adc_set_mic_gain_to_code(config, action_id, template_arg, args): return var -@coroutine_with_priority(100.0) +@coroutine_with_priority(CoroPriority.CORE) async def to_code(config): cg.add_define("USE_AUDIO_ADC") cg.add_global(audio_adc_ns.using) diff --git a/esphome/components/audio_dac/__init__.py b/esphome/components/audio_dac/__init__.py index 978ed195bd..92e6cb18fa 100644 --- a/esphome/components/audio_dac/__init__.py +++ b/esphome/components/audio_dac/__init__.py @@ -3,7 +3,7 @@ from esphome.automation import maybe_simple_id import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_VOLUME -from esphome.core import coroutine_with_priority +from esphome.core import CoroPriority, coroutine_with_priority CODEOWNERS = ["@kbx81"] IS_PLATFORM_COMPONENT = True @@ -51,7 +51,7 @@ async def audio_dac_set_volume_to_code(config, action_id, template_arg, args): return var -@coroutine_with_priority(100.0) +@coroutine_with_priority(CoroPriority.CORE) async def to_code(config): cg.add_define("USE_AUDIO_DAC") cg.add_global(audio_dac_ns.using) diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index b56fde1ffd..6aa97d6e05 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -59,7 +59,7 @@ from esphome.const import ( DEVICE_CLASS_VIBRATION, DEVICE_CLASS_WINDOW, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass from esphome.util import Registry @@ -652,7 +652,7 @@ async def binary_sensor_is_off_to_code(config, condition_id, template_arg, args) return cg.new_Pvariable(condition_id, template_arg, paren, False) -@coroutine_with_priority(100.0) +@coroutine_with_priority(CoroPriority.CORE) async def to_code(config): cg.add_global(binary_sensor_ns.using) diff --git a/esphome/components/button/__init__.py b/esphome/components/button/__init__.py index a23958989e..e1ac875cb0 100644 --- a/esphome/components/button/__init__.py +++ b/esphome/components/button/__init__.py @@ -17,7 +17,7 @@ from esphome.const import ( DEVICE_CLASS_RESTART, DEVICE_CLASS_UPDATE, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass @@ -134,6 +134,6 @@ async def button_press_to_code(config, action_id, template_arg, args): return cg.new_Pvariable(action_id, template_arg, paren) -@coroutine_with_priority(100.0) +@coroutine_with_priority(CoroPriority.CORE) async def to_code(config): cg.add_global(button_ns.using) diff --git a/esphome/components/captive_portal/__init__.py b/esphome/components/captive_portal/__init__.py index cd69b67c78..39cafc7cb4 100644 --- a/esphome/components/captive_portal/__init__.py +++ b/esphome/components/captive_portal/__init__.py @@ -10,7 +10,7 @@ from esphome.const import ( PLATFORM_LN882X, PLATFORM_RTL87XX, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority AUTO_LOAD = ["web_server_base", "ota.web_server"] DEPENDENCIES = ["wifi"] @@ -40,7 +40,7 @@ CONFIG_SCHEMA = cv.All( ) -@coroutine_with_priority(64.0) +@coroutine_with_priority(CoroPriority.COMMUNICATION) async def to_code(config): paren = await cg.get_variable(config[CONF_WEB_SERVER_BASE_ID]) diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py index 4af3a619b5..c0c33d7242 100644 --- a/esphome/components/climate/__init__.py +++ b/esphome/components/climate/__init__.py @@ -47,7 +47,7 @@ from esphome.const import ( CONF_VISUAL, CONF_WEB_SERVER, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass @@ -517,6 +517,6 @@ async def climate_control_to_code(config, action_id, template_arg, args): return var -@coroutine_with_priority(100.0) +@coroutine_with_priority(CoroPriority.CORE) async def to_code(config): cg.add_global(climate_ns.using) diff --git a/esphome/components/cover/__init__.py b/esphome/components/cover/__init__.py index 383cfaf8fb..bec6dcbdac 100644 --- a/esphome/components/cover/__init__.py +++ b/esphome/components/cover/__init__.py @@ -32,7 +32,7 @@ from esphome.const import ( DEVICE_CLASS_SHUTTER, DEVICE_CLASS_WINDOW, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass @@ -263,6 +263,6 @@ async def cover_control_to_code(config, action_id, template_arg, args): return var -@coroutine_with_priority(100.0) +@coroutine_with_priority(CoroPriority.CORE) async def to_code(config): cg.add_global(cover_ns.using) diff --git a/esphome/components/datetime/__init__.py b/esphome/components/datetime/__init__.py index 1d84b75f26..602db3827a 100644 --- a/esphome/components/datetime/__init__.py +++ b/esphome/components/datetime/__init__.py @@ -21,7 +21,7 @@ from esphome.const import ( CONF_WEB_SERVER, CONF_YEAR, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass @@ -172,7 +172,7 @@ async def new_datetime(config, *args): return var -@coroutine_with_priority(100.0) +@coroutine_with_priority(CoroPriority.CORE) async def to_code(config): cg.add_global(datetime_ns.using) diff --git a/esphome/components/display/__init__.py b/esphome/components/display/__init__.py index e55afcebbf..ccbeedcd2f 100644 --- a/esphome/components/display/__init__.py +++ b/esphome/components/display/__init__.py @@ -15,7 +15,7 @@ from esphome.const import ( CONF_UPDATE_INTERVAL, SCHEDULER_DONT_RUN, ) -from esphome.core import coroutine_with_priority +from esphome.core import CoroPriority, coroutine_with_priority IS_PLATFORM_COMPONENT = True @@ -218,7 +218,7 @@ async def display_is_displaying_page_to_code(config, condition_id, template_arg, return var -@coroutine_with_priority(100.0) +@coroutine_with_priority(CoroPriority.CORE) async def to_code(config): cg.add_global(display_ns.using) cg.add_define("USE_DISPLAY") diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index 9ad2f3b25f..558143b007 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -30,7 +30,7 @@ from esphome.const import ( CONF_SERVICE_UUID, CONF_TRIGGER_ID, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.enum import StrEnum from esphome.types import ConfigType @@ -368,7 +368,7 @@ async def to_code(config): # This needs to be run as a job with very low priority so that all components have # chance to call register_ble_tracker and register_client before the list is checked # and added to the global defines list. -@coroutine_with_priority(-1000) +@coroutine_with_priority(CoroPriority.FINAL) async def _add_ble_features(): # Add feature-specific defines based on what's needed if BLEFeatures.ESP_BT_DEVICE in _required_features: diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index 33a4149571..b85314214e 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -17,7 +17,7 @@ from esphome.const import ( PLATFORM_ESP8266, ThreadModel, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.helpers import copy_file_if_changed from .boards import BOARDS, ESP8266_LD_SCRIPTS @@ -176,7 +176,7 @@ CONFIG_SCHEMA = cv.All( ) -@coroutine_with_priority(1000) +@coroutine_with_priority(CoroPriority.PLATFORM) async def to_code(config): cg.add(esp8266_ns.setup_preferences()) diff --git a/esphome/components/esp8266/gpio.py b/esphome/components/esp8266/gpio.py index 050efaacae..2bc2291117 100644 --- a/esphome/components/esp8266/gpio.py +++ b/esphome/components/esp8266/gpio.py @@ -17,7 +17,7 @@ from esphome.const import ( CONF_PULLUP, PLATFORM_ESP8266, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority from . import boards from .const import KEY_BOARD, KEY_ESP8266, KEY_PIN_INITIAL_STATES, esp8266_ns @@ -188,7 +188,7 @@ async def esp8266_pin_to_code(config): return var -@coroutine_with_priority(-999.0) +@coroutine_with_priority(CoroPriority.WORKAROUNDS) async def add_pin_initial_states_array(): # Add includes at the very end, so that they override everything initial_states: list[PinInitialState] = CORE.data[KEY_ESP8266][ diff --git a/esphome/components/esphome/ota/__init__.py b/esphome/components/esphome/ota/__init__.py index 9facdc3bc6..7b579501ed 100644 --- a/esphome/components/esphome/ota/__init__.py +++ b/esphome/components/esphome/ota/__init__.py @@ -16,7 +16,7 @@ from esphome.const import ( CONF_SAFE_MODE, CONF_VERSION, ) -from esphome.core import coroutine_with_priority +from esphome.core import CoroPriority, coroutine_with_priority import esphome.final_validate as fv _LOGGER = logging.getLogger(__name__) @@ -121,7 +121,7 @@ CONFIG_SCHEMA = ( FINAL_VALIDATE_SCHEMA = ota_esphome_final_validate -@coroutine_with_priority(52.0) +@coroutine_with_priority(CoroPriority.COMMUNICATION) async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) cg.add(var.set_port(config[CONF_PORT])) diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index 7a412a643d..a26238553c 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -38,7 +38,12 @@ from esphome.const import ( KEY_CORE, KEY_FRAMEWORK_VERSION, ) -from esphome.core import CORE, TimePeriodMilliseconds, coroutine_with_priority +from esphome.core import ( + CORE, + CoroPriority, + TimePeriodMilliseconds, + coroutine_with_priority, +) import esphome.final_validate as fv CONFLICTS_WITH = ["wifi"] @@ -289,7 +294,7 @@ def phy_register(address: int, value: int, page: int): ) -@coroutine_with_priority(60.0) +@coroutine_with_priority(CoroPriority.COMMUNICATION) async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) diff --git a/esphome/components/event/__init__.py b/esphome/components/event/__init__.py index 1948570ecd..449cc48625 100644 --- a/esphome/components/event/__init__.py +++ b/esphome/components/event/__init__.py @@ -17,7 +17,7 @@ from esphome.const import ( DEVICE_CLASS_EMPTY, DEVICE_CLASS_MOTION, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass @@ -143,6 +143,6 @@ async def event_fire_to_code(config, action_id, template_arg, args): return var -@coroutine_with_priority(100.0) +@coroutine_with_priority(CoroPriority.CORE) async def to_code(config): cg.add_global(event_ns.using) diff --git a/esphome/components/fan/__init__.py b/esphome/components/fan/__init__.py index 3fb217a24e..da8bf850c7 100644 --- a/esphome/components/fan/__init__.py +++ b/esphome/components/fan/__init__.py @@ -31,7 +31,7 @@ from esphome.const import ( CONF_TRIGGER_ID, CONF_WEB_SERVER, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity IS_PLATFORM_COMPONENT = True @@ -398,6 +398,6 @@ async def fan_is_on_off_to_code(config, condition_id, template_arg, args): return cg.new_Pvariable(condition_id, template_arg, paren) -@coroutine_with_priority(100.0) +@coroutine_with_priority(CoroPriority.CORE) async def to_code(config): cg.add_global(fan_ns.using) diff --git a/esphome/components/globals/__init__.py b/esphome/components/globals/__init__.py index e4bce99b0b..633ccea66b 100644 --- a/esphome/components/globals/__init__.py +++ b/esphome/components/globals/__init__.py @@ -8,7 +8,7 @@ from esphome.const import ( CONF_TYPE, CONF_VALUE, ) -from esphome.core import coroutine_with_priority +from esphome.core import CoroPriority, coroutine_with_priority CODEOWNERS = ["@esphome/core"] globals_ns = cg.esphome_ns.namespace("globals") @@ -35,7 +35,7 @@ CONFIG_SCHEMA = cv.Schema( # Run with low priority so that namespaces are registered first -@coroutine_with_priority(-100.0) +@coroutine_with_priority(CoroPriority.LATE) async def to_code(config): type_ = cg.RawExpression(config[CONF_TYPE]) restore = config[CONF_RESTORE_VALUE] diff --git a/esphome/components/http_request/ota/__init__.py b/esphome/components/http_request/ota/__init__.py index a3f6d5840c..fd542e594a 100644 --- a/esphome/components/http_request/ota/__init__.py +++ b/esphome/components/http_request/ota/__init__.py @@ -3,7 +3,7 @@ import esphome.codegen as cg from esphome.components.ota import BASE_OTA_SCHEMA, OTAComponent, ota_to_code import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_PASSWORD, CONF_URL, CONF_USERNAME -from esphome.core import coroutine_with_priority +from esphome.core import CoroPriority, coroutine_with_priority from .. import CONF_HTTP_REQUEST_ID, HttpRequestComponent, http_request_ns @@ -40,7 +40,7 @@ CONFIG_SCHEMA = cv.All( ) -@coroutine_with_priority(52.0) +@coroutine_with_priority(CoroPriority.COMMUNICATION) async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await ota_to_code(var, config) diff --git a/esphome/components/i2c/__init__.py b/esphome/components/i2c/__init__.py index 35b9fab9e4..3cfec1e94d 100644 --- a/esphome/components/i2c/__init__.py +++ b/esphome/components/i2c/__init__.py @@ -18,7 +18,7 @@ from esphome.const import ( PLATFORM_RP2040, PlatformFramework, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority import esphome.final_validate as fv LOGGER = logging.getLogger(__name__) @@ -74,7 +74,7 @@ CONFIG_SCHEMA = cv.All( ) -@coroutine_with_priority(1.0) +@coroutine_with_priority(CoroPriority.BUS) async def to_code(config): cg.add_global(i2c_ns.using) cg.add_define("USE_I2C") diff --git a/esphome/components/json/__init__.py b/esphome/components/json/__init__.py index 87aa823c0d..4cd737c60d 100644 --- a/esphome/components/json/__init__.py +++ b/esphome/components/json/__init__.py @@ -1,6 +1,6 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome.core import coroutine_with_priority +from esphome.core import CoroPriority, coroutine_with_priority CODEOWNERS = ["@esphome/core"] json_ns = cg.esphome_ns.namespace("json") @@ -10,7 +10,7 @@ CONFIG_SCHEMA = cv.All( ) -@coroutine_with_priority(1.0) +@coroutine_with_priority(CoroPriority.BUS) async def to_code(config): cg.add_library("bblanchon/ArduinoJson", "7.4.2") cg.add_define("USE_JSON") diff --git a/esphome/components/light/__init__.py b/esphome/components/light/__init__.py index fa39721ee2..f1089ad64f 100644 --- a/esphome/components/light/__init__.py +++ b/esphome/components/light/__init__.py @@ -37,7 +37,7 @@ from esphome.const import ( CONF_WEB_SERVER, CONF_WHITE, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass @@ -283,6 +283,6 @@ async def new_light(config, *args): return output_var -@coroutine_with_priority(100.0) +@coroutine_with_priority(CoroPriority.CORE) async def to_code(config): cg.add_global(light_ns.using) diff --git a/esphome/components/lock/__init__.py b/esphome/components/lock/__init__.py index 7977efd264..04c1586ddd 100644 --- a/esphome/components/lock/__init__.py +++ b/esphome/components/lock/__init__.py @@ -13,7 +13,7 @@ from esphome.const import ( CONF_TRIGGER_ID, CONF_WEB_SERVER, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass @@ -155,6 +155,6 @@ async def lock_is_off_to_code(config, condition_id, template_arg, args): return cg.new_Pvariable(condition_id, template_arg, paren, False) -@coroutine_with_priority(100.0) +@coroutine_with_priority(CoroPriority.CORE) async def to_code(config): cg.add_global(lock_ns.using) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index d8c95d75f2..2865355278 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -51,7 +51,7 @@ from esphome.const import ( PLATFORM_RTL87XX, PlatformFramework, ) -from esphome.core import CORE, Lambda, coroutine_with_priority +from esphome.core import CORE, CoroPriority, Lambda, coroutine_with_priority CODEOWNERS = ["@esphome/core"] logger_ns = cg.esphome_ns.namespace("logger") @@ -275,7 +275,7 @@ CONFIG_SCHEMA = cv.All( ) -@coroutine_with_priority(90.0) +@coroutine_with_priority(CoroPriority.DIAGNOSTICS) async def to_code(config): baud_rate = config[CONF_BAUD_RATE] level = config[CONF_LEVEL] diff --git a/esphome/components/mdns/__init__.py b/esphome/components/mdns/__init__.py index 469fe8ada6..a21ef9d97b 100644 --- a/esphome/components/mdns/__init__.py +++ b/esphome/components/mdns/__init__.py @@ -11,7 +11,7 @@ from esphome.const import ( CONF_SERVICES, PlatformFramework, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority CODEOWNERS = ["@esphome/core"] DEPENDENCIES = ["network"] @@ -72,7 +72,7 @@ def mdns_service( ) -@coroutine_with_priority(55.0) +@coroutine_with_priority(CoroPriority.COMMUNICATION) async def to_code(config): if config[CONF_DISABLED] is True: return diff --git a/esphome/components/media_player/__init__.py b/esphome/components/media_player/__init__.py index d288e70cba..70c7cf7a56 100644 --- a/esphome/components/media_player/__init__.py +++ b/esphome/components/media_player/__init__.py @@ -14,7 +14,7 @@ from esphome.const import ( ) from esphome.core import CORE from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity -from esphome.coroutine import coroutine_with_priority +from esphome.coroutine import CoroPriority, coroutine_with_priority from esphome.cpp_generator import MockObjClass CODEOWNERS = ["@jesserockz"] @@ -303,7 +303,7 @@ async def media_player_volume_set_action(config, action_id, template_arg, args): return var -@coroutine_with_priority(100.0) +@coroutine_with_priority(CoroPriority.CORE) async def to_code(config): cg.add_global(media_player_ns.using) cg.add_define("USE_MEDIA_PLAYER") diff --git a/esphome/components/microphone/__init__.py b/esphome/components/microphone/__init__.py index 29bdcfa3f3..1fc0df88a3 100644 --- a/esphome/components/microphone/__init__.py +++ b/esphome/components/microphone/__init__.py @@ -12,7 +12,7 @@ from esphome.const import ( CONF_TRIGGER_ID, ) from esphome.core import CORE -from esphome.coroutine import coroutine_with_priority +from esphome.coroutine import CoroPriority, coroutine_with_priority AUTO_LOAD = ["audio"] CODEOWNERS = ["@jesserockz", "@kahrendt"] @@ -213,7 +213,7 @@ automation.register_condition( )(microphone_action) -@coroutine_with_priority(100.0) +@coroutine_with_priority(CoroPriority.CORE) async def to_code(config): cg.add_global(microphone_ns.using) cg.add_define("USE_MICROPHONE") diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index 52d3181780..814fb566d4 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -57,7 +57,7 @@ from esphome.const import ( PLATFORM_ESP8266, PlatformFramework, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority DEPENDENCIES = ["network"] @@ -321,7 +321,7 @@ def exp_mqtt_message(config): ) -@coroutine_with_priority(40.0) +@coroutine_with_priority(CoroPriority.WEB) async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) diff --git a/esphome/components/network/__init__.py b/esphome/components/network/__init__.py index b04fca7a1c..9679961b15 100644 --- a/esphome/components/network/__init__.py +++ b/esphome/components/network/__init__.py @@ -2,7 +2,7 @@ import esphome.codegen as cg from esphome.components.esp32 import add_idf_sdkconfig_option import esphome.config_validation as cv from esphome.const import CONF_ENABLE_IPV6, CONF_MIN_IPV6_ADDR_COUNT -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority CODEOWNERS = ["@esphome/core"] AUTO_LOAD = ["mdns"] @@ -36,7 +36,7 @@ CONFIG_SCHEMA = cv.Schema( ) -@coroutine_with_priority(201.0) +@coroutine_with_priority(CoroPriority.NETWORK) async def to_code(config): cg.add_define("USE_NETWORK") if CORE.using_arduino and CORE.is_esp32: diff --git a/esphome/components/nrf52/__init__.py b/esphome/components/nrf52/__init__.py index a4e387b77a..84e505a90a 100644 --- a/esphome/components/nrf52/__init__.py +++ b/esphome/components/nrf52/__init__.py @@ -30,7 +30,7 @@ from esphome.const import ( PLATFORM_NRF52, ThreadModel, ) -from esphome.core import CORE, EsphomeError, coroutine_with_priority +from esphome.core import CORE, CoroPriority, EsphomeError, coroutine_with_priority from esphome.storage_json import StorageJSON from esphome.types import ConfigType @@ -132,7 +132,7 @@ def _final_validate(config): FINAL_VALIDATE_SCHEMA = _final_validate -@coroutine_with_priority(1000) +@coroutine_with_priority(CoroPriority.PLATFORM) async def to_code(config: ConfigType) -> None: """Convert the configuration to code.""" cg.add_platformio_option("board", config[CONF_BOARD]) @@ -170,7 +170,7 @@ async def to_code(config: ConfigType) -> None: CORE.add_job(_dfu_to_code, dfu_config) -@coroutine_with_priority(90) +@coroutine_with_priority(CoroPriority.DIAGNOSTICS) async def _dfu_to_code(dfu_config): cg.add_define("USE_NRF52_DFU") var = cg.new_Pvariable(dfu_config[CONF_ID]) diff --git a/esphome/components/number/__init__.py b/esphome/components/number/__init__.py index 4a83d5fc5f..c2cad2f7f1 100644 --- a/esphome/components/number/__init__.py +++ b/esphome/components/number/__init__.py @@ -76,7 +76,7 @@ from esphome.const import ( DEVICE_CLASS_WIND_DIRECTION, DEVICE_CLASS_WIND_SPEED, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass @@ -321,7 +321,7 @@ async def number_in_range_to_code(config, condition_id, template_arg, args): return var -@coroutine_with_priority(100.0) +@coroutine_with_priority(CoroPriority.CORE) async def to_code(config): cg.add_global(number_ns.using) diff --git a/esphome/components/ota/__init__.py b/esphome/components/ota/__init__.py index 4d5b8a61e2..cf814fb1ee 100644 --- a/esphome/components/ota/__init__.py +++ b/esphome/components/ota/__init__.py @@ -10,7 +10,7 @@ from esphome.const import ( CONF_TRIGGER_ID, PlatformFramework, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority CODEOWNERS = ["@esphome/core"] AUTO_LOAD = ["md5", "safe_mode"] @@ -82,7 +82,7 @@ BASE_OTA_SCHEMA = cv.Schema( ) -@coroutine_with_priority(54.0) +@coroutine_with_priority(CoroPriority.COMMUNICATION) async def to_code(config): cg.add_define("USE_OTA") diff --git a/esphome/components/rp2040/__init__.py b/esphome/components/rp2040/__init__.py index 46eabb5325..1ec38e0159 100644 --- a/esphome/components/rp2040/__init__.py +++ b/esphome/components/rp2040/__init__.py @@ -18,7 +18,7 @@ from esphome.const import ( PLATFORM_RP2040, ThreadModel, ) -from esphome.core import CORE, EsphomeError, coroutine_with_priority +from esphome.core import CORE, CoroPriority, EsphomeError, coroutine_with_priority from esphome.helpers import copy_file_if_changed, mkdir_p, read_file, write_file from .const import KEY_BOARD, KEY_PIO_FILES, KEY_RP2040, rp2040_ns @@ -159,7 +159,7 @@ CONFIG_SCHEMA = cv.All( ) -@coroutine_with_priority(1000) +@coroutine_with_priority(CoroPriority.PLATFORM) async def to_code(config): cg.add(rp2040_ns.setup_preferences()) diff --git a/esphome/components/safe_mode/__init__.py b/esphome/components/safe_mode/__init__.py index 991747b089..9944d71722 100644 --- a/esphome/components/safe_mode/__init__.py +++ b/esphome/components/safe_mode/__init__.py @@ -10,7 +10,7 @@ from esphome.const import ( CONF_TRIGGER_ID, KEY_PAST_SAFE_MODE, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.cpp_generator import RawExpression CODEOWNERS = ["@paulmonigatti", "@jsuanet", "@kbx81"] @@ -53,7 +53,7 @@ CONFIG_SCHEMA = cv.All( ) -@coroutine_with_priority(50.0) +@coroutine_with_priority(CoroPriority.APPLICATION) async def to_code(config): if not config[CONF_DISABLED]: var = cg.new_Pvariable(config[CONF_ID]) diff --git a/esphome/components/select/__init__.py b/esphome/components/select/__init__.py index 756e98c906..c7146df9fb 100644 --- a/esphome/components/select/__init__.py +++ b/esphome/components/select/__init__.py @@ -16,7 +16,7 @@ from esphome.const import ( CONF_TRIGGER_ID, CONF_WEB_SERVER, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass @@ -124,7 +124,7 @@ async def new_select(config, *args, options: list[str]): return var -@coroutine_with_priority(100.0) +@coroutine_with_priority(CoroPriority.CORE) async def to_code(config): cg.add_global(select_ns.using) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 277718e46c..fe9822b3ca 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -101,7 +101,7 @@ from esphome.const import ( DEVICE_CLASS_WIND_SPEED, ENTITY_CATEGORY_CONFIG, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass from esphome.util import Registry @@ -1142,6 +1142,6 @@ def _lstsq(a, b): return _mat_dot(_mat_dot(x, a_t), b) -@coroutine_with_priority(100.0) +@coroutine_with_priority(CoroPriority.CORE) async def to_code(config): cg.add_global(sensor_ns.using) diff --git a/esphome/components/speaker/__init__.py b/esphome/components/speaker/__init__.py index 2ac1ca0cb9..5f1ba94ee6 100644 --- a/esphome/components/speaker/__init__.py +++ b/esphome/components/speaker/__init__.py @@ -4,7 +4,7 @@ from esphome.components import audio, audio_dac import esphome.config_validation as cv from esphome.const import CONF_DATA, CONF_ID, CONF_VOLUME from esphome.core import CORE -from esphome.coroutine import coroutine_with_priority +from esphome.coroutine import CoroPriority, coroutine_with_priority AUTO_LOAD = ["audio"] CODEOWNERS = ["@jesserockz", "@kahrendt"] @@ -138,7 +138,7 @@ async def speaker_mute_action_to_code(config, action_id, template_arg, args): return cg.new_Pvariable(action_id, template_arg, paren) -@coroutine_with_priority(100.0) +@coroutine_with_priority(CoroPriority.CORE) async def to_code(config): cg.add_global(speaker_ns.using) cg.add_define("USE_SPEAKER") diff --git a/esphome/components/spi/__init__.py b/esphome/components/spi/__init__.py index a436bc6dab..894c6d1878 100644 --- a/esphome/components/spi/__init__.py +++ b/esphome/components/spi/__init__.py @@ -35,7 +35,7 @@ from esphome.const import ( PLATFORM_RP2040, PlatformFramework, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority import esphome.final_validate as fv CODEOWNERS = ["@esphome/core", "@clydebarrow"] @@ -351,7 +351,7 @@ CONFIG_SCHEMA = cv.All( ) -@coroutine_with_priority(1.0) +@coroutine_with_priority(CoroPriority.BUS) async def to_code(configs): cg.add_define("USE_SPI") cg.add_global(spi_ns.using) diff --git a/esphome/components/status_led/__init__.py b/esphome/components/status_led/__init__.py index b299ae7ff7..b0fce37126 100644 --- a/esphome/components/status_led/__init__.py +++ b/esphome/components/status_led/__init__.py @@ -2,7 +2,7 @@ from esphome import pins import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_PIN -from esphome.core import coroutine_with_priority +from esphome.core import CoroPriority, coroutine_with_priority status_led_ns = cg.esphome_ns.namespace("status_led") StatusLED = status_led_ns.class_("StatusLED", cg.Component) @@ -15,7 +15,7 @@ CONFIG_SCHEMA = cv.Schema( ).extend(cv.COMPONENT_SCHEMA) -@coroutine_with_priority(80.0) +@coroutine_with_priority(CoroPriority.STATUS) async def to_code(config): pin = await cg.gpio_pin_expression(config[CONF_PIN]) rhs = StatusLED.new(pin) diff --git a/esphome/components/stepper/__init__.py b/esphome/components/stepper/__init__.py index c234388e7e..62bc71f2d1 100644 --- a/esphome/components/stepper/__init__.py +++ b/esphome/components/stepper/__init__.py @@ -10,7 +10,7 @@ from esphome.const import ( CONF_SPEED, CONF_TARGET, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority IS_PLATFORM_COMPONENT = True @@ -178,6 +178,6 @@ async def stepper_set_deceleration_to_code(config, action_id, template_arg, args return var -@coroutine_with_priority(100.0) +@coroutine_with_priority(CoroPriority.CORE) async def to_code(config): cg.add_global(stepper_ns.using) diff --git a/esphome/components/switch/__init__.py b/esphome/components/switch/__init__.py index f495dbc0b4..0e7b35b373 100644 --- a/esphome/components/switch/__init__.py +++ b/esphome/components/switch/__init__.py @@ -21,7 +21,7 @@ from esphome.const import ( DEVICE_CLASS_OUTLET, DEVICE_CLASS_SWITCH, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass @@ -230,6 +230,6 @@ async def switch_is_off_to_code(config, condition_id, template_arg, args): return cg.new_Pvariable(condition_id, template_arg, paren, False) -@coroutine_with_priority(100.0) +@coroutine_with_priority(CoroPriority.CORE) async def to_code(config): cg.add_global(switch_ns.using) diff --git a/esphome/components/text/__init__.py b/esphome/components/text/__init__.py index aa831d1f06..1baacc239f 100644 --- a/esphome/components/text/__init__.py +++ b/esphome/components/text/__init__.py @@ -13,7 +13,7 @@ from esphome.const import ( CONF_VALUE, CONF_WEB_SERVER, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass @@ -149,7 +149,7 @@ async def new_text( return var -@coroutine_with_priority(100.0) +@coroutine_with_priority(CoroPriority.CORE) async def to_code(config): cg.add_global(text_ns.using) diff --git a/esphome/components/text_sensor/__init__.py b/esphome/components/text_sensor/__init__.py index e4aa701a7b..f7b3b5c55e 100644 --- a/esphome/components/text_sensor/__init__.py +++ b/esphome/components/text_sensor/__init__.py @@ -20,7 +20,7 @@ from esphome.const import ( DEVICE_CLASS_EMPTY, DEVICE_CLASS_TIMESTAMP, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass from esphome.util import Registry @@ -230,7 +230,7 @@ async def new_text_sensor(config, *args): return var -@coroutine_with_priority(100.0) +@coroutine_with_priority(CoroPriority.CORE) async def to_code(config): cg.add_global(text_sensor_ns.using) diff --git a/esphome/components/time/__init__.py b/esphome/components/time/__init__.py index a38ad4eae3..a20d79b857 100644 --- a/esphome/components/time/__init__.py +++ b/esphome/components/time/__init__.py @@ -26,7 +26,7 @@ from esphome.const import ( CONF_TIMEZONE, CONF_TRIGGER_ID, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority _LOGGER = logging.getLogger(__name__) @@ -340,7 +340,7 @@ async def register_time(time_var, config): await setup_time_core_(time_var, config) -@coroutine_with_priority(100.0) +@coroutine_with_priority(CoroPriority.CORE) async def to_code(config): if CORE.using_zephyr: zephyr_add_prj_conf("POSIX_CLOCK", True) diff --git a/esphome/components/touchscreen/__init__.py b/esphome/components/touchscreen/__init__.py index 01a271a34e..4a5c03ace4 100644 --- a/esphome/components/touchscreen/__init__.py +++ b/esphome/components/touchscreen/__init__.py @@ -13,7 +13,7 @@ from esphome.const import ( CONF_SWAP_XY, CONF_TRANSFORM, ) -from esphome.core import coroutine_with_priority +from esphome.core import CoroPriority, coroutine_with_priority CODEOWNERS = ["@jesserockz", "@nielsnl68"] DEPENDENCIES = ["display"] @@ -152,7 +152,7 @@ async def register_touchscreen(var, config): ) -@coroutine_with_priority(100.0) +@coroutine_with_priority(CoroPriority.CORE) async def to_code(config): cg.add_global(touchscreen_ns.using) cg.add_define("USE_TOUCHSCREEN") diff --git a/esphome/components/update/__init__.py b/esphome/components/update/__init__.py index 50d8aaf139..35fc4eaf1d 100644 --- a/esphome/components/update/__init__.py +++ b/esphome/components/update/__init__.py @@ -14,7 +14,7 @@ from esphome.const import ( DEVICE_CLASS_FIRMWARE, ENTITY_CATEGORY_CONFIG, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass @@ -124,7 +124,7 @@ async def new_update(config): return var -@coroutine_with_priority(100.0) +@coroutine_with_priority(CoroPriority.CORE) async def to_code(config): cg.add_global(update_ns.using) diff --git a/esphome/components/valve/__init__.py b/esphome/components/valve/__init__.py index 8185bd6ea2..6f31fc3a20 100644 --- a/esphome/components/valve/__init__.py +++ b/esphome/components/valve/__init__.py @@ -21,7 +21,7 @@ from esphome.const import ( DEVICE_CLASS_GAS, DEVICE_CLASS_WATER, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass @@ -233,6 +233,6 @@ async def valve_control_to_code(config, action_id, template_arg, args): return var -@coroutine_with_priority(100.0) +@coroutine_with_priority(CoroPriority.CORE) async def to_code(config): cg.add_global(valve_ns.using) diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index be193bbab8..288d928e80 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -31,7 +31,7 @@ from esphome.const import ( PLATFORM_LN882X, PLATFORM_RTL87XX, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority import esphome.final_validate as fv from esphome.types import ConfigType @@ -269,7 +269,7 @@ def add_resource_as_progmem( cg.add_global(cg.RawExpression(size_t)) -@coroutine_with_priority(40.0) +@coroutine_with_priority(CoroPriority.WEB) async def to_code(config): paren = await cg.get_variable(config[CONF_WEB_SERVER_BASE_ID]) diff --git a/esphome/components/web_server/ota/__init__.py b/esphome/components/web_server/ota/__init__.py index 3af14fd453..3ec7e65e1d 100644 --- a/esphome/components/web_server/ota/__init__.py +++ b/esphome/components/web_server/ota/__init__.py @@ -3,7 +3,7 @@ from esphome.components.esp32 import add_idf_component from esphome.components.ota import BASE_OTA_SCHEMA, OTAComponent, ota_to_code import esphome.config_validation as cv from esphome.const import CONF_ID -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority CODEOWNERS = ["@esphome/core"] DEPENDENCIES = ["network", "web_server_base"] @@ -22,7 +22,7 @@ CONFIG_SCHEMA = ( ) -@coroutine_with_priority(52.0) +@coroutine_with_priority(CoroPriority.COMMUNICATION) async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await ota_to_code(var, config) diff --git a/esphome/components/web_server_base/__init__.py b/esphome/components/web_server_base/__init__.py index 50ae6b92fa..8add2f051f 100644 --- a/esphome/components/web_server_base/__init__.py +++ b/esphome/components/web_server_base/__init__.py @@ -1,7 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import CONF_ID -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority CODEOWNERS = ["@esphome/core"] DEPENDENCIES = ["network"] @@ -26,7 +26,7 @@ CONFIG_SCHEMA = cv.Schema( ) -@coroutine_with_priority(65.0) +@coroutine_with_priority(CoroPriority.COMMUNICATION) async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 4013e8f400..7943911021 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -44,7 +44,7 @@ from esphome.const import ( CONF_USERNAME, PlatformFramework, ) -from esphome.core import CORE, HexInt, coroutine_with_priority +from esphome.core import CORE, CoroPriority, HexInt, coroutine_with_priority import esphome.final_validate as fv from . import wpa2_eap @@ -370,7 +370,7 @@ def wifi_network(config, ap, static_ip): return ap -@coroutine_with_priority(60.0) +@coroutine_with_priority(CoroPriority.COMMUNICATION) async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) cg.add(var.set_use_address(config[CONF_USE_ADDRESS])) diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index 8a9630735e..89e3eff7d8 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -29,6 +29,7 @@ from esphome.const import ( # pylint: disable=unused-import from esphome.coroutine import ( # noqa: F401 + CoroPriority, FakeAwaitable as _FakeAwaitable, FakeEventLoop as _FakeEventLoop, coroutine, diff --git a/esphome/core/config.py b/esphome/core/config.py index b6ff1d8afd..96b9e23861 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -39,7 +39,7 @@ from esphome.const import ( PlatformFramework, __version__ as ESPHOME_VERSION, ) -from esphome.core import CORE, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.helpers import ( copy_file_if_changed, fnv1a_32bit_hash, @@ -359,7 +359,7 @@ ARDUINO_GLUE_CODE = """\ """ -@coroutine_with_priority(-999.0) +@coroutine_with_priority(CoroPriority.WORKAROUNDS) async def add_arduino_global_workaround(): # The Arduino framework defined these itself in the global # namespace. For the esphome codebase that is not a problem, @@ -376,7 +376,7 @@ async def add_arduino_global_workaround(): cg.add_global(cg.RawStatement(line)) -@coroutine_with_priority(-1000.0) +@coroutine_with_priority(CoroPriority.FINAL) async def add_includes(includes): # Add includes at the very end, so that the included files can access global variables for include in includes: @@ -392,7 +392,7 @@ async def add_includes(includes): include_file(path, basename) -@coroutine_with_priority(-1000.0) +@coroutine_with_priority(CoroPriority.FINAL) async def _add_platformio_options(pio_options): # Add includes at the very end, so that they override everything for key, val in pio_options.items(): @@ -401,7 +401,7 @@ async def _add_platformio_options(pio_options): cg.add_platformio_option(key, val) -@coroutine_with_priority(30.0) +@coroutine_with_priority(CoroPriority.AUTOMATION) async def _add_automations(config): for conf in config.get(CONF_ON_BOOT, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], conf.get(CONF_PRIORITY)) @@ -423,7 +423,7 @@ async def _add_automations(config): DATETIME_SUBTYPES = {"date", "time", "datetime"} -@coroutine_with_priority(-1000.0) +@coroutine_with_priority(CoroPriority.FINAL) async def _add_platform_defines() -> None: # Generate compile-time defines for platforms that have actual entities # Only add USE_* and count defines when there are entities @@ -442,7 +442,7 @@ async def _add_platform_defines() -> None: cg.add_define(f"USE_{platform_name.upper()}") -@coroutine_with_priority(100.0) +@coroutine_with_priority(CoroPriority.CORE) async def to_code(config: ConfigType) -> None: cg.add_global(cg.global_ns.namespace("esphome").using) # These can be used by user lambdas, put them to default scope diff --git a/esphome/coroutine.py b/esphome/coroutine.py index 8d952246f3..741a0c7c0c 100644 --- a/esphome/coroutine.py +++ b/esphome/coroutine.py @@ -42,7 +42,10 @@ Here everything is combined in `yield` expressions. You await other coroutines u the last `yield` expression defines what is returned. """ +from __future__ import annotations + from collections.abc import Awaitable, Callable, Generator, Iterator +import enum import functools import heapq import inspect @@ -53,6 +56,79 @@ from typing import Any _LOGGER = logging.getLogger(__name__) +class CoroPriority(enum.IntEnum): + """Execution priority stages for ESPHome code generation. + + Higher values run first. These stages ensure proper dependency + resolution during code generation. + """ + + # Platform initialization - must run first + # Examples: esp32, esp8266, rp2040 + PLATFORM = 1000 + + # Network infrastructure setup + # Examples: network (201) + NETWORK = 201 + + # Network transport layer + # Examples: async_tcp (200) + NETWORK_TRANSPORT = 200 + + # Core system components + # Examples: esphome core, most entity base components (cover, update, datetime, + # valve, alarm_control_panel, lock, event, binary_sensor, button, climate, fan, + # light, media_player, number, select, sensor, switch, text_sensor, text), + # microphone, speaker, audio_dac, touchscreen, stepper + CORE = 100 + + # Diagnostic and debugging systems + # Examples: logger (90) + DIAGNOSTICS = 90 + + # Status and monitoring systems + # Examples: status_led (80) + STATUS = 80 + + # Communication protocols and services + # Examples: web_server_base (65), captive_portal (64), wifi (60), ethernet (60), + # mdns (55), ota_updates (54), web_server_ota (52) + COMMUNICATION = 60 + + # Application-level services + # Examples: safe_mode (50) + APPLICATION = 50 + + # Web and UI services + # Examples: web_server (40) + WEB = 40 + + # Automations and user logic + # Examples: esphome core automations (30) + AUTOMATION = 30 + + # Bus and peripheral setup + # Examples: i2c (1) + BUS = 1 + + # Standard component priority (default) + # Components without explicit priority run at 0 + COMPONENT = 0 + + # Components that need others to be registered first + # Examples: globals (-100) + LATE = -100 + + # Platform-specific workarounds and fixes + # Examples: add_arduino_global_workaround (-999), esp8266 pin states (-999) + WORKAROUNDS = -999 + + # Final setup that requires all components to be registered + # Examples: add_includes, _add_platformio_options, _add_platform_defines (all -1000), + # esp32_ble_tracker feature defines (-1000) + FINAL = -1000 + + def coroutine(func: Callable[..., Any]) -> Callable[..., Awaitable[Any]]: """Decorator to apply to methods to convert them to ESPHome coroutines.""" if getattr(func, "_esphome_coroutine", False): @@ -95,15 +171,16 @@ def coroutine(func: Callable[..., Any]) -> Callable[..., Awaitable[Any]]: return coro -def coroutine_with_priority(priority: float): +def coroutine_with_priority(priority: float | CoroPriority): """Decorator to apply to functions to convert them to ESPHome coroutines. :param priority: priority with which to schedule the coroutine, higher priorities run first. + Can be a float or a CoroPriority enum value. """ def decorator(func): coro = coroutine(func) - coro.priority = priority + coro.priority = float(priority) return coro return decorator @@ -173,7 +250,7 @@ class _Task: self.iterator = iterator self.original_function = original_function - def with_priority(self, priority: float) -> "_Task": + def with_priority(self, priority: float) -> _Task: return _Task(priority, self.id_number, self.iterator, self.original_function) @property diff --git a/tests/unit_tests/test_coroutine.py b/tests/unit_tests/test_coroutine.py new file mode 100644 index 0000000000..138b08edb5 --- /dev/null +++ b/tests/unit_tests/test_coroutine.py @@ -0,0 +1,204 @@ +"""Tests for the coroutine module.""" + +import pytest + +from esphome.coroutine import CoroPriority, FakeEventLoop, coroutine_with_priority + + +def test_coro_priority_enum_values() -> None: + """Test that CoroPriority enum values match expected priorities.""" + assert CoroPriority.PLATFORM == 1000 + assert CoroPriority.NETWORK == 201 + assert CoroPriority.NETWORK_TRANSPORT == 200 + assert CoroPriority.CORE == 100 + assert CoroPriority.DIAGNOSTICS == 90 + assert CoroPriority.STATUS == 80 + assert CoroPriority.COMMUNICATION == 60 + assert CoroPriority.APPLICATION == 50 + assert CoroPriority.WEB == 40 + assert CoroPriority.AUTOMATION == 30 + assert CoroPriority.BUS == 1 + assert CoroPriority.COMPONENT == 0 + assert CoroPriority.LATE == -100 + assert CoroPriority.WORKAROUNDS == -999 + assert CoroPriority.FINAL == -1000 + + +def test_coroutine_with_priority_accepts_float() -> None: + """Test that coroutine_with_priority accepts float values.""" + + @coroutine_with_priority(100.0) + def test_func() -> None: + pass + + assert hasattr(test_func, "priority") + assert test_func.priority == 100.0 + + +def test_coroutine_with_priority_accepts_enum() -> None: + """Test that coroutine_with_priority accepts CoroPriority enum values.""" + + @coroutine_with_priority(CoroPriority.CORE) + def test_func() -> None: + pass + + assert hasattr(test_func, "priority") + assert test_func.priority == 100.0 + + +def test_float_and_enum_are_interchangeable() -> None: + """Test that float and CoroPriority enum values produce the same priority.""" + + @coroutine_with_priority(100.0) + def func_with_float() -> None: + pass + + @coroutine_with_priority(CoroPriority.CORE) + def func_with_enum() -> None: + pass + + assert func_with_float.priority == func_with_enum.priority + assert func_with_float.priority == 100.0 + + +@pytest.mark.parametrize( + ("enum_value", "float_value"), + [ + (CoroPriority.PLATFORM, 1000.0), + (CoroPriority.NETWORK, 201.0), + (CoroPriority.NETWORK_TRANSPORT, 200.0), + (CoroPriority.CORE, 100.0), + (CoroPriority.DIAGNOSTICS, 90.0), + (CoroPriority.STATUS, 80.0), + (CoroPriority.COMMUNICATION, 60.0), + (CoroPriority.APPLICATION, 50.0), + (CoroPriority.WEB, 40.0), + (CoroPriority.AUTOMATION, 30.0), + (CoroPriority.BUS, 1.0), + (CoroPriority.COMPONENT, 0.0), + (CoroPriority.LATE, -100.0), + (CoroPriority.WORKAROUNDS, -999.0), + (CoroPriority.FINAL, -1000.0), + ], +) +def test_all_priority_values_are_interchangeable( + enum_value: CoroPriority, float_value: float +) -> None: + """Test that all CoroPriority values work correctly with coroutine_with_priority.""" + + @coroutine_with_priority(enum_value) + def func_with_enum() -> None: + pass + + @coroutine_with_priority(float_value) + def func_with_float() -> None: + pass + + assert func_with_enum.priority == float_value + assert func_with_float.priority == float_value + assert func_with_enum.priority == func_with_float.priority + + +def test_execution_order_with_enum_priorities() -> None: + """Test that execution order is correct when using enum priorities.""" + execution_order: list[str] = [] + + @coroutine_with_priority(CoroPriority.PLATFORM) + async def platform_func() -> None: + execution_order.append("platform") + + @coroutine_with_priority(CoroPriority.CORE) + async def core_func() -> None: + execution_order.append("core") + + @coroutine_with_priority(CoroPriority.FINAL) + async def final_func() -> None: + execution_order.append("final") + + # Create event loop and add jobs + loop = FakeEventLoop() + loop.add_job(platform_func) + loop.add_job(core_func) + loop.add_job(final_func) + + # Run all tasks + loop.flush_tasks() + + # Check execution order (higher priority runs first) + assert execution_order == ["platform", "core", "final"] + + +def test_mixed_float_and_enum_priorities() -> None: + """Test that mixing float and enum priorities works correctly.""" + execution_order: list[str] = [] + + @coroutine_with_priority(1000.0) # Same as PLATFORM + async def func1() -> None: + execution_order.append("func1") + + @coroutine_with_priority(CoroPriority.CORE) + async def func2() -> None: + execution_order.append("func2") + + @coroutine_with_priority(-1000.0) # Same as FINAL + async def func3() -> None: + execution_order.append("func3") + + # Create event loop and add jobs + loop = FakeEventLoop() + loop.add_job(func2) + loop.add_job(func3) + loop.add_job(func1) + + # Run all tasks + loop.flush_tasks() + + # Check execution order + assert execution_order == ["func1", "func2", "func3"] + + +def test_enum_priority_comparison() -> None: + """Test that enum priorities can be compared directly.""" + assert CoroPriority.PLATFORM > CoroPriority.NETWORK + assert CoroPriority.NETWORK > CoroPriority.NETWORK_TRANSPORT + assert CoroPriority.NETWORK_TRANSPORT > CoroPriority.CORE + assert CoroPriority.CORE > CoroPriority.DIAGNOSTICS + assert CoroPriority.DIAGNOSTICS > CoroPriority.STATUS + assert CoroPriority.STATUS > CoroPriority.COMMUNICATION + assert CoroPriority.COMMUNICATION > CoroPriority.APPLICATION + assert CoroPriority.APPLICATION > CoroPriority.WEB + assert CoroPriority.WEB > CoroPriority.AUTOMATION + assert CoroPriority.AUTOMATION > CoroPriority.BUS + assert CoroPriority.BUS > CoroPriority.COMPONENT + assert CoroPriority.COMPONENT > CoroPriority.LATE + assert CoroPriority.LATE > CoroPriority.WORKAROUNDS + assert CoroPriority.WORKAROUNDS > CoroPriority.FINAL + + +def test_custom_priority_between_enum_values() -> None: + """Test that custom float priorities between enum values work correctly.""" + execution_order: list[str] = [] + + @coroutine_with_priority(CoroPriority.CORE) # 100 + async def core_func() -> None: + execution_order.append("core") + + @coroutine_with_priority(95.0) # Between CORE and DIAGNOSTICS + async def custom_func() -> None: + execution_order.append("custom") + + @coroutine_with_priority(CoroPriority.DIAGNOSTICS) # 90 + async def diag_func() -> None: + execution_order.append("diagnostics") + + # Create event loop and add jobs + loop = FakeEventLoop() + loop.add_job(diag_func) + loop.add_job(core_func) + loop.add_job(custom_func) + + # Run all tasks + loop.flush_tasks() + + # Check execution order + assert execution_order == ["core", "custom", "diagnostics"] From e3fb9c2a7855d7c961c94a849ceb4c69e0489c35 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 2 Sep 2025 19:51:17 -0400 Subject: [PATCH 118/208] [esp32] Remove hardcoding of ulp (#10535) --- esphome/components/esp32/__init__.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index ac236f4eb3..47832a08ae 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -855,11 +855,6 @@ async def to_code(config): cg.add_platformio_option("platform_packages", [conf[CONF_SOURCE]]) - # platformio/toolchain-esp32ulp does not support linux_aarch64 yet and has not been updated for over 2 years - # This is espressif's own published version which is more up to date. - cg.add_platformio_option( - "platform_packages", ["espressif/toolchain-esp32ulp@2.35.0-20220830"] - ) add_idf_sdkconfig_option(f"CONFIG_IDF_TARGET_{variant}", True) add_idf_sdkconfig_option( f"CONFIG_ESPTOOLPY_FLASHSIZE_{config[CONF_FLASH_SIZE]}", True From 1a054299d4813a9a367b3367d8516fdd92cb23fc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 2 Sep 2025 21:17:14 -0500 Subject: [PATCH 119/208] [core] Optimize fnv1_hash to avoid string allocations for static entities (#10529) --- esphome/core/entity_base.cpp | 14 +++++++++++--- esphome/core/entity_base.h | 3 +++ esphome/core/helpers.cpp | 10 ++++++---- esphome/core/helpers.h | 3 ++- 4 files changed, 22 insertions(+), 8 deletions(-) diff --git a/esphome/core/entity_base.cpp b/esphome/core/entity_base.cpp index 411a877bbf..4883c72cf1 100644 --- a/esphome/core/entity_base.cpp +++ b/esphome/core/entity_base.cpp @@ -45,10 +45,15 @@ void EntityBase::set_icon(const char *icon) { #endif } +// Check if the object_id is dynamic (changes with MAC suffix) +bool EntityBase::is_object_id_dynamic_() const { + return !this->flags_.has_own_name && App.is_name_add_mac_suffix_enabled(); +} + // Entity Object ID std::string EntityBase::get_object_id() const { // Check if `App.get_friendly_name()` is constant or dynamic. - if (!this->flags_.has_own_name && App.is_name_add_mac_suffix_enabled()) { + if (this->is_object_id_dynamic_()) { // `App.get_friendly_name()` is dynamic. return str_sanitize(str_snake_case(App.get_friendly_name())); } @@ -58,7 +63,7 @@ std::string EntityBase::get_object_id() const { StringRef EntityBase::get_object_id_ref_for_api_() const { static constexpr auto EMPTY_STRING = StringRef::from_lit(""); // Return empty for dynamic case (MAC suffix) - if (!this->flags_.has_own_name && App.is_name_add_mac_suffix_enabled()) { + if (this->is_object_id_dynamic_()) { return EMPTY_STRING; } // For static case, return the string or empty if null @@ -70,7 +75,10 @@ void EntityBase::set_object_id(const char *object_id) { } // Calculate Object ID Hash from Entity Name -void EntityBase::calc_object_id_() { this->object_id_hash_ = fnv1_hash(this->get_object_id()); } +void EntityBase::calc_object_id_() { + this->object_id_hash_ = + fnv1_hash(this->is_object_id_dynamic_() ? this->get_object_id().c_str() : this->object_id_c_str_); +} uint32_t EntityBase::get_object_id_hash() { return this->object_id_hash_; } diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index 8a65a9627a..4a6460e708 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -126,6 +126,9 @@ class EntityBase { virtual uint32_t hash_base() { return 0L; } void calc_object_id_(); + /// Check if the object_id is dynamic (changes with MAC suffix) + bool is_object_id_dynamic_() const; + StringRef name_; const char *object_id_c_str_{nullptr}; #ifdef USE_ENTITY_ICON diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 44e9193994..43d6f1153c 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -142,11 +142,13 @@ uint16_t crc16be(const uint8_t *data, uint16_t len, uint16_t crc, uint16_t poly, return refout ? (crc ^ 0xffff) : crc; } -uint32_t fnv1_hash(const std::string &str) { +uint32_t fnv1_hash(const char *str) { uint32_t hash = 2166136261UL; - for (char c : str) { - hash *= 16777619UL; - hash ^= c; + if (str) { + while (*str) { + hash *= 16777619UL; + hash ^= *str++; + } } return hash; } diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 53ec7a2a5a..a6741925d0 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -155,7 +155,8 @@ uint16_t crc16be(const uint8_t *data, uint16_t len, uint16_t crc = 0, uint16_t p bool refout = false); /// Calculate a FNV-1 hash of \p str. -uint32_t fnv1_hash(const std::string &str); +uint32_t fnv1_hash(const char *str); +inline uint32_t fnv1_hash(const std::string &str) { return fnv1_hash(str.c_str()); } /// Return a random 32-bit unsigned integer. uint32_t random_uint32(); From 83fbd77c4a4922411c1a5b79f83f108c9fd101da Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 2 Sep 2025 21:23:46 -0500 Subject: [PATCH 120/208] [core] Use get_icon_ref() in entity platform logging to avoid string allocations (#10530) --- esphome/components/button/button.cpp | 4 ++-- esphome/components/datetime/date_entity.h | 4 ++-- esphome/components/datetime/datetime_entity.h | 4 ++-- esphome/components/datetime/time_entity.h | 4 ++-- esphome/components/event/event.h | 4 ++-- esphome/components/lock/lock.h | 4 ++-- esphome/components/number/number.cpp | 4 ++-- esphome/components/select/select.h | 4 ++-- esphome/components/sensor/sensor.cpp | 4 ++-- esphome/components/switch/switch.cpp | 4 ++-- esphome/components/text/text.h | 4 ++-- esphome/components/text_sensor/text_sensor.h | 4 ++-- 12 files changed, 24 insertions(+), 24 deletions(-) diff --git a/esphome/components/button/button.cpp b/esphome/components/button/button.cpp index 63d71dcb8a..c968d31088 100644 --- a/esphome/components/button/button.cpp +++ b/esphome/components/button/button.cpp @@ -14,8 +14,8 @@ void log_button(const char *tag, const char *prefix, const char *type, Button *o ESP_LOGCONFIG(tag, "%s%s '%s'", prefix, type, obj->get_name().c_str()); - if (!obj->get_icon().empty()) { - ESP_LOGCONFIG(tag, "%s Icon: '%s'", prefix, obj->get_icon().c_str()); + if (!obj->get_icon_ref().empty()) { + ESP_LOGCONFIG(tag, "%s Icon: '%s'", prefix, obj->get_icon_ref().c_str()); } } diff --git a/esphome/components/datetime/date_entity.h b/esphome/components/datetime/date_entity.h index ce43c5639d..fcbb46cf17 100644 --- a/esphome/components/datetime/date_entity.h +++ b/esphome/components/datetime/date_entity.h @@ -16,8 +16,8 @@ namespace datetime { #define LOG_DATETIME_DATE(prefix, type, obj) \ if ((obj) != nullptr) { \ ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ - if (!(obj)->get_icon().empty()) { \ - ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon().c_str()); \ + if (!(obj)->get_icon_ref().empty()) { \ + ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon_ref().c_str()); \ } \ } diff --git a/esphome/components/datetime/datetime_entity.h b/esphome/components/datetime/datetime_entity.h index 27db84cf7e..275eedfd3b 100644 --- a/esphome/components/datetime/datetime_entity.h +++ b/esphome/components/datetime/datetime_entity.h @@ -16,8 +16,8 @@ namespace datetime { #define LOG_DATETIME_DATETIME(prefix, type, obj) \ if ((obj) != nullptr) { \ ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ - if (!(obj)->get_icon().empty()) { \ - ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon().c_str()); \ + if (!(obj)->get_icon_ref().empty()) { \ + ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon_ref().c_str()); \ } \ } diff --git a/esphome/components/datetime/time_entity.h b/esphome/components/datetime/time_entity.h index f7e0a7ddd9..e79b8c225d 100644 --- a/esphome/components/datetime/time_entity.h +++ b/esphome/components/datetime/time_entity.h @@ -16,8 +16,8 @@ namespace datetime { #define LOG_DATETIME_TIME(prefix, type, obj) \ if ((obj) != nullptr) { \ ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ - if (!(obj)->get_icon().empty()) { \ - ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon().c_str()); \ + if (!(obj)->get_icon_ref().empty()) { \ + ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon_ref().c_str()); \ } \ } diff --git a/esphome/components/event/event.h b/esphome/components/event/event.h index 03c3c8d95a..0f35c0657d 100644 --- a/esphome/components/event/event.h +++ b/esphome/components/event/event.h @@ -13,8 +13,8 @@ namespace event { #define LOG_EVENT(prefix, type, obj) \ if ((obj) != nullptr) { \ ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ - if (!(obj)->get_icon().empty()) { \ - ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon().c_str()); \ + if (!(obj)->get_icon_ref().empty()) { \ + ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon_ref().c_str()); \ } \ if (!(obj)->get_device_class().empty()) { \ ESP_LOGCONFIG(TAG, "%s Device Class: '%s'", prefix, (obj)->get_device_class().c_str()); \ diff --git a/esphome/components/lock/lock.h b/esphome/components/lock/lock.h index 2173c84903..04c4cd71cd 100644 --- a/esphome/components/lock/lock.h +++ b/esphome/components/lock/lock.h @@ -15,8 +15,8 @@ class Lock; #define LOG_LOCK(prefix, type, obj) \ if ((obj) != nullptr) { \ ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ - if (!(obj)->get_icon().empty()) { \ - ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon().c_str()); \ + if (!(obj)->get_icon_ref().empty()) { \ + ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon_ref().c_str()); \ } \ if ((obj)->traits.get_assumed_state()) { \ ESP_LOGCONFIG(TAG, "%s Assumed State: YES", prefix); \ diff --git a/esphome/components/number/number.cpp b/esphome/components/number/number.cpp index 4769c1ed12..e0f9fd89de 100644 --- a/esphome/components/number/number.cpp +++ b/esphome/components/number/number.cpp @@ -14,8 +14,8 @@ void log_number(const char *tag, const char *prefix, const char *type, Number *o ESP_LOGCONFIG(tag, "%s%s '%s'", prefix, type, obj->get_name().c_str()); - if (!obj->get_icon().empty()) { - ESP_LOGCONFIG(tag, "%s Icon: '%s'", prefix, obj->get_icon().c_str()); + if (!obj->get_icon_ref().empty()) { + ESP_LOGCONFIG(tag, "%s Icon: '%s'", prefix, obj->get_icon_ref().c_str()); } if (!obj->traits.get_unit_of_measurement().empty()) { diff --git a/esphome/components/select/select.h b/esphome/components/select/select.h index 3ab651b241..902b8a78ce 100644 --- a/esphome/components/select/select.h +++ b/esphome/components/select/select.h @@ -12,8 +12,8 @@ namespace select { #define LOG_SELECT(prefix, type, obj) \ if ((obj) != nullptr) { \ ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ - if (!(obj)->get_icon().empty()) { \ - ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon().c_str()); \ + if (!(obj)->get_icon_ref().empty()) { \ + ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon_ref().c_str()); \ } \ } diff --git a/esphome/components/sensor/sensor.cpp b/esphome/components/sensor/sensor.cpp index 6df6347c18..91bf965584 100644 --- a/esphome/components/sensor/sensor.cpp +++ b/esphome/components/sensor/sensor.cpp @@ -24,8 +24,8 @@ void log_sensor(const char *tag, const char *prefix, const char *type, Sensor *o ESP_LOGCONFIG(tag, "%s Device Class: '%s'", prefix, obj->get_device_class().c_str()); } - if (!obj->get_icon().empty()) { - ESP_LOGCONFIG(tag, "%s Icon: '%s'", prefix, obj->get_icon().c_str()); + if (!obj->get_icon_ref().empty()) { + ESP_LOGCONFIG(tag, "%s Icon: '%s'", prefix, obj->get_icon_ref().c_str()); } if (obj->get_force_update()) { diff --git a/esphome/components/switch/switch.cpp b/esphome/components/switch/switch.cpp index 49acd274b2..bfb9a277a2 100644 --- a/esphome/components/switch/switch.cpp +++ b/esphome/components/switch/switch.cpp @@ -91,8 +91,8 @@ void log_switch(const char *tag, const char *prefix, const char *type, Switch *o LOG_STR_ARG(onoff)); // Add optional fields separately - if (!obj->get_icon().empty()) { - ESP_LOGCONFIG(tag, "%s Icon: '%s'", prefix, obj->get_icon().c_str()); + if (!obj->get_icon_ref().empty()) { + ESP_LOGCONFIG(tag, "%s Icon: '%s'", prefix, obj->get_icon_ref().c_str()); } if (obj->assumed_state()) { ESP_LOGCONFIG(tag, "%s Assumed State: YES", prefix); diff --git a/esphome/components/text/text.h b/esphome/components/text/text.h index 3cc0cefc3e..74d08eda8a 100644 --- a/esphome/components/text/text.h +++ b/esphome/components/text/text.h @@ -12,8 +12,8 @@ namespace text { #define LOG_TEXT(prefix, type, obj) \ if ((obj) != nullptr) { \ ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ - if (!(obj)->get_icon().empty()) { \ - ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon().c_str()); \ + if (!(obj)->get_icon_ref().empty()) { \ + ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon_ref().c_str()); \ } \ } diff --git a/esphome/components/text_sensor/text_sensor.h b/esphome/components/text_sensor/text_sensor.h index b54f75155b..d68078b244 100644 --- a/esphome/components/text_sensor/text_sensor.h +++ b/esphome/components/text_sensor/text_sensor.h @@ -17,8 +17,8 @@ namespace text_sensor { if (!(obj)->get_device_class().empty()) { \ ESP_LOGCONFIG(TAG, "%s Device Class: '%s'", prefix, (obj)->get_device_class().c_str()); \ } \ - if (!(obj)->get_icon().empty()) { \ - ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon().c_str()); \ + if (!(obj)->get_icon_ref().empty()) { \ + ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon_ref().c_str()); \ } \ } From d2b23ba3a72fedb29d557c639db9b4f9be078789 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 2 Sep 2025 21:24:16 -0500 Subject: [PATCH 121/208] [sensor] Change state_class_to_string() to return const char* to avoid allocations (#10533) --- esphome/components/sensor/sensor.cpp | 6 +++--- esphome/components/sensor/sensor.h | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/sensor/sensor.cpp b/esphome/components/sensor/sensor.cpp index 91bf965584..81e55cf05f 100644 --- a/esphome/components/sensor/sensor.cpp +++ b/esphome/components/sensor/sensor.cpp @@ -17,8 +17,8 @@ void log_sensor(const char *tag, const char *prefix, const char *type, Sensor *o "%s State Class: '%s'\n" "%s Unit of Measurement: '%s'\n" "%s Accuracy Decimals: %d", - prefix, type, obj->get_name().c_str(), prefix, state_class_to_string(obj->get_state_class()).c_str(), - prefix, obj->get_unit_of_measurement().c_str(), prefix, obj->get_accuracy_decimals()); + prefix, type, obj->get_name().c_str(), prefix, state_class_to_string(obj->get_state_class()), prefix, + obj->get_unit_of_measurement().c_str(), prefix, obj->get_accuracy_decimals()); if (!obj->get_device_class().empty()) { ESP_LOGCONFIG(tag, "%s Device Class: '%s'", prefix, obj->get_device_class().c_str()); @@ -33,7 +33,7 @@ void log_sensor(const char *tag, const char *prefix, const char *type, Sensor *o } } -std::string state_class_to_string(StateClass state_class) { +const char *state_class_to_string(StateClass state_class) { switch (state_class) { case STATE_CLASS_MEASUREMENT: return "measurement"; diff --git a/esphome/components/sensor/sensor.h b/esphome/components/sensor/sensor.h index b3206d8dab..507cb326b2 100644 --- a/esphome/components/sensor/sensor.h +++ b/esphome/components/sensor/sensor.h @@ -33,7 +33,7 @@ enum StateClass : uint8_t { STATE_CLASS_TOTAL = 3, }; -std::string state_class_to_string(StateClass state_class); +const char *state_class_to_string(StateClass state_class); /** Base-class for all sensors. * From 5ba1c32242dd58611c3050b26f3d359191a9bf2d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 2 Sep 2025 21:26:43 -0500 Subject: [PATCH 122/208] [host] Fix memory allocation in preferences load() method (#10506) --- esphome/components/host/preferences.h | 5 +- .../fixtures/host_preferences_save_load.yaml | 110 ++++++++++++ tests/integration/test_host_preferences.py | 167 ++++++++++++++++++ 3 files changed, 280 insertions(+), 2 deletions(-) create mode 100644 tests/integration/fixtures/host_preferences_save_load.yaml create mode 100644 tests/integration/test_host_preferences.py diff --git a/esphome/components/host/preferences.h b/esphome/components/host/preferences.h index 6707366517..6b2e7eb8f9 100644 --- a/esphome/components/host/preferences.h +++ b/esphome/components/host/preferences.h @@ -42,9 +42,10 @@ class HostPreferences : public ESPPreferences { if (len > 255) return false; this->setup_(); - if (this->data.count(key) == 0) + auto it = this->data.find(key); + if (it == this->data.end()) return false; - auto vec = this->data[key]; + const auto &vec = it->second; if (vec.size() != len) return false; memcpy(data, vec.data(), len); diff --git a/tests/integration/fixtures/host_preferences_save_load.yaml b/tests/integration/fixtures/host_preferences_save_load.yaml new file mode 100644 index 0000000000..929c5f7ff0 --- /dev/null +++ b/tests/integration/fixtures/host_preferences_save_load.yaml @@ -0,0 +1,110 @@ +esphome: + name: test_device + on_boot: + - lambda: |- + ESP_LOGD("test", "Host preferences test starting"); + +host: + +logger: + level: DEBUG + +api: + +preferences: + flash_write_interval: 0s # Disable automatic saving for test control + +switch: + - platform: template + name: "Test Switch" + id: test_switch + optimistic: true + restore_mode: DISABLED # Don't auto-restore for test control + +number: + - platform: template + name: "Test Number" + id: test_number + min_value: 0 + max_value: 100 + step: 0.1 + optimistic: true + restore_value: false # Don't auto-restore for test control + +button: + - platform: template + name: "Save Preferences" + on_press: + - lambda: |- + // Save current values to preferences + ESPPreferenceObject switch_pref = global_preferences->make_preference(0x1234); + ESPPreferenceObject number_pref = global_preferences->make_preference(0x5678); + + bool switch_value = id(test_switch).state; + float number_value = id(test_number).state; + + if (switch_pref.save(&switch_value)) { + ESP_LOGI("test", "Preference saved: key=switch, value=%.1f", switch_value ? 1.0 : 0.0); + } + if (number_pref.save(&number_value)) { + ESP_LOGI("test", "Preference saved: key=number, value=%.1f", number_value); + } + + // Force sync to disk + global_preferences->sync(); + + - platform: template + name: "Load Preferences" + on_press: + - lambda: |- + // Load values from preferences + ESPPreferenceObject switch_pref = global_preferences->make_preference(0x1234); + ESPPreferenceObject number_pref = global_preferences->make_preference(0x5678); + + // Also try to load non-existent preferences (tests our fix) + ESPPreferenceObject fake_pref1 = global_preferences->make_preference(0x9999); + ESPPreferenceObject fake_pref2 = global_preferences->make_preference(0xAAAA); + + bool switch_value = false; + float number_value = 0.0; + uint32_t fake_value = 0; + int loaded_count = 0; + + // These should not exist and shouldn't create map entries + fake_pref1.load(&fake_value); + fake_pref2.load(&fake_value); + + if (switch_pref.load(&switch_value)) { + id(test_switch).publish_state(switch_value); + ESP_LOGI("test", "Preference loaded: key=switch, value=%.1f", switch_value ? 1.0 : 0.0); + loaded_count++; + } else { + ESP_LOGW("test", "Failed to load switch preference"); + } + + if (number_pref.load(&number_value)) { + id(test_number).publish_state(number_value); + ESP_LOGI("test", "Preference loaded: key=number, value=%.1f", number_value); + loaded_count++; + } else { + ESP_LOGW("test", "Failed to load number preference"); + } + + // Log completion message for the test to detect + ESP_LOGI("test", "Final load test: loaded %d preferences successfully", loaded_count); + + - platform: template + name: "Verify Preferences" + on_press: + - lambda: |- + // Verify current values match what we expect + bool switch_value = id(test_switch).state; + float number_value = id(test_number).state; + + // After loading, switch should be true (1.0) and number should be 42.5 + if (switch_value == true && number_value == 42.5) { + ESP_LOGI("test", "Preferences verified: values match!"); + } else { + ESP_LOGE("test", "Preferences mismatch: switch=%d, number=%.1f", + switch_value, number_value); + } diff --git a/tests/integration/test_host_preferences.py b/tests/integration/test_host_preferences.py new file mode 100644 index 0000000000..38c6460cf1 --- /dev/null +++ b/tests/integration/test_host_preferences.py @@ -0,0 +1,167 @@ +"""Test host preferences save and load functionality.""" + +from __future__ import annotations + +import asyncio +import re +from typing import Any + +from aioesphomeapi import ButtonInfo, EntityInfo, NumberInfo, SwitchInfo +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +def find_entity_by_name( + entities: list[EntityInfo], entity_type: type, name: str +) -> Any: + """Helper to find an entity by type and name.""" + return next( + (e for e in entities if isinstance(e, entity_type) and e.name == name), None + ) + + +@pytest.mark.asyncio +async def test_host_preferences_save_load( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that preferences are correctly saved and loaded after our optimization fix.""" + loop = asyncio.get_running_loop() + log_lines: list[str] = [] + preferences_saved = loop.create_future() + preferences_loaded = loop.create_future() + values_match = loop.create_future() + final_load_complete = loop.create_future() + + # Patterns to match preference logs + save_pattern = re.compile(r"Preference saved: key=(\w+), value=([0-9.]+)") + load_pattern = re.compile(r"Preference loaded: key=(\w+), value=([0-9.]+)") + verify_pattern = re.compile(r"Preferences verified: values match!") + final_load_success_pattern = re.compile( + r"Final load test: loaded \d+ preferences successfully" + ) + + saved_values: dict[str, float] = {} + loaded_values: dict[str, float] = {} + + def check_output(line: str) -> None: + """Check log output for preference operations.""" + log_lines.append(line) + + # Look for save operations + match = save_pattern.search(line) + if match: + key = match.group(1) + value = float(match.group(2)) + saved_values[key] = value + if len(saved_values) >= 2 and not preferences_saved.done(): + preferences_saved.set_result(True) + + # Look for load operations + match = load_pattern.search(line) + if match: + key = match.group(1) + value = float(match.group(2)) + loaded_values[key] = value + if len(loaded_values) >= 2 and not preferences_loaded.done(): + preferences_loaded.set_result(True) + + # Look for verification + if verify_pattern.search(line) and not values_match.done(): + values_match.set_result(True) + + # Look for final load test completion + if final_load_success_pattern.search(line) and not final_load_complete.done(): + final_load_complete.set_result(True) + + async with ( + run_compiled(yaml_config, line_callback=check_output), + api_client_connected() as client, + ): + # Get entity list + entities, _ = await client.list_entities_services() + + # Find our test entities using helper + test_switch = find_entity_by_name(entities, SwitchInfo, "Test Switch") + test_number = find_entity_by_name(entities, NumberInfo, "Test Number") + save_button = find_entity_by_name(entities, ButtonInfo, "Save Preferences") + load_button = find_entity_by_name(entities, ButtonInfo, "Load Preferences") + verify_button = find_entity_by_name(entities, ButtonInfo, "Verify Preferences") + + assert test_switch is not None, "Test Switch not found" + assert test_number is not None, "Test Number not found" + assert save_button is not None, "Save Preferences button not found" + assert load_button is not None, "Load Preferences button not found" + assert verify_button is not None, "Verify Preferences button not found" + + # Set initial values + client.switch_command(test_switch.key, True) + client.number_command(test_number.key, 42.5) + + # Save preferences + client.button_command(save_button.key) + + # Wait for save to complete + try: + await asyncio.wait_for(preferences_saved, timeout=5.0) + except TimeoutError: + pytest.fail("Preferences not saved within timeout") + + # Verify we saved the expected values + assert "switch" in saved_values, f"Switch preference not saved: {saved_values}" + assert "number" in saved_values, f"Number preference not saved: {saved_values}" + assert saved_values["switch"] == 1.0, ( + f"Switch value incorrect: {saved_values['switch']}" + ) + assert saved_values["number"] == 42.5, ( + f"Number value incorrect: {saved_values['number']}" + ) + + # Change the values to something else + client.switch_command(test_switch.key, False) + client.number_command(test_number.key, 13.7) + + # Load preferences (should restore the saved values) + client.button_command(load_button.key) + + # Wait for load to complete + try: + await asyncio.wait_for(preferences_loaded, timeout=5.0) + except TimeoutError: + pytest.fail("Preferences not loaded within timeout") + + # Verify loaded values match saved values + assert "switch" in loaded_values, ( + f"Switch preference not loaded: {loaded_values}" + ) + assert "number" in loaded_values, ( + f"Number preference not loaded: {loaded_values}" + ) + assert loaded_values["switch"] == saved_values["switch"], ( + f"Loaded switch value {loaded_values['switch']} doesn't match saved {saved_values['switch']}" + ) + assert loaded_values["number"] == saved_values["number"], ( + f"Loaded number value {loaded_values['number']} doesn't match saved {saved_values['number']}" + ) + + # Verify the values were actually restored + client.button_command(verify_button.key) + + # Wait for verification + try: + await asyncio.wait_for(values_match, timeout=5.0) + except TimeoutError: + pytest.fail("Preference verification failed within timeout") + + # Test that non-existent preferences don't crash (tests our fix) + # This will trigger load attempts for keys that don't exist + # Our fix should prevent map entries from being created + client.button_command(load_button.key) + + # Wait for the final load test to complete + try: + await asyncio.wait_for(final_load_complete, timeout=5.0) + except TimeoutError: + pytest.fail("Final load test did not complete within timeout") From 086f1982fa176bcabb31a09ca65f5b233f8097e3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 2 Sep 2025 21:26:53 -0500 Subject: [PATCH 123/208] [core] Use get_device_class_ref() in entity platform logging to avoid string allocations (#10531) --- esphome/components/binary_sensor/binary_sensor.cpp | 4 ++-- esphome/components/cover/cover.h | 4 ++-- esphome/components/event/event.h | 4 ++-- esphome/components/number/number.cpp | 4 ++-- esphome/components/sensor/sensor.cpp | 4 ++-- esphome/components/switch/switch.cpp | 4 ++-- esphome/components/text_sensor/text_sensor.h | 4 ++-- esphome/components/valve/valve.h | 4 ++-- 8 files changed, 16 insertions(+), 16 deletions(-) diff --git a/esphome/components/binary_sensor/binary_sensor.cpp b/esphome/components/binary_sensor/binary_sensor.cpp index e652d302b6..39319d3c1c 100644 --- a/esphome/components/binary_sensor/binary_sensor.cpp +++ b/esphome/components/binary_sensor/binary_sensor.cpp @@ -15,8 +15,8 @@ void log_binary_sensor(const char *tag, const char *prefix, const char *type, Bi ESP_LOGCONFIG(tag, "%s%s '%s'", prefix, type, obj->get_name().c_str()); - if (!obj->get_device_class().empty()) { - ESP_LOGCONFIG(tag, "%s Device Class: '%s'", prefix, obj->get_device_class().c_str()); + if (!obj->get_device_class_ref().empty()) { + ESP_LOGCONFIG(tag, "%s Device Class: '%s'", prefix, obj->get_device_class_ref().c_str()); } } diff --git a/esphome/components/cover/cover.h b/esphome/components/cover/cover.h index 8b6f5b8a72..ada5953d57 100644 --- a/esphome/components/cover/cover.h +++ b/esphome/components/cover/cover.h @@ -19,8 +19,8 @@ const extern float COVER_CLOSED; if (traits_.get_is_assumed_state()) { \ ESP_LOGCONFIG(TAG, "%s Assumed State: YES", prefix); \ } \ - if (!(obj)->get_device_class().empty()) { \ - ESP_LOGCONFIG(TAG, "%s Device Class: '%s'", prefix, (obj)->get_device_class().c_str()); \ + if (!(obj)->get_device_class_ref().empty()) { \ + ESP_LOGCONFIG(TAG, "%s Device Class: '%s'", prefix, (obj)->get_device_class_ref().c_str()); \ } \ } diff --git a/esphome/components/event/event.h b/esphome/components/event/event.h index 0f35c0657d..a90c8ebe05 100644 --- a/esphome/components/event/event.h +++ b/esphome/components/event/event.h @@ -16,8 +16,8 @@ namespace event { if (!(obj)->get_icon_ref().empty()) { \ ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon_ref().c_str()); \ } \ - if (!(obj)->get_device_class().empty()) { \ - ESP_LOGCONFIG(TAG, "%s Device Class: '%s'", prefix, (obj)->get_device_class().c_str()); \ + if (!(obj)->get_device_class_ref().empty()) { \ + ESP_LOGCONFIG(TAG, "%s Device Class: '%s'", prefix, (obj)->get_device_class_ref().c_str()); \ } \ } diff --git a/esphome/components/number/number.cpp b/esphome/components/number/number.cpp index e0f9fd89de..8f5cd9bdeb 100644 --- a/esphome/components/number/number.cpp +++ b/esphome/components/number/number.cpp @@ -22,8 +22,8 @@ void log_number(const char *tag, const char *prefix, const char *type, Number *o ESP_LOGCONFIG(tag, "%s Unit of Measurement: '%s'", prefix, obj->traits.get_unit_of_measurement().c_str()); } - if (!obj->traits.get_device_class().empty()) { - ESP_LOGCONFIG(tag, "%s Device Class: '%s'", prefix, obj->traits.get_device_class().c_str()); + if (!obj->traits.get_device_class_ref().empty()) { + ESP_LOGCONFIG(tag, "%s Device Class: '%s'", prefix, obj->traits.get_device_class_ref().c_str()); } } diff --git a/esphome/components/sensor/sensor.cpp b/esphome/components/sensor/sensor.cpp index 81e55cf05f..ae45498949 100644 --- a/esphome/components/sensor/sensor.cpp +++ b/esphome/components/sensor/sensor.cpp @@ -20,8 +20,8 @@ void log_sensor(const char *tag, const char *prefix, const char *type, Sensor *o prefix, type, obj->get_name().c_str(), prefix, state_class_to_string(obj->get_state_class()), prefix, obj->get_unit_of_measurement().c_str(), prefix, obj->get_accuracy_decimals()); - if (!obj->get_device_class().empty()) { - ESP_LOGCONFIG(tag, "%s Device Class: '%s'", prefix, obj->get_device_class().c_str()); + if (!obj->get_device_class_ref().empty()) { + ESP_LOGCONFIG(tag, "%s Device Class: '%s'", prefix, obj->get_device_class_ref().c_str()); } if (!obj->get_icon_ref().empty()) { diff --git a/esphome/components/switch/switch.cpp b/esphome/components/switch/switch.cpp index bfb9a277a2..02cee91a76 100644 --- a/esphome/components/switch/switch.cpp +++ b/esphome/components/switch/switch.cpp @@ -100,8 +100,8 @@ void log_switch(const char *tag, const char *prefix, const char *type, Switch *o if (obj->is_inverted()) { ESP_LOGCONFIG(tag, "%s Inverted: YES", prefix); } - if (!obj->get_device_class().empty()) { - ESP_LOGCONFIG(tag, "%s Device Class: '%s'", prefix, obj->get_device_class().c_str()); + if (!obj->get_device_class_ref().empty()) { + ESP_LOGCONFIG(tag, "%s Device Class: '%s'", prefix, obj->get_device_class_ref().c_str()); } } } diff --git a/esphome/components/text_sensor/text_sensor.h b/esphome/components/text_sensor/text_sensor.h index d68078b244..3ab88e2d91 100644 --- a/esphome/components/text_sensor/text_sensor.h +++ b/esphome/components/text_sensor/text_sensor.h @@ -14,8 +14,8 @@ namespace text_sensor { #define LOG_TEXT_SENSOR(prefix, type, obj) \ if ((obj) != nullptr) { \ ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \ - if (!(obj)->get_device_class().empty()) { \ - ESP_LOGCONFIG(TAG, "%s Device Class: '%s'", prefix, (obj)->get_device_class().c_str()); \ + if (!(obj)->get_device_class_ref().empty()) { \ + ESP_LOGCONFIG(TAG, "%s Device Class: '%s'", prefix, (obj)->get_device_class_ref().c_str()); \ } \ if (!(obj)->get_icon_ref().empty()) { \ ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon_ref().c_str()); \ diff --git a/esphome/components/valve/valve.h b/esphome/components/valve/valve.h index 0e14a8d8f0..ab7ff5abe1 100644 --- a/esphome/components/valve/valve.h +++ b/esphome/components/valve/valve.h @@ -19,8 +19,8 @@ const extern float VALVE_CLOSED; if (traits_.get_is_assumed_state()) { \ ESP_LOGCONFIG(TAG, "%s Assumed State: YES", prefix); \ } \ - if (!(obj)->get_device_class().empty()) { \ - ESP_LOGCONFIG(TAG, "%s Device Class: '%s'", prefix, (obj)->get_device_class().c_str()); \ + if (!(obj)->get_device_class_ref().empty()) { \ + ESP_LOGCONFIG(TAG, "%s Device Class: '%s'", prefix, (obj)->get_device_class_ref().c_str()); \ } \ } From 68628a85b15659c9bacf0059739774886bf6989f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 3 Sep 2025 01:08:57 -0500 Subject: [PATCH 124/208] [core] Use get_unit_of_measurement_ref() in entity logging to avoid string allocations (#10532) --- esphome/components/number/number.cpp | 4 ++-- esphome/components/sensor/sensor.cpp | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/number/number.cpp b/esphome/components/number/number.cpp index 8f5cd9bdeb..da08faf655 100644 --- a/esphome/components/number/number.cpp +++ b/esphome/components/number/number.cpp @@ -18,8 +18,8 @@ void log_number(const char *tag, const char *prefix, const char *type, Number *o ESP_LOGCONFIG(tag, "%s Icon: '%s'", prefix, obj->get_icon_ref().c_str()); } - if (!obj->traits.get_unit_of_measurement().empty()) { - ESP_LOGCONFIG(tag, "%s Unit of Measurement: '%s'", prefix, obj->traits.get_unit_of_measurement().c_str()); + if (!obj->traits.get_unit_of_measurement_ref().empty()) { + ESP_LOGCONFIG(tag, "%s Unit of Measurement: '%s'", prefix, obj->traits.get_unit_of_measurement_ref().c_str()); } if (!obj->traits.get_device_class_ref().empty()) { diff --git a/esphome/components/sensor/sensor.cpp b/esphome/components/sensor/sensor.cpp index ae45498949..e2e8302d8b 100644 --- a/esphome/components/sensor/sensor.cpp +++ b/esphome/components/sensor/sensor.cpp @@ -18,7 +18,7 @@ void log_sensor(const char *tag, const char *prefix, const char *type, Sensor *o "%s Unit of Measurement: '%s'\n" "%s Accuracy Decimals: %d", prefix, type, obj->get_name().c_str(), prefix, state_class_to_string(obj->get_state_class()), prefix, - obj->get_unit_of_measurement().c_str(), prefix, obj->get_accuracy_decimals()); + obj->get_unit_of_measurement_ref().c_str(), prefix, obj->get_accuracy_decimals()); if (!obj->get_device_class_ref().empty()) { ESP_LOGCONFIG(tag, "%s Device Class: '%s'", prefix, obj->get_device_class_ref().c_str()); @@ -128,7 +128,7 @@ void Sensor::internal_send_state_to_frontend(float state) { this->set_has_state(true); this->state = state; ESP_LOGD(TAG, "'%s': Sending state %.5f %s with %d decimals of accuracy", this->get_name().c_str(), state, - this->get_unit_of_measurement().c_str(), this->get_accuracy_decimals()); + this->get_unit_of_measurement_ref().c_str(), this->get_accuracy_decimals()); this->callback_.call(state); } From 4d681ffe3d1629cc5e07e1bf3af8c23caeded9a6 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 3 Sep 2025 11:47:51 -0400 Subject: [PATCH 125/208] [esp32] Rebuild when idf_component.yml changes (#10540) --- esphome/components/esp32/__init__.py | 7 ++++++- esphome/writer.py | 4 ++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 47832a08ae..54f83ab6e8 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -40,6 +40,7 @@ from esphome.cpp_generator import RawExpression import esphome.final_validate as fv from esphome.helpers import copy_file_if_changed, mkdir_p, write_file_if_changed from esphome.types import ConfigType +from esphome.writer import clean_cmake_cache from .boards import BOARDS, STANDARD_BOARDS from .const import ( # noqa @@ -1074,7 +1075,11 @@ def _write_idf_component_yml(): contents = yaml_util.dump({"dependencies": dependencies}) else: contents = "" - write_file_if_changed(yml_path, contents) + if write_file_if_changed(yml_path, contents): + dependencies_lock = CORE.relative_build_path("dependencies.lock") + if os.path.isfile(dependencies_lock): + os.remove(dependencies_lock) + clean_cmake_cache() # Called by writer.py diff --git a/esphome/writer.py b/esphome/writer.py index 4b25a25f7e..b8fe44abdd 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -310,6 +310,10 @@ def clean_build(): if os.path.isdir(piolibdeps): _LOGGER.info("Deleting %s", piolibdeps) shutil.rmtree(piolibdeps) + dependencies_lock = CORE.relative_build_path("dependencies.lock") + if os.path.isfile(dependencies_lock): + _LOGGER.info("Deleting %s", dependencies_lock) + os.remove(dependencies_lock) GITIGNORE_CONTENT = """# Gitignore settings for ESPHome From c3359edb33f6e403efa5b8970bd60ebcb1e80079 Mon Sep 17 00:00:00 2001 From: Anton Viktorov Date: Wed, 3 Sep 2025 19:18:26 +0200 Subject: [PATCH 126/208] [i2c] Fix bug write_register16 (#10547) --- esphome/components/i2c/i2c.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/i2c/i2c.cpp b/esphome/components/i2c/i2c.cpp index e66ab8ba73..48e1cf8aca 100644 --- a/esphome/components/i2c/i2c.cpp +++ b/esphome/components/i2c/i2c.cpp @@ -47,9 +47,9 @@ ErrorCode I2CDevice::write_register(uint8_t a_register, const uint8_t *data, siz ErrorCode I2CDevice::write_register16(uint16_t a_register, const uint8_t *data, size_t len) const { std::vector v(len + 2); - v.push_back(a_register >> 8); - v.push_back(a_register); - v.insert(v.end(), data, data + len); + v[0] = a_register >> 8; + v[1] = a_register; + std::copy(data, data + len, v.begin() + 2); return bus_->write_readv(this->address_, v.data(), v.size(), nullptr, 0); } From 8aeb6d3ba2e3eda1726ec1e2acaf8d5060518ca0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 3 Sep 2025 14:27:39 -0500 Subject: [PATCH 127/208] [bluetooth_proxy] Change default for active connections to true (#10546) --- esphome/components/bluetooth_proxy/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/bluetooth_proxy/__init__.py b/esphome/components/bluetooth_proxy/__init__.py index 112faa27e5..f21b5028c7 100644 --- a/esphome/components/bluetooth_proxy/__init__.py +++ b/esphome/components/bluetooth_proxy/__init__.py @@ -80,7 +80,7 @@ CONFIG_SCHEMA = cv.All( cv.Schema( { cv.GenerateID(): cv.declare_id(BluetoothProxy), - cv.Optional(CONF_ACTIVE, default=False): cv.boolean, + cv.Optional(CONF_ACTIVE, default=True): cv.boolean, cv.SplitDefault(CONF_CACHE_SERVICES, esp32_idf=True): cv.All( cv.only_with_esp_idf, cv.boolean ), From 0ab65c225e9152ad0930a885515803f3f6c14f64 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 3 Sep 2025 15:58:42 -0400 Subject: [PATCH 128/208] [wifi] Check for esp32_hosted on no wifi variants (#10528) --- esphome/components/wifi/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 7943911021..c63a12f879 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -51,7 +51,7 @@ from . import wpa2_eap AUTO_LOAD = ["network"] -NO_WIFI_VARIANTS = [const.VARIANT_ESP32H2] +NO_WIFI_VARIANTS = [const.VARIANT_ESP32H2, const.VARIANT_ESP32P4] CONF_SAVE = "save" wifi_ns = cg.esphome_ns.namespace("wifi") @@ -179,8 +179,8 @@ WIFI_NETWORK_STA = WIFI_NETWORK_BASE.extend( def validate_variant(_): if CORE.is_esp32: variant = get_esp32_variant() - if variant in NO_WIFI_VARIANTS: - raise cv.Invalid(f"{variant} does not support WiFi") + if variant in NO_WIFI_VARIANTS and "esp32_hosted" not in fv.full_config.get(): + raise cv.Invalid(f"WiFi requires component esp32_hosted on {variant}") def final_validate(config): From 5759692627d7679f5f2693585a779b14a6d1a3c0 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Wed, 3 Sep 2025 15:59:47 -0400 Subject: [PATCH 129/208] [esp32] Clear IDF environment variables (#10527) Co-authored-by: J. Nick Koston --- esphome/components/esp32/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 54f83ab6e8..12d84dd4b3 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -841,6 +841,9 @@ async def to_code(config): if conf[CONF_ADVANCED][CONF_IGNORE_EFUSE_CUSTOM_MAC]: cg.add_define("USE_ESP32_IGNORE_EFUSE_CUSTOM_MAC") + for clean_var in ("IDF_PATH", "IDF_TOOLS_PATH"): + os.environ.pop(clean_var, None) + add_extra_script( "post", "post_build.py", From 23c66509029dc8a32f7669901de69947f4ef778a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 3 Sep 2025 15:07:13 -0500 Subject: [PATCH 130/208] [api] Fix VERY_VERBOSE logging compilation error with bool arrays (#10539) --- esphome/components/api/api_pb2_dump.cpp | 2 +- script/api_protobuf/api_protobuf.py | 4 +++- tests/integration/fixtures/parallel_script_delays.yaml | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index 3e7df9195b..1d7d315419 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -1135,7 +1135,7 @@ void ExecuteServiceArgument::dump_to(std::string &out) const { dump_field(out, "string_", this->string_); dump_field(out, "int_", this->int_); for (const auto it : this->bool_array) { - dump_field(out, "bool_array", it, 4); + dump_field(out, "bool_array", static_cast(it), 4); } for (const auto &it : this->int_array) { dump_field(out, "int_array", it, 4); diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 511d70d3ec..205bac4937 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -1059,7 +1059,9 @@ def _generate_array_dump_content( # Check if underlying type can use dump_field if ti.can_use_dump_field(): # For types that have dump_field overloads, use them with extra indent - o += f' dump_field(out, "{name}", {ti.dump_field_value("it")}, 4);\n' + # std::vector iterators return proxy objects, need explicit cast + value_expr = "static_cast(it)" if is_bool else ti.dump_field_value("it") + o += f' dump_field(out, "{name}", {value_expr}, 4);\n' else: # For complex types (messages, bytes), use the old pattern o += f' out.append(" {name}: ");\n' diff --git a/tests/integration/fixtures/parallel_script_delays.yaml b/tests/integration/fixtures/parallel_script_delays.yaml index 6887045913..71d5b904e9 100644 --- a/tests/integration/fixtures/parallel_script_delays.yaml +++ b/tests/integration/fixtures/parallel_script_delays.yaml @@ -4,7 +4,7 @@ esphome: host: logger: - level: DEBUG + level: VERY_VERBOSE api: actions: From c03d978b46e96bccd08d0e2d595e9f66594b09ad Mon Sep 17 00:00:00 2001 From: Maxim Raznatovski Date: Thu, 4 Sep 2025 04:02:49 +0200 Subject: [PATCH 131/208] [wizard] extend the wizard dashboard API to allow upload and empty config options (#10203) --- esphome/dashboard/web_server.py | 77 ++++++++++++++++++++++++++++----- esphome/wizard.py | 59 +++++++++++++++---------- tests/unit_tests/test_wizard.py | 61 ++++++++++++++++++++++++++ 3 files changed, 164 insertions(+), 33 deletions(-) diff --git a/esphome/dashboard/web_server.py b/esphome/dashboard/web_server.py index fd16667d8a..294a180794 100644 --- a/esphome/dashboard/web_server.py +++ b/esphome/dashboard/web_server.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio import base64 +import binascii from collections.abc import Callable, Iterable import datetime import functools @@ -490,7 +491,17 @@ class WizardRequestHandler(BaseHandler): kwargs = { k: v for k, v in json.loads(self.request.body.decode()).items() - if k in ("name", "platform", "board", "ssid", "psk", "password") + if k + in ( + "type", + "name", + "platform", + "board", + "ssid", + "psk", + "password", + "file_content", + ) } if not kwargs["name"]: self.set_status(422) @@ -498,19 +509,65 @@ class WizardRequestHandler(BaseHandler): self.write(json.dumps({"error": "Name is required"})) return + if "type" not in kwargs: + # Default to basic wizard type for backwards compatibility + kwargs["type"] = "basic" + kwargs["friendly_name"] = kwargs["name"] kwargs["name"] = friendly_name_slugify(kwargs["friendly_name"]) - - kwargs["ota_password"] = secrets.token_hex(16) - noise_psk = secrets.token_bytes(32) - kwargs["api_encryption_key"] = base64.b64encode(noise_psk).decode() + if kwargs["type"] == "basic": + kwargs["ota_password"] = secrets.token_hex(16) + noise_psk = secrets.token_bytes(32) + kwargs["api_encryption_key"] = base64.b64encode(noise_psk).decode() + elif kwargs["type"] == "upload": + try: + kwargs["file_text"] = base64.b64decode(kwargs["file_content"]).decode( + "utf-8" + ) + except (binascii.Error, UnicodeDecodeError): + self.set_status(422) + self.set_header("content-type", "application/json") + self.write( + json.dumps({"error": "The uploaded file is not correctly encoded."}) + ) + return + elif kwargs["type"] != "empty": + self.set_status(422) + self.set_header("content-type", "application/json") + self.write( + json.dumps( + {"error": f"Invalid wizard type specified: {kwargs['type']}"} + ) + ) + return filename = f"{kwargs['name']}.yaml" destination = settings.rel_path(filename) - wizard.wizard_write(path=destination, **kwargs) - self.set_status(200) - self.set_header("content-type", "application/json") - self.write(json.dumps({"configuration": filename})) - self.finish() + + # Check if destination file already exists + if os.path.exists(destination): + self.set_status(409) # Conflict status code + self.set_header("content-type", "application/json") + self.write( + json.dumps({"error": f"Configuration file '{filename}' already exists"}) + ) + self.finish() + return + + success = wizard.wizard_write(path=destination, **kwargs) + if success: + self.set_status(200) + self.set_header("content-type", "application/json") + self.write(json.dumps({"configuration": filename})) + self.finish() + else: + self.set_status(500) + self.set_header("content-type", "application/json") + self.write( + json.dumps( + {"error": "Failed to write configuration, see logs for details"} + ) + ) + self.finish() class ImportRequestHandler(BaseHandler): diff --git a/esphome/wizard.py b/esphome/wizard.py index 8602e90222..cb599df59a 100644 --- a/esphome/wizard.py +++ b/esphome/wizard.py @@ -189,32 +189,45 @@ def wizard_write(path, **kwargs): from esphome.components.rtl87xx import boards as rtl87xx_boards name = kwargs["name"] - board = kwargs["board"] + if kwargs["type"] == "empty": + file_text = "" + # Will be updated later after editing the file + hardware = "UNKNOWN" + elif kwargs["type"] == "upload": + file_text = kwargs["file_text"] + hardware = "UNKNOWN" + else: # "basic" + board = kwargs["board"] - for key in ("ssid", "psk", "password", "ota_password"): - if key in kwargs: - kwargs[key] = sanitize_double_quotes(kwargs[key]) + for key in ("ssid", "psk", "password", "ota_password"): + if key in kwargs: + kwargs[key] = sanitize_double_quotes(kwargs[key]) + if "platform" not in kwargs: + if board in esp8266_boards.BOARDS: + platform = "ESP8266" + elif board in esp32_boards.BOARDS: + platform = "ESP32" + elif board in rp2040_boards.BOARDS: + platform = "RP2040" + elif board in bk72xx_boards.BOARDS: + platform = "BK72XX" + elif board in ln882x_boards.BOARDS: + platform = "LN882X" + elif board in rtl87xx_boards.BOARDS: + platform = "RTL87XX" + else: + safe_print(color(AnsiFore.RED, f'The board "{board}" is unknown.')) + return False + kwargs["platform"] = platform + hardware = kwargs["platform"] + file_text = wizard_file(**kwargs) - if "platform" not in kwargs: - if board in esp8266_boards.BOARDS: - platform = "ESP8266" - elif board in esp32_boards.BOARDS: - platform = "ESP32" - elif board in rp2040_boards.BOARDS: - platform = "RP2040" - elif board in bk72xx_boards.BOARDS: - platform = "BK72XX" - elif board in ln882x_boards.BOARDS: - platform = "LN882X" - elif board in rtl87xx_boards.BOARDS: - platform = "RTL87XX" - else: - safe_print(color(AnsiFore.RED, f'The board "{board}" is unknown.')) - return False - kwargs["platform"] = platform - hardware = kwargs["platform"] + # Check if file already exists to prevent overwriting + if os.path.exists(path) and os.path.isfile(path): + safe_print(color(AnsiFore.RED, f'The file "{path}" already exists.')) + return False - write_file(path, wizard_file(**kwargs)) + write_file(path, file_text) storage = StorageJSON.from_wizard(name, name, f"{name}.local", hardware) storage_path = ext_storage_path(os.path.basename(path)) storage.save(storage_path) diff --git a/tests/unit_tests/test_wizard.py b/tests/unit_tests/test_wizard.py index ab20b2abb5..fea2fb5558 100644 --- a/tests/unit_tests/test_wizard.py +++ b/tests/unit_tests/test_wizard.py @@ -17,6 +17,7 @@ import esphome.wizard as wz @pytest.fixture def default_config(): return { + "type": "basic", "name": "test-name", "platform": "ESP8266", "board": "esp01_1m", @@ -125,6 +126,47 @@ def test_wizard_write_sets_platform(default_config, tmp_path, monkeypatch): assert "esp8266:" in generated_config +def test_wizard_empty_config(tmp_path, monkeypatch): + """ + The wizard should be able to create an empty configuration + """ + # Given + empty_config = { + "type": "empty", + "name": "test-empty", + } + monkeypatch.setattr(wz, "write_file", MagicMock()) + monkeypatch.setattr(CORE, "config_path", os.path.dirname(tmp_path)) + + # When + wz.wizard_write(tmp_path, **empty_config) + + # Then + generated_config = wz.write_file.call_args.args[1] + assert generated_config == "" + + +def test_wizard_upload_config(tmp_path, monkeypatch): + """ + The wizard should be able to import an base64 encoded configuration + """ + # Given + empty_config = { + "type": "upload", + "name": "test-upload", + "file_text": "# imported file 📁\n\n", + } + monkeypatch.setattr(wz, "write_file", MagicMock()) + monkeypatch.setattr(CORE, "config_path", os.path.dirname(tmp_path)) + + # When + wz.wizard_write(tmp_path, **empty_config) + + # Then + generated_config = wz.write_file.call_args.args[1] + assert generated_config == "# imported file 📁\n\n" + + def test_wizard_write_defaults_platform_from_board_esp8266( default_config, tmp_path, monkeypatch ): @@ -471,3 +513,22 @@ def test_wizard_requires_valid_ssid(tmpdir, monkeypatch, wizard_answers): # Then assert retval == 0 + + +def test_wizard_write_protects_existing_config(tmpdir, default_config, monkeypatch): + """ + The wizard_write function should not overwrite existing config files and return False + """ + # Given + config_file = tmpdir.join("test.yaml") + original_content = "# Original config content\n" + config_file.write(original_content) + + monkeypatch.setattr(CORE, "config_path", str(tmpdir)) + + # When + result = wz.wizard_write(str(config_file), **default_config) + + # Then + assert result is False # Should return False when file exists + assert config_file.read() == original_content From 8fb6420b1c487a5dedeb1a5b2391866a8070e310 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 4 Sep 2025 02:44:12 -0500 Subject: [PATCH 132/208] [esp8266] Store GPIO initialization arrays in PROGMEM to save RAM (#10560) --- esphome/components/esp8266/core.cpp | 4 ++-- esphome/components/esp8266/gpio.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/esp8266/core.cpp b/esphome/components/esp8266/core.cpp index 2d3959b031..200ca567c2 100644 --- a/esphome/components/esp8266/core.cpp +++ b/esphome/components/esp8266/core.cpp @@ -58,8 +58,8 @@ extern "C" void resetPins() { // NOLINT #ifdef USE_ESP8266_EARLY_PIN_INIT for (int i = 0; i < 16; i++) { - uint8_t mode = ESPHOME_ESP8266_GPIO_INITIAL_MODE[i]; - uint8_t level = ESPHOME_ESP8266_GPIO_INITIAL_LEVEL[i]; + uint8_t mode = progmem_read_byte(&ESPHOME_ESP8266_GPIO_INITIAL_MODE[i]); + uint8_t level = progmem_read_byte(&ESPHOME_ESP8266_GPIO_INITIAL_LEVEL[i]); if (mode != 255) pinMode(i, mode); // NOLINT if (level != 255) diff --git a/esphome/components/esp8266/gpio.py b/esphome/components/esp8266/gpio.py index 2bc2291117..e7492fc505 100644 --- a/esphome/components/esp8266/gpio.py +++ b/esphome/components/esp8266/gpio.py @@ -199,11 +199,11 @@ async def add_pin_initial_states_array(): cg.add_global( cg.RawExpression( - f"const uint8_t ESPHOME_ESP8266_GPIO_INITIAL_MODE[16] = {{{initial_modes_s}}}" + f"const uint8_t ESPHOME_ESP8266_GPIO_INITIAL_MODE[16] PROGMEM = {{{initial_modes_s}}}" ) ) cg.add_global( cg.RawExpression( - f"const uint8_t ESPHOME_ESP8266_GPIO_INITIAL_LEVEL[16] = {{{initial_levels_s}}}" + f"const uint8_t ESPHOME_ESP8266_GPIO_INITIAL_LEVEL[16] PROGMEM = {{{initial_levels_s}}}" ) ) From 101d553df999784b3e5507b2f76f4880bd18874d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 4 Sep 2025 02:46:50 -0500 Subject: [PATCH 133/208] [esp8266] Reduce preference memory usage by 40% through field optimization (#10557) --- esphome/components/esp8266/preferences.cpp | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/esphome/components/esp8266/preferences.cpp b/esphome/components/esp8266/preferences.cpp index bb7e436bea..a26e9cc498 100644 --- a/esphome/components/esp8266/preferences.cpp +++ b/esphome/components/esp8266/preferences.cpp @@ -1,6 +1,7 @@ #ifdef USE_ESP8266 #include +#include extern "C" { #include "spi_flash.h" } @@ -119,16 +120,16 @@ static bool load_from_rtc(size_t offset, uint32_t *data, size_t len) { class ESP8266PreferenceBackend : public ESPPreferenceBackend { public: - size_t offset = 0; uint32_t type = 0; + uint16_t offset = 0; + uint8_t length_words = 0; // Max 255 words (1020 bytes of data) bool in_flash = false; - size_t length_words = 0; bool save(const uint8_t *data, size_t len) override { if (bytes_to_words(len) != length_words) { return false; } - size_t buffer_size = length_words + 1; + size_t buffer_size = static_cast(length_words) + 1; std::unique_ptr buffer(new uint32_t[buffer_size]()); // Note the () for zero-initialization memcpy(buffer.get(), data, len); buffer[length_words] = calculate_crc(buffer.get(), buffer.get() + length_words, type); @@ -142,7 +143,7 @@ class ESP8266PreferenceBackend : public ESPPreferenceBackend { if (bytes_to_words(len) != length_words) { return false; } - size_t buffer_size = length_words + 1; + size_t buffer_size = static_cast(length_words) + 1; std::unique_ptr buffer(new uint32_t[buffer_size]()); bool ret = in_flash ? load_from_flash(offset, buffer.get(), buffer_size) : load_from_rtc(offset, buffer.get(), buffer_size); @@ -176,15 +177,19 @@ class ESP8266Preferences : public ESPPreferences { ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash) override { uint32_t length_words = bytes_to_words(length); + if (length_words > 255) { + ESP_LOGE(TAG, "Preference too large: %" PRIu32 " words > 255", length_words); + return {}; + } if (in_flash) { uint32_t start = current_flash_offset; uint32_t end = start + length_words + 1; if (end > ESP8266_FLASH_STORAGE_SIZE) return {}; auto *pref = new ESP8266PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory) - pref->offset = start; + pref->offset = static_cast(start); pref->type = type; - pref->length_words = length_words; + pref->length_words = static_cast(length_words); pref->in_flash = true; current_flash_offset = end; return {pref}; @@ -210,9 +215,9 @@ class ESP8266Preferences : public ESPPreferences { uint32_t rtc_offset = in_normal ? start + 32 : start - 96; auto *pref = new ESP8266PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory) - pref->offset = rtc_offset; + pref->offset = static_cast(rtc_offset); pref->type = type; - pref->length_words = length_words; + pref->length_words = static_cast(length_words); pref->in_flash = false; current_offset += length_words + 1; return pref; From e0617e01e018526a77e9c90742ca02046e56852e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 4 Sep 2025 11:10:53 -0500 Subject: [PATCH 134/208] Bump pypa/gh-action-pypi-publish from 1.12.4 to 1.13.0 in /.github/workflows (#10572) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9af67aa310..f9b0cfb6a0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -70,7 +70,7 @@ jobs: pip3 install build python3 -m build - name: Publish - uses: pypa/gh-action-pypi-publish@v1.12.4 + uses: pypa/gh-action-pypi-publish@v1.13.0 with: skip-existing: true From dc45a613f35f247df81a96dc57ee1be7439c48cd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 4 Sep 2025 14:14:18 -0500 Subject: [PATCH 135/208] Bump pytest from 8.4.1 to 8.4.2 (#10579) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 22f58fd3d7..e0009f2427 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -5,7 +5,7 @@ pyupgrade==3.20.0 # also change in .pre-commit-config.yaml when updating pre-commit # Unit tests -pytest==8.4.1 +pytest==8.4.2 pytest-cov==6.2.1 pytest-mock==3.14.1 pytest-asyncio==1.1.0 From 25489b6009c74f4daf101f2e0af5fe0e93507192 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 4 Sep 2025 14:14:28 -0500 Subject: [PATCH 136/208] Bump codecov/codecov-action from 5.5.0 to 5.5.1 (#10585) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8f429f7b40..ea5ca47a19 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -156,7 +156,7 @@ jobs: . venv/bin/activate pytest -vv --cov-report=xml --tb=native -n auto tests --ignore=tests/integration/ - name: Upload coverage to Codecov - uses: codecov/codecov-action@v5.5.0 + uses: codecov/codecov-action@v5.5.1 with: token: ${{ secrets.CODECOV_TOKEN }} - name: Save Python virtual environment cache From edf7094662923d5fe550ce1c2cbf13c12d4ac09b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 4 Sep 2025 14:14:50 -0500 Subject: [PATCH 137/208] Bump esphome-dashboard from 20250828.0 to 20250904.0 (#10580) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 32fdfabcda..b86c7765c7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ pyserial==3.5 platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile esptool==5.0.2 click==8.1.7 -esphome-dashboard==20250828.0 +esphome-dashboard==20250904.0 aioesphomeapi==39.0.1 zeroconf==0.147.0 puremagic==1.30 From cbac9caa522a81db481db34376e9d5dc5d19afe8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 4 Sep 2025 14:15:43 -0500 Subject: [PATCH 138/208] Bump actions/setup-python from 5.6.0 to 6.0.0 (#10584) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci-api-proto.yml | 2 +- .github/workflows/ci-clang-tidy-hash.yml | 2 +- .github/workflows/ci-docker.yml | 2 +- .github/workflows/ci.yml | 4 ++-- .github/workflows/release.yml | 4 ++-- .github/workflows/sync-device-classes.yml | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci-api-proto.yml b/.github/workflows/ci-api-proto.yml index c7cc720323..144cc11647 100644 --- a/.github/workflows/ci-api-proto.yml +++ b/.github/workflows/ci-api-proto.yml @@ -23,7 +23,7 @@ jobs: - name: Checkout uses: actions/checkout@v5.0.0 - name: Set up Python - uses: actions/setup-python@v5.6.0 + uses: actions/setup-python@v6.0.0 with: python-version: "3.11" diff --git a/.github/workflows/ci-clang-tidy-hash.yml b/.github/workflows/ci-clang-tidy-hash.yml index c7da7f6672..582be17fbb 100644 --- a/.github/workflows/ci-clang-tidy-hash.yml +++ b/.github/workflows/ci-clang-tidy-hash.yml @@ -23,7 +23,7 @@ jobs: uses: actions/checkout@v5.0.0 - name: Set up Python - uses: actions/setup-python@v5.6.0 + uses: actions/setup-python@v6.0.0 with: python-version: "3.11" diff --git a/.github/workflows/ci-docker.yml b/.github/workflows/ci-docker.yml index 61ecf8183b..915a4dfb7e 100644 --- a/.github/workflows/ci-docker.yml +++ b/.github/workflows/ci-docker.yml @@ -45,7 +45,7 @@ jobs: steps: - uses: actions/checkout@v5.0.0 - name: Set up Python - uses: actions/setup-python@v5.6.0 + uses: actions/setup-python@v6.0.0 with: python-version: "3.11" - name: Set up Docker Buildx diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ea5ca47a19..07fd91b1c8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,7 +42,7 @@ jobs: run: echo key="${{ hashFiles('requirements.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT - name: Set up Python ${{ env.DEFAULT_PYTHON }} id: python - uses: actions/setup-python@v5.6.0 + uses: actions/setup-python@v6.0.0 with: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore Python virtual environment @@ -217,7 +217,7 @@ jobs: uses: actions/checkout@v5.0.0 - name: Set up Python 3.13 id: python - uses: actions/setup-python@v5.6.0 + uses: actions/setup-python@v6.0.0 with: python-version: "3.13" - name: Restore Python virtual environment diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f9b0cfb6a0..41db736caa 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -62,7 +62,7 @@ jobs: steps: - uses: actions/checkout@v5.0.0 - name: Set up Python - uses: actions/setup-python@v5.6.0 + uses: actions/setup-python@v6.0.0 with: python-version: "3.x" - name: Build @@ -94,7 +94,7 @@ jobs: steps: - uses: actions/checkout@v5.0.0 - name: Set up Python - uses: actions/setup-python@v5.6.0 + uses: actions/setup-python@v6.0.0 with: python-version: "3.11" diff --git a/.github/workflows/sync-device-classes.yml b/.github/workflows/sync-device-classes.yml index cc03ed3e3f..b129e8f4bf 100644 --- a/.github/workflows/sync-device-classes.yml +++ b/.github/workflows/sync-device-classes.yml @@ -22,7 +22,7 @@ jobs: path: lib/home-assistant - name: Setup Python - uses: actions/setup-python@v5.6.0 + uses: actions/setup-python@v6.0.0 with: python-version: 3.13 From c471bdb44617a0b47153186eb1b98e5deaf6b1f8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 4 Sep 2025 14:15:57 -0500 Subject: [PATCH 139/208] Bump actions/setup-python from 5.6.0 to 6.0.0 in /.github/actions/restore-python (#10586) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/actions/restore-python/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/restore-python/action.yml b/.github/actions/restore-python/action.yml index 9fb80e6a9d..5d290894a7 100644 --- a/.github/actions/restore-python/action.yml +++ b/.github/actions/restore-python/action.yml @@ -17,7 +17,7 @@ runs: steps: - name: Set up Python ${{ inputs.python-version }} id: python - uses: actions/setup-python@v5.6.0 + uses: actions/setup-python@v6.0.0 with: python-version: ${{ inputs.python-version }} - name: Restore Python virtual environment From ba2433197e232ef6e28e825b3ab450cf4da7efa1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 4 Sep 2025 14:16:17 -0500 Subject: [PATCH 140/208] Bump actions/github-script from 7.0.1 to 8.0.0 (#10583) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/auto-label-pr.yml | 2 +- .github/workflows/ci-api-proto.yml | 4 ++-- .github/workflows/ci-clang-tidy-hash.yml | 4 ++-- .github/workflows/codeowner-review-request.yml | 2 +- .github/workflows/external-component-bot.yml | 2 +- .github/workflows/issue-codeowner-notify.yml | 2 +- .github/workflows/release.yml | 4 ++-- .github/workflows/status-check-labels.yml | 2 +- 8 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/auto-label-pr.yml b/.github/workflows/auto-label-pr.yml index c42b5330d2..66369c706f 100644 --- a/.github/workflows/auto-label-pr.yml +++ b/.github/workflows/auto-label-pr.yml @@ -32,7 +32,7 @@ jobs: private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }} - name: Auto Label PR - uses: actions/github-script@v7.0.1 + uses: actions/github-script@v8.0.0 with: github-token: ${{ steps.generate-token.outputs.token }} script: | diff --git a/.github/workflows/ci-api-proto.yml b/.github/workflows/ci-api-proto.yml index 144cc11647..ec214d1a77 100644 --- a/.github/workflows/ci-api-proto.yml +++ b/.github/workflows/ci-api-proto.yml @@ -47,7 +47,7 @@ jobs: fi - if: failure() name: Review PR - uses: actions/github-script@v7.0.1 + uses: actions/github-script@v8.0.0 with: script: | await github.rest.pulls.createReview({ @@ -70,7 +70,7 @@ jobs: esphome/components/api/api_pb2_service.* - if: success() name: Dismiss review - uses: actions/github-script@v7.0.1 + uses: actions/github-script@v8.0.0 with: script: | let reviews = await github.rest.pulls.listReviews({ diff --git a/.github/workflows/ci-clang-tidy-hash.yml b/.github/workflows/ci-clang-tidy-hash.yml index 582be17fbb..2f47386abf 100644 --- a/.github/workflows/ci-clang-tidy-hash.yml +++ b/.github/workflows/ci-clang-tidy-hash.yml @@ -41,7 +41,7 @@ jobs: - if: failure() name: Request changes - uses: actions/github-script@v7.0.1 + uses: actions/github-script@v8.0.0 with: script: | await github.rest.pulls.createReview({ @@ -54,7 +54,7 @@ jobs: - if: success() name: Dismiss review - uses: actions/github-script@v7.0.1 + uses: actions/github-script@v8.0.0 with: script: | let reviews = await github.rest.pulls.listReviews({ diff --git a/.github/workflows/codeowner-review-request.yml b/.github/workflows/codeowner-review-request.yml index ab3377365d..475e05b970 100644 --- a/.github/workflows/codeowner-review-request.yml +++ b/.github/workflows/codeowner-review-request.yml @@ -25,7 +25,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Request reviews from component codeowners - uses: actions/github-script@v7.0.1 + uses: actions/github-script@v8.0.0 with: script: | const owner = context.repo.owner; diff --git a/.github/workflows/external-component-bot.yml b/.github/workflows/external-component-bot.yml index 29103e8eee..736c986f7e 100644 --- a/.github/workflows/external-component-bot.yml +++ b/.github/workflows/external-component-bot.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Add external component comment - uses: actions/github-script@v7.0.1 + uses: actions/github-script@v8.0.0 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | diff --git a/.github/workflows/issue-codeowner-notify.yml b/.github/workflows/issue-codeowner-notify.yml index 3639d346f5..ab9b96b45a 100644 --- a/.github/workflows/issue-codeowner-notify.yml +++ b/.github/workflows/issue-codeowner-notify.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Notify codeowners for component issues - uses: actions/github-script@v7.0.1 + uses: actions/github-script@v8.0.0 with: script: | const owner = context.repo.owner; diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 41db736caa..efc8424cd6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -220,7 +220,7 @@ jobs: - deploy-manifest steps: - name: Trigger Workflow - uses: actions/github-script@v7.0.1 + uses: actions/github-script@v8.0.0 with: github-token: ${{ secrets.DEPLOY_HA_ADDON_REPO_TOKEN }} script: | @@ -246,7 +246,7 @@ jobs: environment: ${{ needs.init.outputs.deploy_env }} steps: - name: Trigger Workflow - uses: actions/github-script@v7.0.1 + uses: actions/github-script@v8.0.0 with: github-token: ${{ secrets.DEPLOY_ESPHOME_SCHEMA_REPO_TOKEN }} script: | diff --git a/.github/workflows/status-check-labels.yml b/.github/workflows/status-check-labels.yml index 157f60f3a1..675be49c27 100644 --- a/.github/workflows/status-check-labels.yml +++ b/.github/workflows/status-check-labels.yml @@ -16,7 +16,7 @@ jobs: - merge-after-release steps: - name: Check for ${{ matrix.label }} label - uses: actions/github-script@v7.0.1 + uses: actions/github-script@v8.0.0 with: script: | const { data: labels } = await github.rest.issues.listLabelsOnIssue({ From e55bce83e3a8b77a068c7b9f381dbe9f0a3e0431 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 4 Sep 2025 14:20:11 -0500 Subject: [PATCH 141/208] Bump actions/stale from 9.1.0 to 10.0.0 (#10582) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/stale.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index b79939fc8e..88e07d3f58 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -17,7 +17,7 @@ jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale@v9.1.0 + - uses: actions/stale@v10.0.0 with: days-before-pr-stale: 90 days-before-pr-close: 7 @@ -37,7 +37,7 @@ jobs: close-issues: runs-on: ubuntu-latest steps: - - uses: actions/stale@v9.1.0 + - uses: actions/stale@v10.0.0 with: days-before-pr-stale: -1 days-before-pr-close: -1 From 4c2f356b357647368ee1da5c21f20c504e5c9682 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 4 Sep 2025 19:24:23 +0000 Subject: [PATCH 142/208] Bump ruff from 0.12.11 to 0.12.12 (#10578) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston --- .pre-commit-config.yaml | 2 +- requirements_test.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e05733ec96..2b161cf05c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ ci: repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.12.11 + rev: v0.12.12 hooks: # Run the linter. - id: ruff diff --git a/requirements_test.txt b/requirements_test.txt index e0009f2427..ba4ebeef2e 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==3.3.8 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating -ruff==0.12.11 # also change in .pre-commit-config.yaml when updating +ruff==0.12.12 # also change in .pre-commit-config.yaml when updating pyupgrade==3.20.0 # also change in .pre-commit-config.yaml when updating pre-commit From e327ae8c95a220f6843a056972d98b8c050ae39d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 4 Sep 2025 23:36:11 +0000 Subject: [PATCH 143/208] Bump pytest-mock from 3.14.1 to 3.15.0 (#10593) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index ba4ebeef2e..eba14fc0b1 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -7,7 +7,7 @@ pre-commit # Unit tests pytest==8.4.2 pytest-cov==6.2.1 -pytest-mock==3.14.1 +pytest-mock==3.15.0 pytest-asyncio==1.1.0 pytest-xdist==3.8.0 asyncmock==0.4.2 From 365a427b5776ebbe240e5e7786fd9ab242301f76 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 4 Sep 2025 23:37:03 +0000 Subject: [PATCH 144/208] Bump aioesphomeapi from 39.0.1 to 40.0.0 (#10594) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index b86c7765c7..60ce3e67e3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile esptool==5.0.2 click==8.1.7 esphome-dashboard==20250904.0 -aioesphomeapi==39.0.1 +aioesphomeapi==40.0.0 zeroconf==0.147.0 puremagic==1.30 ruamel.yaml==0.18.15 # dashboard_import From b8ed7ec145d72657ed5805a08932af82d289f243 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 5 Sep 2025 01:08:15 +0000 Subject: [PATCH 145/208] Bump aioesphomeapi from 40.0.0 to 40.0.1 (#10596) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 60ce3e67e3..bac5622708 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile esptool==5.0.2 click==8.1.7 esphome-dashboard==20250904.0 -aioesphomeapi==40.0.0 +aioesphomeapi==40.0.1 zeroconf==0.147.0 puremagic==1.30 ruamel.yaml==0.18.15 # dashboard_import From b4b795dcaf0cb1ef3ba440bcc3d6cda223627876 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 4 Sep 2025 23:26:46 -0500 Subject: [PATCH 146/208] [i2c] Optimize memory usage with stack allocation for small buffers (#10565) Co-authored-by: Keith Burzinski --- esphome/components/i2c/i2c.cpp | 22 ++++++++++------- esphome/components/i2c/i2c_bus.h | 42 +++++++++++++++++++++++++++----- 2 files changed, 49 insertions(+), 15 deletions(-) diff --git a/esphome/components/i2c/i2c.cpp b/esphome/components/i2c/i2c.cpp index 48e1cf8aca..31c21f398c 100644 --- a/esphome/components/i2c/i2c.cpp +++ b/esphome/components/i2c/i2c.cpp @@ -39,18 +39,22 @@ ErrorCode I2CDevice::read_register16(uint16_t a_register, uint8_t *data, size_t } ErrorCode I2CDevice::write_register(uint8_t a_register, const uint8_t *data, size_t len) const { - std::vector v{}; - v.push_back(a_register); - v.insert(v.end(), data, data + len); - return bus_->write_readv(this->address_, v.data(), v.size(), nullptr, 0); + SmallBufferWithHeapFallback<17> buffer_alloc; // Most I2C writes are <= 16 bytes + uint8_t *buffer = buffer_alloc.get(len + 1); + + buffer[0] = a_register; + std::copy(data, data + len, buffer + 1); + return this->bus_->write_readv(this->address_, buffer, len + 1, nullptr, 0); } ErrorCode I2CDevice::write_register16(uint16_t a_register, const uint8_t *data, size_t len) const { - std::vector v(len + 2); - v[0] = a_register >> 8; - v[1] = a_register; - std::copy(data, data + len, v.begin() + 2); - return bus_->write_readv(this->address_, v.data(), v.size(), nullptr, 0); + SmallBufferWithHeapFallback<18> buffer_alloc; // Most I2C writes are <= 16 bytes + 2 for register + uint8_t *buffer = buffer_alloc.get(len + 2); + + buffer[0] = a_register >> 8; + buffer[1] = a_register; + std::copy(data, data + len, buffer + 2); + return this->bus_->write_readv(this->address_, buffer, len + 2, nullptr, 0); } bool I2CDevice::read_bytes_16(uint8_t a_register, uint16_t *data, uint8_t len) { diff --git a/esphome/components/i2c/i2c_bus.h b/esphome/components/i2c/i2c_bus.h index df4df628e8..1acbe506a3 100644 --- a/esphome/components/i2c/i2c_bus.h +++ b/esphome/components/i2c/i2c_bus.h @@ -2,6 +2,7 @@ #include #include #include +#include #include #include @@ -10,6 +11,22 @@ namespace esphome { namespace i2c { +/// @brief Helper class for efficient buffer allocation - uses stack for small sizes, heap for large +template class SmallBufferWithHeapFallback { + public: + uint8_t *get(size_t size) { + if (size <= STACK_SIZE) { + return this->stack_buffer_; + } + this->heap_buffer_ = std::unique_ptr(new uint8_t[size]); + return this->heap_buffer_.get(); + } + + private: + uint8_t stack_buffer_[STACK_SIZE]; + std::unique_ptr heap_buffer_; +}; + /// @brief Error codes returned by I2CBus and I2CDevice methods enum ErrorCode { NO_ERROR = 0, ///< No error found during execution of method @@ -74,14 +91,17 @@ class I2CBus { for (size_t i = 0; i != count; i++) { total_len += read_buffers[i].len; } - std::vector buffer(total_len); - auto err = this->write_readv(address, nullptr, 0, buffer.data(), total_len); + + SmallBufferWithHeapFallback<128> buffer_alloc; // Most I2C reads are small + uint8_t *buffer = buffer_alloc.get(total_len); + + auto err = this->write_readv(address, nullptr, 0, buffer, total_len); if (err != ERROR_OK) return err; size_t pos = 0; for (size_t i = 0; i != count; i++) { if (read_buffers[i].len != 0) { - std::memcpy(read_buffers[i].data, buffer.data() + pos, read_buffers[i].len); + std::memcpy(read_buffers[i].data, buffer + pos, read_buffers[i].len); pos += read_buffers[i].len; } } @@ -91,11 +111,21 @@ class I2CBus { ESPDEPRECATED("This method is deprecated and will be removed in ESPHome 2026.3.0. Use write_readv() instead.", "2025.9.0") ErrorCode writev(uint8_t address, const WriteBuffer *write_buffers, size_t count, bool stop = true) { - std::vector buffer{}; + size_t total_len = 0; for (size_t i = 0; i != count; i++) { - buffer.insert(buffer.end(), write_buffers[i].data, write_buffers[i].data + write_buffers[i].len); + total_len += write_buffers[i].len; } - return this->write_readv(address, buffer.data(), buffer.size(), nullptr, 0); + + SmallBufferWithHeapFallback<128> buffer_alloc; // Most I2C writes are small + uint8_t *buffer = buffer_alloc.get(total_len); + + size_t pos = 0; + for (size_t i = 0; i != count; i++) { + std::memcpy(buffer + pos, write_buffers[i].data, write_buffers[i].len); + pos += write_buffers[i].len; + } + + return this->write_readv(address, buffer, total_len, nullptr, 0); } protected: From 86c2af48825d825801098982efa32b67f8a21cf6 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Fri, 5 Sep 2025 01:37:57 -0500 Subject: [PATCH 147/208] [sen5x] Fix initialization (#10603) --- esphome/components/sen5x/sen5x.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/sen5x/sen5x.cpp b/esphome/components/sen5x/sen5x.cpp index 0f27ec1b10..f3222221a2 100644 --- a/esphome/components/sen5x/sen5x.cpp +++ b/esphome/components/sen5x/sen5x.cpp @@ -38,6 +38,7 @@ void SEN5XComponent::setup() { this->mark_failed(); return; } + delay(20); // per datasheet uint16_t raw_read_status; if (!this->read_data(raw_read_status)) { From 0069163d31fc24a336afd4b9a3454da9f3e0796a Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Fri, 5 Sep 2025 14:11:14 -0500 Subject: [PATCH 148/208] [sps30] Tidy up, optimize (#10606) --- esphome/components/sps30/sps30.cpp | 41 +++++++++++++----------------- esphome/components/sps30/sps30.h | 6 ++--- 2 files changed, 21 insertions(+), 26 deletions(-) diff --git a/esphome/components/sps30/sps30.cpp b/esphome/components/sps30/sps30.cpp index 272acc78f2..b99bf416d6 100644 --- a/esphome/components/sps30/sps30.cpp +++ b/esphome/components/sps30/sps30.cpp @@ -43,20 +43,20 @@ void SPS30Component::setup() { this->serial_number_[i * 2] = static_cast(raw_serial_number[i] >> 8); this->serial_number_[i * 2 + 1] = uint16_t(uint16_t(raw_serial_number[i] & 0xFF)); } - ESP_LOGD(TAG, " Serial Number: '%s'", this->serial_number_); + ESP_LOGV(TAG, " Serial number: %s", this->serial_number_); bool result; if (this->fan_interval_.has_value()) { // override default value - result = write_command(SPS30_CMD_SET_AUTOMATIC_CLEANING_INTERVAL_SECONDS, this->fan_interval_.value()); + result = this->write_command(SPS30_CMD_SET_AUTOMATIC_CLEANING_INTERVAL_SECONDS, this->fan_interval_.value()); } else { - result = write_command(SPS30_CMD_SET_AUTOMATIC_CLEANING_INTERVAL_SECONDS); + result = this->write_command(SPS30_CMD_SET_AUTOMATIC_CLEANING_INTERVAL_SECONDS); } if (result) { delay(20); uint16_t secs[2]; if (this->read_data(secs, 2)) { - fan_interval_ = secs[0] << 16 | secs[1]; + this->fan_interval_ = secs[0] << 16 | secs[1]; } } @@ -67,7 +67,7 @@ void SPS30Component::setup() { } void SPS30Component::dump_config() { - ESP_LOGCONFIG(TAG, "sps30:"); + ESP_LOGCONFIG(TAG, "SPS30:"); LOG_I2C_DEVICE(this); if (this->is_failed()) { switch (this->error_code_) { @@ -78,16 +78,16 @@ void SPS30Component::dump_config() { ESP_LOGW(TAG, "Measurement Initialization failed"); break; case SERIAL_NUMBER_REQUEST_FAILED: - ESP_LOGW(TAG, "Unable to request sensor serial number"); + ESP_LOGW(TAG, "Unable to request serial number"); break; case SERIAL_NUMBER_READ_FAILED: - ESP_LOGW(TAG, "Unable to read sensor serial number"); + ESP_LOGW(TAG, "Unable to read serial number"); break; case FIRMWARE_VERSION_REQUEST_FAILED: - ESP_LOGW(TAG, "Unable to request sensor firmware version"); + ESP_LOGW(TAG, "Unable to request firmware version"); break; case FIRMWARE_VERSION_READ_FAILED: - ESP_LOGW(TAG, "Unable to read sensor firmware version"); + ESP_LOGW(TAG, "Unable to read firmware version"); break; default: ESP_LOGW(TAG, "Unknown setup error"); @@ -96,9 +96,9 @@ void SPS30Component::dump_config() { } LOG_UPDATE_INTERVAL(this); ESP_LOGCONFIG(TAG, - " Serial Number: '%s'\n" + " Serial number: %s\n" " Firmware version v%0d.%0d", - this->serial_number_, (raw_firmware_version_ >> 8), uint16_t(raw_firmware_version_ & 0xFF)); + this->serial_number_, this->raw_firmware_version_ >> 8, this->raw_firmware_version_ & 0xFF); LOG_SENSOR(" ", "PM1.0 Weight Concentration", this->pm_1_0_sensor_); LOG_SENSOR(" ", "PM2.5 Weight Concentration", this->pm_2_5_sensor_); LOG_SENSOR(" ", "PM4 Weight Concentration", this->pm_4_0_sensor_); @@ -113,15 +113,15 @@ void SPS30Component::dump_config() { void SPS30Component::update() { /// Check if warning flag active (sensor reconnected?) if (this->status_has_warning()) { - ESP_LOGD(TAG, "Trying to reconnect"); + ESP_LOGD(TAG, "Reconnecting"); if (this->write_command(SPS30_CMD_SOFT_RESET)) { - ESP_LOGD(TAG, "Soft-reset successful. Waiting for reconnection in 500 ms"); + ESP_LOGD(TAG, "Soft-reset successful; waiting 500 ms"); this->set_timeout(500, [this]() { this->start_continuous_measurement_(); /// Sensor restarted and reading attempt made next cycle this->status_clear_warning(); this->skipped_data_read_cycles_ = 0; - ESP_LOGD(TAG, "Reconnect successful. Resuming continuous measurement"); + ESP_LOGD(TAG, "Reconnected; resuming continuous measurement"); }); } else { ESP_LOGD(TAG, "Soft-reset failed"); @@ -136,12 +136,12 @@ void SPS30Component::update() { uint16_t raw_read_status; if (!this->read_data(&raw_read_status, 1) || raw_read_status == 0x00) { - ESP_LOGD(TAG, "Not ready yet"); + ESP_LOGD(TAG, "Not ready"); this->skipped_data_read_cycles_++; /// The following logic is required to address the cases when a sensor is quickly replaced before it's marked /// as failed so that new sensor is eventually forced to be reinitialized for continuous measurement. if (this->skipped_data_read_cycles_ > MAX_SKIPPED_DATA_CYCLES_BEFORE_ERROR) { - ESP_LOGD(TAG, "Exceeded max allowed attempts; communication will be reinitialized"); + ESP_LOGD(TAG, "Exceeded max attempts; will reinitialize"); this->status_set_warning(); } return; @@ -211,11 +211,6 @@ void SPS30Component::update() { } bool SPS30Component::start_continuous_measurement_() { - uint8_t data[4]; - data[0] = SPS30_CMD_START_CONTINUOUS_MEASUREMENTS & 0xFF; - data[1] = 0x03; - data[2] = 0x00; - data[3] = sht_crc_(0x03, 0x00); if (!this->write_command(SPS30_CMD_START_CONTINUOUS_MEASUREMENTS, SPS30_CMD_START_CONTINUOUS_MEASUREMENTS_ARG)) { ESP_LOGE(TAG, "Error initiating measurements"); return false; @@ -224,9 +219,9 @@ bool SPS30Component::start_continuous_measurement_() { } bool SPS30Component::start_fan_cleaning() { - if (!write_command(SPS30_CMD_START_FAN_CLEANING)) { + if (!this->write_command(SPS30_CMD_START_FAN_CLEANING)) { this->status_set_warning(); - ESP_LOGE(TAG, "write error start fan (%d)", this->last_error_); + ESP_LOGE(TAG, "Start fan cleaning failed (%d)", this->last_error_); return false; } else { ESP_LOGD(TAG, "Fan auto clean started"); diff --git a/esphome/components/sps30/sps30.h b/esphome/components/sps30/sps30.h index 04189247e8..461a770ab6 100644 --- a/esphome/components/sps30/sps30.h +++ b/esphome/components/sps30/sps30.h @@ -30,12 +30,12 @@ class SPS30Component : public PollingComponent, public sensirion_common::Sensiri bool start_fan_cleaning(); protected: - char serial_number_[17] = {0}; /// Terminating NULL character uint16_t raw_firmware_version_; - bool start_continuous_measurement_(); + char serial_number_[17] = {0}; /// Terminating NULL character uint8_t skipped_data_read_cycles_ = 0; + bool start_continuous_measurement_(); - enum ErrorCode { + enum ErrorCode : uint8_t { COMMUNICATION_FAILED, FIRMWARE_VERSION_REQUEST_FAILED, FIRMWARE_VERSION_READ_FAILED, From 09b40b882e3755d3778e6252a41bffbdeabffeb0 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Fri, 5 Sep 2025 14:20:11 -0500 Subject: [PATCH 149/208] [sgp30] Tidy up, optimize (#10607) --- .../sensirion_common/i2c_sensirion.h | 2 + esphome/components/sgp30/sgp30.cpp | 69 +++++++++---------- esphome/components/sgp30/sgp30.h | 21 +++--- 3 files changed, 47 insertions(+), 45 deletions(-) diff --git a/esphome/components/sensirion_common/i2c_sensirion.h b/esphome/components/sensirion_common/i2c_sensirion.h index aba93d6cc3..e141591525 100644 --- a/esphome/components/sensirion_common/i2c_sensirion.h +++ b/esphome/components/sensirion_common/i2c_sensirion.h @@ -15,6 +15,8 @@ namespace sensirion_common { * Format: * | 16 Bit Command Code | 16 bit Data word 1 | CRC of DW 1 | 16 bit Data word 1 | CRC of DW 2 | .. */ +static const uint8_t CRC_POLYNOMIAL = 0x31; // default for Sensirion + class SensirionI2CDevice : public i2c::I2CDevice { public: enum CommandLen : uint8_t { ADDR_8_BIT = 1, ADDR_16_BIT = 2 }; diff --git a/esphome/components/sgp30/sgp30.cpp b/esphome/components/sgp30/sgp30.cpp index 42baff6d23..0e3aeff812 100644 --- a/esphome/components/sgp30/sgp30.cpp +++ b/esphome/components/sgp30/sgp30.cpp @@ -1,9 +1,11 @@ #include "sgp30.h" -#include #include "esphome/core/application.h" #include "esphome/core/hal.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include + namespace esphome { namespace sgp30 { @@ -39,9 +41,8 @@ void SGP30Component::setup() { this->mark_failed(); return; } - this->serial_number_ = (uint64_t(raw_serial_number[0]) << 24) | (uint64_t(raw_serial_number[1]) << 16) | - (uint64_t(raw_serial_number[2])); - ESP_LOGD(TAG, "Serial Number: %" PRIu64, this->serial_number_); + this->serial_number_ = encode_uint24(raw_serial_number[0], raw_serial_number[1], raw_serial_number[2]); + ESP_LOGD(TAG, "Serial number: %" PRIu64, this->serial_number_); // Featureset identification for future use uint16_t raw_featureset; @@ -61,11 +62,11 @@ void SGP30Component::setup() { this->mark_failed(); return; } - ESP_LOGD(TAG, "Product version: 0x%0X", uint16_t(this->featureset_ & 0x1FF)); + ESP_LOGV(TAG, "Product version: 0x%0X", uint16_t(this->featureset_ & 0x1FF)); // Sensor initialization if (!this->write_command(SGP30_CMD_IAQ_INIT)) { - ESP_LOGE(TAG, "Sensor sgp30_iaq_init failed."); + ESP_LOGE(TAG, "sgp30_iaq_init failed"); this->error_code_ = MEASUREMENT_INIT_FAILED; this->mark_failed(); return; @@ -123,7 +124,7 @@ void SGP30Component::read_iaq_baseline_() { uint16_t eco2baseline = (raw_data[0]); uint16_t tvocbaseline = (raw_data[1]); - ESP_LOGI(TAG, "Current eCO2 baseline: 0x%04X, TVOC baseline: 0x%04X", eco2baseline, tvocbaseline); + ESP_LOGI(TAG, "Baselines: eCO2: 0x%04X, TVOC: 0x%04X", eco2baseline, tvocbaseline); if (eco2baseline != this->eco2_baseline_ || tvocbaseline != this->tvoc_baseline_) { this->eco2_baseline_ = eco2baseline; this->tvoc_baseline_ = tvocbaseline; @@ -142,7 +143,7 @@ void SGP30Component::read_iaq_baseline_() { this->baselines_storage_.eco2 = this->eco2_baseline_; this->baselines_storage_.tvoc = this->tvoc_baseline_; if (this->pref_.save(&this->baselines_storage_)) { - ESP_LOGI(TAG, "Store eCO2 baseline: 0x%04X, TVOC baseline: 0x%04X", this->baselines_storage_.eco2, + ESP_LOGI(TAG, "Store baselines: eCO2: 0x%04X, TVOC: 0x%04X", this->baselines_storage_.eco2, this->baselines_storage_.tvoc); } else { ESP_LOGW(TAG, "Could not store eCO2 and TVOC baselines"); @@ -164,7 +165,7 @@ void SGP30Component::send_env_data_() { if (this->humidity_sensor_ != nullptr) humidity = this->humidity_sensor_->state; if (std::isnan(humidity) || humidity < 0.0f || humidity > 100.0f) { - ESP_LOGW(TAG, "Compensation not possible yet: bad humidity data."); + ESP_LOGW(TAG, "Compensation not possible yet: bad humidity data"); return; } else { ESP_LOGD(TAG, "External compensation data received: Humidity %0.2f%%", humidity); @@ -174,7 +175,7 @@ void SGP30Component::send_env_data_() { temperature = float(this->temperature_sensor_->state); } if (std::isnan(temperature) || temperature < -40.0f || temperature > 85.0f) { - ESP_LOGW(TAG, "Compensation not possible yet: bad temperature value data."); + ESP_LOGW(TAG, "Compensation not possible yet: bad temperature value"); return; } else { ESP_LOGD(TAG, "External compensation data received: Temperature %0.2f°C", temperature); @@ -192,18 +193,17 @@ void SGP30Component::send_env_data_() { ((humidity * 0.061121f * std::exp((18.678f - temperature / 234.5f) * (temperature / (257.14f + temperature)))) / (273.15f + temperature)); } - uint8_t humidity_full = uint8_t(std::floor(absolute_humidity)); - uint8_t humidity_dec = uint8_t(std::floor((absolute_humidity - std::floor(absolute_humidity)) * 256)); - ESP_LOGD(TAG, "Calculated Absolute humidity: %0.3f g/m³ (0x%04X)", absolute_humidity, - uint16_t(uint16_t(humidity_full) << 8 | uint16_t(humidity_dec))); - uint8_t crc = sht_crc_(humidity_full, humidity_dec); - uint8_t data[4]; - data[0] = SGP30_CMD_SET_ABSOLUTE_HUMIDITY & 0xFF; - data[1] = humidity_full; - data[2] = humidity_dec; - data[3] = crc; + uint8_t data[4] = { + SGP30_CMD_SET_ABSOLUTE_HUMIDITY & 0xFF, + uint8_t(std::floor(absolute_humidity)), // humidity_full + uint8_t(std::floor((absolute_humidity - std::floor(absolute_humidity)) * 256)), // humidity_dec + 0, + }; + data[3] = crc8(&data[1], 2, 0xFF, sensirion_common::CRC_POLYNOMIAL, true); + ESP_LOGD(TAG, "Calculated absolute humidity: %0.3f g/m³ (0x%04X)", absolute_humidity, + encode_uint16(data[1], data[2])); if (!this->write_bytes(SGP30_CMD_SET_ABSOLUTE_HUMIDITY >> 8, data, 4)) { - ESP_LOGE(TAG, "Error sending compensation data."); + ESP_LOGE(TAG, "Error sending compensation data"); } } @@ -212,15 +212,14 @@ void SGP30Component::write_iaq_baseline_(uint16_t eco2_baseline, uint16_t tvoc_b data[0] = SGP30_CMD_SET_IAQ_BASELINE & 0xFF; data[1] = tvoc_baseline >> 8; data[2] = tvoc_baseline & 0xFF; - data[3] = sht_crc_(data[1], data[2]); + data[3] = crc8(&data[1], 2, 0xFF, sensirion_common::CRC_POLYNOMIAL, true); data[4] = eco2_baseline >> 8; data[5] = eco2_baseline & 0xFF; - data[6] = sht_crc_(data[4], data[5]); + data[6] = crc8(&data[4], 2, 0xFF, sensirion_common::CRC_POLYNOMIAL, true); if (!this->write_bytes(SGP30_CMD_SET_IAQ_BASELINE >> 8, data, 7)) { - ESP_LOGE(TAG, "Error applying eCO2 baseline: 0x%04X, TVOC baseline: 0x%04X", eco2_baseline, tvoc_baseline); + ESP_LOGE(TAG, "Error applying baselines: eCO2: 0x%04X, TVOC: 0x%04X", eco2_baseline, tvoc_baseline); } else { - ESP_LOGI(TAG, "Initial baselines applied successfully! eCO2 baseline: 0x%04X, TVOC baseline: 0x%04X", eco2_baseline, - tvoc_baseline); + ESP_LOGI(TAG, "Initial baselines applied: eCO2: 0x%04X, TVOC: 0x%04X", eco2_baseline, tvoc_baseline); } } @@ -236,10 +235,10 @@ void SGP30Component::dump_config() { ESP_LOGW(TAG, "Measurement Initialization failed"); break; case INVALID_ID: - ESP_LOGW(TAG, "Sensor reported an invalid ID. Is this an SGP30?"); + ESP_LOGW(TAG, "Invalid ID"); break; case UNSUPPORTED_ID: - ESP_LOGW(TAG, "Sensor reported an unsupported ID (SGPC3)"); + ESP_LOGW(TAG, "Unsupported ID"); break; default: ESP_LOGW(TAG, "Unknown setup error"); @@ -249,12 +248,12 @@ void SGP30Component::dump_config() { ESP_LOGCONFIG(TAG, " Serial number: %" PRIu64, this->serial_number_); if (this->eco2_baseline_ != 0x0000 && this->tvoc_baseline_ != 0x0000) { ESP_LOGCONFIG(TAG, - " Baseline:\n" - " eCO2 Baseline: 0x%04X\n" - " TVOC Baseline: 0x%04X", + " Baselines:\n" + " eCO2: 0x%04X\n" + " TVOC: 0x%04X", this->eco2_baseline_, this->tvoc_baseline_); } else { - ESP_LOGCONFIG(TAG, " Baseline: No baseline configured"); + ESP_LOGCONFIG(TAG, " Baselines not configured"); } ESP_LOGCONFIG(TAG, " Warm up time: %" PRIu32 "s", this->required_warm_up_time_); } @@ -266,8 +265,8 @@ void SGP30Component::dump_config() { ESP_LOGCONFIG(TAG, "Store baseline: %s", YESNO(this->store_baseline_)); if (this->humidity_sensor_ != nullptr && this->temperature_sensor_ != nullptr) { ESP_LOGCONFIG(TAG, " Compensation:"); - LOG_SENSOR(" ", "Temperature Source:", this->temperature_sensor_); - LOG_SENSOR(" ", "Humidity Source:", this->humidity_sensor_); + LOG_SENSOR(" ", "Temperature source:", this->temperature_sensor_); + LOG_SENSOR(" ", "Humidity source:", this->humidity_sensor_); } else { ESP_LOGCONFIG(TAG, " Compensation: No source configured"); } @@ -289,7 +288,7 @@ void SGP30Component::update() { float eco2 = (raw_data[0]); float tvoc = (raw_data[1]); - ESP_LOGD(TAG, "Got eCO2=%.1fppm TVOC=%.1fppb", eco2, tvoc); + ESP_LOGV(TAG, "eCO2=%.1fppm TVOC=%.1fppb", eco2, tvoc); if (this->eco2_sensor_ != nullptr) this->eco2_sensor_->publish_state(eco2); if (this->tvoc_sensor_ != nullptr) diff --git a/esphome/components/sgp30/sgp30.h b/esphome/components/sgp30/sgp30.h index e6429a7bfa..4648a33e15 100644 --- a/esphome/components/sgp30/sgp30.h +++ b/esphome/components/sgp30/sgp30.h @@ -1,8 +1,8 @@ #pragma once -#include "esphome/core/component.h" -#include "esphome/components/sensor/sensor.h" #include "esphome/components/sensirion_common/i2c_sensirion.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/core/component.h" #include "esphome/core/preferences.h" #include @@ -38,14 +38,16 @@ class SGP30Component : public PollingComponent, public sensirion_common::Sensiri void read_iaq_baseline_(); bool is_sensor_baseline_reliable_(); void write_iaq_baseline_(uint16_t eco2_baseline, uint16_t tvoc_baseline); + uint64_t serial_number_; - uint16_t featureset_; uint32_t required_warm_up_time_; uint32_t seconds_since_last_store_; - SGP30Baselines baselines_storage_; - ESPPreferenceObject pref_; + uint16_t featureset_; + uint16_t eco2_baseline_{0x0000}; + uint16_t tvoc_baseline_{0x0000}; + bool store_baseline_; - enum ErrorCode { + enum ErrorCode : uint8_t { COMMUNICATION_FAILED, MEASUREMENT_INIT_FAILED, INVALID_ID, @@ -53,14 +55,13 @@ class SGP30Component : public PollingComponent, public sensirion_common::Sensiri UNKNOWN } error_code_{UNKNOWN}; + ESPPreferenceObject pref_; + SGP30Baselines baselines_storage_; + sensor::Sensor *eco2_sensor_{nullptr}; sensor::Sensor *tvoc_sensor_{nullptr}; sensor::Sensor *eco2_sensor_baseline_{nullptr}; sensor::Sensor *tvoc_sensor_baseline_{nullptr}; - uint16_t eco2_baseline_{0x0000}; - uint16_t tvoc_baseline_{0x0000}; - bool store_baseline_; - /// Input sensor for humidity and temperature compensation. sensor::Sensor *humidity_sensor_{nullptr}; sensor::Sensor *temperature_sensor_{nullptr}; From a49669ee58036bceb4a8e0b0b82a646777858e21 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Fri, 5 Sep 2025 17:17:20 -0500 Subject: [PATCH 150/208] [sensirion_common] Tidy up, optimize (#10604) --- .../sensirion_common/i2c_sensirion.cpp | 65 ++++--------- .../sensirion_common/i2c_sensirion.h | 92 ++++++++----------- 2 files changed, 55 insertions(+), 102 deletions(-) diff --git a/esphome/components/sensirion_common/i2c_sensirion.cpp b/esphome/components/sensirion_common/i2c_sensirion.cpp index f71b3c14cb..22c4b0e53c 100644 --- a/esphome/components/sensirion_common/i2c_sensirion.cpp +++ b/esphome/components/sensirion_common/i2c_sensirion.cpp @@ -11,21 +11,22 @@ static const char *const TAG = "sensirion_i2c"; // To avoid memory allocations for small writes a stack buffer is used static const size_t BUFFER_STACK_SIZE = 16; -bool SensirionI2CDevice::read_data(uint16_t *data, uint8_t len) { +bool SensirionI2CDevice::read_data(uint16_t *data, const uint8_t len) { const uint8_t num_bytes = len * 3; - std::vector buf(num_bytes); + uint8_t buf[num_bytes]; - last_error_ = this->read(buf.data(), num_bytes); - if (last_error_ != i2c::ERROR_OK) { + this->last_error_ = this->read(buf, num_bytes); + if (this->last_error_ != i2c::ERROR_OK) { return false; } for (uint8_t i = 0; i < len; i++) { const uint8_t j = 3 * i; - uint8_t crc = sht_crc_(buf[j], buf[j + 1]); + // Use MSB first since Sensirion devices use CRC-8 with MSB first + uint8_t crc = crc8(&buf[j], 2, 0xFF, CRC_POLYNOMIAL, true); if (crc != buf[j + 2]) { - ESP_LOGE(TAG, "CRC8 Checksum invalid at pos %d! 0x%02X != 0x%02X", i, buf[j + 2], crc); - last_error_ = i2c::ERROR_CRC; + ESP_LOGE(TAG, "CRC invalid @ %d! 0x%02X != 0x%02X", i, buf[j + 2], crc); + this->last_error_ = i2c::ERROR_CRC; return false; } data[i] = encode_uint16(buf[j], buf[j + 1]); @@ -34,10 +35,10 @@ bool SensirionI2CDevice::read_data(uint16_t *data, uint8_t len) { } /*** * write command with parameters and insert crc - * use stack array for less than 4 parameters. Most sensirion i2c commands have less parameters + * use stack array for less than 4 parameters. Most Sensirion I2C commands have less parameters */ bool SensirionI2CDevice::write_command_(uint16_t command, CommandLen command_len, const uint16_t *data, - uint8_t data_len) { + const uint8_t data_len) { uint8_t temp_stack[BUFFER_STACK_SIZE]; std::unique_ptr temp_heap; uint8_t *temp; @@ -74,56 +75,26 @@ bool SensirionI2CDevice::write_command_(uint16_t command, CommandLen command_len temp[raw_idx++] = data[i] & 0xFF; temp[raw_idx++] = data[i] >> 8; #endif - temp[raw_idx++] = sht_crc_(data[i]); + // Use MSB first since Sensirion devices use CRC-8 with MSB first + temp[raw_idx++] = crc8(&temp[raw_idx - 2], 2, 0xFF, CRC_POLYNOMIAL, true); } - last_error_ = this->write(temp, raw_idx); - return last_error_ == i2c::ERROR_OK; + this->last_error_ = this->write(temp, raw_idx); + return this->last_error_ == i2c::ERROR_OK; } -bool SensirionI2CDevice::get_register_(uint16_t reg, CommandLen command_len, uint16_t *data, uint8_t len, - uint8_t delay_ms) { +bool SensirionI2CDevice::get_register_(uint16_t reg, CommandLen command_len, uint16_t *data, const uint8_t len, + const uint8_t delay_ms) { if (!this->write_command_(reg, command_len, nullptr, 0)) { - ESP_LOGE(TAG, "Failed to write i2c register=0x%X (%d) err=%d,", reg, command_len, this->last_error_); + ESP_LOGE(TAG, "Write failed: reg=0x%X (%d) err=%d,", reg, command_len, this->last_error_); return false; } delay(delay_ms); bool result = this->read_data(data, len); if (!result) { - ESP_LOGE(TAG, "Failed to read data from register=0x%X err=%d,", reg, this->last_error_); + ESP_LOGE(TAG, "Read failed: reg=0x%X err=%d,", reg, this->last_error_); } return result; } -// The 8-bit CRC checksum is transmitted after each data word -uint8_t SensirionI2CDevice::sht_crc_(uint16_t data) { - uint8_t bit; - uint8_t crc = 0xFF; -#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ - crc ^= data >> 8; -#else - crc ^= data & 0xFF; -#endif - for (bit = 8; bit > 0; --bit) { - if (crc & 0x80) { - crc = (crc << 1) ^ crc_polynomial_; - } else { - crc = (crc << 1); - } - } -#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ - crc ^= data & 0xFF; -#else - crc ^= data >> 8; -#endif - for (bit = 8; bit > 0; --bit) { - if (crc & 0x80) { - crc = (crc << 1) ^ crc_polynomial_; - } else { - crc = (crc << 1); - } - } - return crc; -} - } // namespace sensirion_common } // namespace esphome diff --git a/esphome/components/sensirion_common/i2c_sensirion.h b/esphome/components/sensirion_common/i2c_sensirion.h index e141591525..f3eb3761f6 100644 --- a/esphome/components/sensirion_common/i2c_sensirion.h +++ b/esphome/components/sensirion_common/i2c_sensirion.h @@ -8,10 +8,10 @@ namespace esphome { namespace sensirion_common { /** - * Implementation of a i2c functions for Sensirion sensors - * Sensirion data requires crc checking. + * Implementation of I2C functions for Sensirion sensors + * Sensirion data requires CRC checking. * Each 16 bit word is/must be followed 8 bit CRC code - * (Applies to read and write - note the i2c command code doesn't need a CRC) + * (Applies to read and write - note the I2C command code doesn't need a CRC) * Format: * | 16 Bit Command Code | 16 bit Data word 1 | CRC of DW 1 | 16 bit Data word 1 | CRC of DW 2 | .. */ @@ -21,79 +21,79 @@ class SensirionI2CDevice : public i2c::I2CDevice { public: enum CommandLen : uint8_t { ADDR_8_BIT = 1, ADDR_16_BIT = 2 }; - /** Read data words from i2c device. - * handles crc check used by Sensirion sensors + /** Read data words from I2C device. + * handles CRC check used by Sensirion sensors * @param data pointer to raw result * @param len number of words to read * @return true if reading succeeded */ bool read_data(uint16_t *data, uint8_t len); - /** Read 1 data word from i2c device. + /** Read 1 data word from I2C device. * @param data reference to raw result * @return true if reading succeeded */ bool read_data(uint16_t &data) { return this->read_data(&data, 1); } - /** get data words from i2c register. - * handles crc check used by Sensirion sensors - * @param i2c register + /** get data words from I2C register. + * handles CRC check used by Sensirion sensors + * @param I2C register * @param data pointer to raw result * @param len number of words to read - * @param delay milliseconds to to wait between sending the i2c command and reading the result + * @param delay milliseconds to to wait between sending the I2C command and reading the result * @return true if reading succeeded */ bool get_register(uint16_t command, uint16_t *data, uint8_t len, uint8_t delay = 0) { return get_register_(command, ADDR_16_BIT, data, len, delay); } - /** Read 1 data word from 16 bit i2c register. - * @param i2c register + /** Read 1 data word from 16 bit I2C register. + * @param I2C register * @param data reference to raw result - * @param delay milliseconds to to wait between sending the i2c command and reading the result + * @param delay milliseconds to to wait between sending the I2C command and reading the result * @return true if reading succeeded */ bool get_register(uint16_t i2c_register, uint16_t &data, uint8_t delay = 0) { return this->get_register_(i2c_register, ADDR_16_BIT, &data, 1, delay); } - /** get data words from i2c register. - * handles crc check used by Sensirion sensors - * @param i2c register + /** get data words from I2C register. + * handles CRC check used by Sensirion sensors + * @param I2C register * @param data pointer to raw result * @param len number of words to read - * @param delay milliseconds to to wait between sending the i2c command and reading the result + * @param delay milliseconds to to wait between sending the I2C command and reading the result * @return true if reading succeeded */ bool get_8bit_register(uint8_t i2c_register, uint16_t *data, uint8_t len, uint8_t delay = 0) { return get_register_(i2c_register, ADDR_8_BIT, data, len, delay); } - /** Read 1 data word from 8 bit i2c register. - * @param i2c register + /** Read 1 data word from 8 bit I2C register. + * @param I2C register * @param data reference to raw result - * @param delay milliseconds to to wait between sending the i2c command and reading the result + * @param delay milliseconds to to wait between sending the I2C command and reading the result * @return true if reading succeeded */ bool get_8bit_register(uint8_t i2c_register, uint16_t &data, uint8_t delay = 0) { return this->get_register_(i2c_register, ADDR_8_BIT, &data, 1, delay); } - /** Write a command to the i2c device. - * @param command i2c command to send + /** Write a command to the I2C device. + * @param command I2C command to send * @return true if reading succeeded */ template bool write_command(T i2c_register) { return write_command(i2c_register, nullptr, 0); } - /** Write a command and one data word to the i2c device . - * @param command i2c command to send - * @param data argument for the i2c command + /** Write a command and one data word to the I2C device . + * @param command I2C command to send + * @param data argument for the I2C command * @return true if reading succeeded */ template bool write_command(T i2c_register, uint16_t data) { return write_command(i2c_register, &data, 1); } /** Write a command with arguments as words - * @param i2c_register i2c command to send - an be uint8_t or uint16_t - * @param data vector arguments for the i2c command + * @param i2c_register I2C command to send - an be uint8_t or uint16_t + * @param data vector arguments for the I2C command * @return true if reading succeeded */ template bool write_command(T i2c_register, const std::vector &data) { @@ -101,57 +101,39 @@ class SensirionI2CDevice : public i2c::I2CDevice { } /** Write a command with arguments as words - * @param i2c_register i2c command to send - an be uint8_t or uint16_t - * @param data arguments for the i2c command + * @param i2c_register I2C command to send - an be uint8_t or uint16_t + * @param data arguments for the I2C command * @param len number of arguments (words) * @return true if reading succeeded */ template bool write_command(T i2c_register, const uint16_t *data, uint8_t len) { // limit to 8 or 16 bit only - static_assert(sizeof(i2c_register) == 1 || sizeof(i2c_register) == 2, - "only 8 or 16 bit command types are supported."); + static_assert(sizeof(i2c_register) == 1 || sizeof(i2c_register) == 2, "Only 8 or 16 bit command types supported"); return write_command_(i2c_register, CommandLen(sizeof(T)), data, len); } protected: - uint8_t crc_polynomial_{0x31u}; // default for sensirion /** Write a command with arguments as words - * @param command i2c command to send can be uint8_t or uint16_t + * @param command I2C command to send can be uint8_t or uint16_t * @param command_len either 1 for short 8 bit command or 2 for 16 bit command codes - * @param data arguments for the i2c command + * @param data arguments for the I2C command * @param data_len number of arguments (words) * @return true if reading succeeded */ bool write_command_(uint16_t command, CommandLen command_len, const uint16_t *data, uint8_t data_len); - /** get data words from i2c register. - * handles crc check used by Sensirion sensors - * @param i2c register + /** get data words from I2C register. + * handles CRC check used by Sensirion sensors + * @param I2C register * @param command_len either 1 for short 8 bit command or 2 for 16 bit command codes * @param data pointer to raw result * @param len number of words to read - * @param delay milliseconds to to wait between sending the i2c command and reading the result + * @param delay milliseconds to to wait between sending the I2C command and reading the result * @return true if reading succeeded */ bool get_register_(uint16_t reg, CommandLen command_len, uint16_t *data, uint8_t len, uint8_t delay); - /** 8-bit CRC checksum that is transmitted after each data word for read and write operation - * @param command i2c command to send - * @param data data word for which the crc8 checksum is calculated - * @param len number of arguments (words) - * @return 8 Bit CRC - */ - uint8_t sht_crc_(uint16_t data); - - /** 8-bit CRC checksum that is transmitted after each data word for read and write operation - * @param command i2c command to send - * @param data1 high byte of data word - * @param data2 low byte of data word - * @return 8 Bit CRC - */ - uint8_t sht_crc_(uint8_t data1, uint8_t data2) { return sht_crc_(encode_uint16(data1, data2)); } - - /** last error code from i2c operation + /** last error code from I2C operation */ i2c::ErrorCode last_error_; }; From 1510db277c66c1e8999252d0468445d20ad8c00f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 5 Sep 2025 17:44:23 -0500 Subject: [PATCH 151/208] [esphome] ESP8266: Move OTA error strings to PROGMEM (saves 116 bytes RAM) (#10620) --- .../components/esphome/ota/ota_esphome.cpp | 40 ++++++++++--------- esphome/components/esphome/ota/ota_esphome.h | 7 ++-- 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index fc10e5366e..6654ef8748 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -30,19 +30,19 @@ void ESPHomeOTAComponent::setup() { this->server_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0); // monitored for incoming connections if (this->server_ == nullptr) { - this->log_socket_error_("creation"); + this->log_socket_error_(LOG_STR("creation")); this->mark_failed(); return; } int enable = 1; int err = this->server_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)); if (err != 0) { - this->log_socket_error_("reuseaddr"); + this->log_socket_error_(LOG_STR("reuseaddr")); // we can still continue } err = this->server_->setblocking(false); if (err != 0) { - this->log_socket_error_("non-blocking"); + this->log_socket_error_(LOG_STR("non-blocking")); this->mark_failed(); return; } @@ -51,21 +51,21 @@ void ESPHomeOTAComponent::setup() { socklen_t sl = socket::set_sockaddr_any((struct sockaddr *) &server, sizeof(server), this->port_); if (sl == 0) { - this->log_socket_error_("set sockaddr"); + this->log_socket_error_(LOG_STR("set sockaddr")); this->mark_failed(); return; } err = this->server_->bind((struct sockaddr *) &server, sizeof(server)); if (err != 0) { - this->log_socket_error_("bind"); + this->log_socket_error_(LOG_STR("bind")); this->mark_failed(); return; } err = this->server_->listen(4); if (err != 0) { - this->log_socket_error_("listen"); + this->log_socket_error_(LOG_STR("listen")); this->mark_failed(); return; } @@ -114,17 +114,17 @@ void ESPHomeOTAComponent::handle_handshake_() { return; int err = this->client_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int)); if (err != 0) { - this->log_socket_error_("nodelay"); + this->log_socket_error_(LOG_STR("nodelay")); this->cleanup_connection_(); return; } err = this->client_->setblocking(false); if (err != 0) { - this->log_socket_error_("non-blocking"); + this->log_socket_error_(LOG_STR("non-blocking")); this->cleanup_connection_(); return; } - this->log_start_("handshake"); + this->log_start_(LOG_STR("handshake")); this->client_connect_time_ = App.get_loop_component_start_time(); this->magic_buf_pos_ = 0; // Reset magic buffer position } @@ -150,7 +150,7 @@ void ESPHomeOTAComponent::handle_handshake_() { if (read <= 0) { // Error or connection closed if (read == -1) { - this->log_socket_error_("reading magic bytes"); + this->log_socket_error_(LOG_STR("reading magic bytes")); } else { ESP_LOGW(TAG, "Remote closed during handshake"); } @@ -209,7 +209,7 @@ void ESPHomeOTAComponent::handle_data_() { // Read features - 1 byte if (!this->readall_(buf, 1)) { - this->log_read_error_("features"); + this->log_read_error_(LOG_STR("features")); goto error; // NOLINT(cppcoreguidelines-avoid-goto) } ota_features = buf[0]; // NOLINT @@ -288,7 +288,7 @@ void ESPHomeOTAComponent::handle_data_() { // Read size, 4 bytes MSB first if (!this->readall_(buf, 4)) { - this->log_read_error_("size"); + this->log_read_error_(LOG_STR("size")); goto error; // NOLINT(cppcoreguidelines-avoid-goto) } ota_size = 0; @@ -302,7 +302,7 @@ void ESPHomeOTAComponent::handle_data_() { // starting the update, set the warning status and notify // listeners. This ensures that port scanners do not // accidentally trigger the update process. - this->log_start_("update"); + this->log_start_(LOG_STR("update")); this->status_set_warning(); #ifdef USE_OTA_STATE_CALLBACK this->state_callback_.call(ota::OTA_STARTED, 0.0f, 0); @@ -320,7 +320,7 @@ void ESPHomeOTAComponent::handle_data_() { // Read binary MD5, 32 bytes if (!this->readall_(buf, 32)) { - this->log_read_error_("MD5 checksum"); + this->log_read_error_(LOG_STR("MD5 checksum")); goto error; // NOLINT(cppcoreguidelines-avoid-goto) } sbuf[32] = '\0'; @@ -393,7 +393,7 @@ void ESPHomeOTAComponent::handle_data_() { // Read ACK if (!this->readall_(buf, 1) || buf[0] != ota::OTA_RESPONSE_OK) { - this->log_read_error_("ack"); + this->log_read_error_(LOG_STR("ack")); // do not go to error, this is not fatal } @@ -477,12 +477,14 @@ float ESPHomeOTAComponent::get_setup_priority() const { return setup_priority::A uint16_t ESPHomeOTAComponent::get_port() const { return this->port_; } void ESPHomeOTAComponent::set_port(uint16_t port) { this->port_ = port; } -void ESPHomeOTAComponent::log_socket_error_(const char *msg) { ESP_LOGW(TAG, "Socket %s: errno %d", msg, errno); } +void ESPHomeOTAComponent::log_socket_error_(const LogString *msg) { + ESP_LOGW(TAG, "Socket %s: errno %d", LOG_STR_ARG(msg), errno); +} -void ESPHomeOTAComponent::log_read_error_(const char *what) { ESP_LOGW(TAG, "Read %s failed", what); } +void ESPHomeOTAComponent::log_read_error_(const LogString *what) { ESP_LOGW(TAG, "Read %s failed", LOG_STR_ARG(what)); } -void ESPHomeOTAComponent::log_start_(const char *phase) { - ESP_LOGD(TAG, "Starting %s from %s", phase, this->client_->getpeername().c_str()); +void ESPHomeOTAComponent::log_start_(const LogString *phase) { + ESP_LOGD(TAG, "Starting %s from %s", LOG_STR_ARG(phase), this->client_->getpeername().c_str()); } void ESPHomeOTAComponent::cleanup_connection_() { diff --git a/esphome/components/esphome/ota/ota_esphome.h b/esphome/components/esphome/ota/ota_esphome.h index c1919c71e9..3a5d9f4f7a 100644 --- a/esphome/components/esphome/ota/ota_esphome.h +++ b/esphome/components/esphome/ota/ota_esphome.h @@ -3,6 +3,7 @@ #include "esphome/core/defines.h" #ifdef USE_OTA #include "esphome/core/helpers.h" +#include "esphome/core/log.h" #include "esphome/core/preferences.h" #include "esphome/components/ota/ota_backend.h" #include "esphome/components/socket/socket.h" @@ -31,9 +32,9 @@ class ESPHomeOTAComponent : public ota::OTAComponent { void handle_data_(); bool readall_(uint8_t *buf, size_t len); bool writeall_(const uint8_t *buf, size_t len); - void log_socket_error_(const char *msg); - void log_read_error_(const char *what); - void log_start_(const char *phase); + void log_socket_error_(const LogString *msg); + void log_read_error_(const LogString *what); + void log_start_(const LogString *phase); void cleanup_connection_(); void yield_and_feed_watchdog_(); From 1340665ac7d0f44698c061dcb4b880bdd97f12cc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 5 Sep 2025 17:47:53 -0500 Subject: [PATCH 152/208] [logger] Use LogString for UART selection strings (saves 28 bytes RAM on ESP8266) (#10615) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/logger/logger.cpp | 2 +- esphome/components/logger/logger.h | 2 +- esphome/components/logger/logger_esp32.cpp | 24 ++++++++++++------- esphome/components/logger/logger_esp8266.cpp | 14 ++++++++--- .../components/logger/logger_libretiny.cpp | 16 ++++++++++--- esphome/components/logger/logger_rp2040.cpp | 17 ++++++++++--- esphome/components/logger/logger_zephyr.cpp | 19 +++++++++++---- 7 files changed, 71 insertions(+), 23 deletions(-) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 195e04948d..0ade9cedae 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -258,7 +258,7 @@ void Logger::dump_config() { ESP_LOGCONFIG(TAG, " Log Baud Rate: %" PRIu32 "\n" " Hardware UART: %s", - this->baud_rate_, get_uart_selection_()); + this->baud_rate_, LOG_STR_ARG(get_uart_selection_())); #endif #ifdef USE_ESPHOME_TASK_LOG_BUFFER if (this->log_buffer_) { diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index aa76a188c9..a4cf5e3004 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -226,7 +226,7 @@ class Logger : public Component { } #ifndef USE_HOST - const char *get_uart_selection_(); + const LogString *get_uart_selection_(); #endif // Group 4-byte aligned members first diff --git a/esphome/components/logger/logger_esp32.cpp b/esphome/components/logger/logger_esp32.cpp index 44243d4aa8..6cb57c1540 100644 --- a/esphome/components/logger/logger_esp32.cpp +++ b/esphome/components/logger/logger_esp32.cpp @@ -190,20 +190,28 @@ void HOT Logger::write_msg_(const char *msg) { void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); } #endif -const char *const UART_SELECTIONS[] = { - "UART0", "UART1", +const LogString *Logger::get_uart_selection_() { + switch (this->uart_) { + case UART_SELECTION_UART0: + return LOG_STR("UART0"); + case UART_SELECTION_UART1: + return LOG_STR("UART1"); #ifdef USE_ESP32_VARIANT_ESP32 - "UART2", + case UART_SELECTION_UART2: + return LOG_STR("UART2"); #endif #ifdef USE_LOGGER_USB_CDC - "USB_CDC", + case UART_SELECTION_USB_CDC: + return LOG_STR("USB_CDC"); #endif #ifdef USE_LOGGER_USB_SERIAL_JTAG - "USB_SERIAL_JTAG", + case UART_SELECTION_USB_SERIAL_JTAG: + return LOG_STR("USB_SERIAL_JTAG"); #endif -}; - -const char *Logger::get_uart_selection_() { return UART_SELECTIONS[this->uart_]; } + default: + return LOG_STR("UNKNOWN"); + } +} } // namespace esphome::logger #endif diff --git a/esphome/components/logger/logger_esp8266.cpp b/esphome/components/logger/logger_esp8266.cpp index fb5f6cee5d..5063d88b92 100644 --- a/esphome/components/logger/logger_esp8266.cpp +++ b/esphome/components/logger/logger_esp8266.cpp @@ -35,9 +35,17 @@ void Logger::pre_setup() { void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); } -const char *const UART_SELECTIONS[] = {"UART0", "UART1", "UART0_SWAP"}; - -const char *Logger::get_uart_selection_() { return UART_SELECTIONS[this->uart_]; } +const LogString *Logger::get_uart_selection_() { + switch (this->uart_) { + case UART_SELECTION_UART0: + return LOG_STR("UART0"); + case UART_SELECTION_UART1: + return LOG_STR("UART1"); + case UART_SELECTION_UART0_SWAP: + default: + return LOG_STR("UART0_SWAP"); + } +} } // namespace esphome::logger #endif diff --git a/esphome/components/logger/logger_libretiny.cpp b/esphome/components/logger/logger_libretiny.cpp index 09d0622bc3..3edfa74480 100644 --- a/esphome/components/logger/logger_libretiny.cpp +++ b/esphome/components/logger/logger_libretiny.cpp @@ -51,9 +51,19 @@ void Logger::pre_setup() { void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); } -const char *const UART_SELECTIONS[] = {"DEFAULT", "UART0", "UART1", "UART2"}; - -const char *Logger::get_uart_selection_() { return UART_SELECTIONS[this->uart_]; } +const LogString *Logger::get_uart_selection_() { + switch (this->uart_) { + case UART_SELECTION_DEFAULT: + return LOG_STR("DEFAULT"); + case UART_SELECTION_UART0: + return LOG_STR("UART0"); + case UART_SELECTION_UART1: + return LOG_STR("UART1"); + case UART_SELECTION_UART2: + default: + return LOG_STR("UART2"); + } +} } // namespace esphome::logger diff --git a/esphome/components/logger/logger_rp2040.cpp b/esphome/components/logger/logger_rp2040.cpp index f1cad9b283..63727c2cda 100644 --- a/esphome/components/logger/logger_rp2040.cpp +++ b/esphome/components/logger/logger_rp2040.cpp @@ -29,9 +29,20 @@ void Logger::pre_setup() { void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); } -const char *const UART_SELECTIONS[] = {"UART0", "UART1", "USB_CDC"}; - -const char *Logger::get_uart_selection_() { return UART_SELECTIONS[this->uart_]; } +const LogString *Logger::get_uart_selection_() { + switch (this->uart_) { + case UART_SELECTION_UART0: + return LOG_STR("UART0"); + case UART_SELECTION_UART1: + return LOG_STR("UART1"); +#ifdef USE_LOGGER_USB_CDC + case UART_SELECTION_USB_CDC: + return LOG_STR("USB_CDC"); +#endif + default: + return LOG_STR("UNKNOWN"); + } +} } // namespace esphome::logger #endif // USE_RP2040 diff --git a/esphome/components/logger/logger_zephyr.cpp b/esphome/components/logger/logger_zephyr.cpp index 58a09facd5..817ca168f8 100644 --- a/esphome/components/logger/logger_zephyr.cpp +++ b/esphome/components/logger/logger_zephyr.cpp @@ -54,7 +54,7 @@ void Logger::pre_setup() { #endif } if (!device_is_ready(uart_dev)) { - ESP_LOGE(TAG, "%s is not ready.", get_uart_selection_()); + ESP_LOGE(TAG, "%s is not ready.", LOG_STR_ARG(get_uart_selection_())); } else { this->uart_dev_ = uart_dev; } @@ -77,9 +77,20 @@ void HOT Logger::write_msg_(const char *msg) { uart_poll_out(this->uart_dev_, '\n'); } -const char *const UART_SELECTIONS[] = {"UART0", "UART1", "USB_CDC"}; - -const char *Logger::get_uart_selection_() { return UART_SELECTIONS[this->uart_]; } +const LogString *Logger::get_uart_selection_() { + switch (this->uart_) { + case UART_SELECTION_UART0: + return LOG_STR("UART0"); + case UART_SELECTION_UART1: + return LOG_STR("UART1"); +#ifdef USE_LOGGER_USB_CDC + case UART_SELECTION_USB_CDC: + return LOG_STR("USB_CDC"); +#endif + default: + return LOG_STR("UNKNOWN"); + } +} } // namespace esphome::logger From 5b283d6d3882e45c3636cfbbc3051179d32ed1bc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 5 Sep 2025 17:51:35 -0500 Subject: [PATCH 153/208] [sensor] ESP8266: Use LogString for state_class_to_string() to save RAM (#10617) --- esphome/components/mqtt/mqtt_sensor.cpp | 9 +++++++-- esphome/components/sensor/sensor.cpp | 13 +++++++------ esphome/components/sensor/sensor.h | 2 +- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/esphome/components/mqtt/mqtt_sensor.cpp b/esphome/components/mqtt/mqtt_sensor.cpp index 2e1db1908f..9e61f6ef3b 100644 --- a/esphome/components/mqtt/mqtt_sensor.cpp +++ b/esphome/components/mqtt/mqtt_sensor.cpp @@ -58,8 +58,13 @@ void MQTTSensorComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCon if (this->sensor_->get_force_update()) root[MQTT_FORCE_UPDATE] = true; - if (this->sensor_->get_state_class() != STATE_CLASS_NONE) - root[MQTT_STATE_CLASS] = state_class_to_string(this->sensor_->get_state_class()); + if (this->sensor_->get_state_class() != STATE_CLASS_NONE) { +#ifdef USE_STORE_LOG_STR_IN_FLASH + root[MQTT_STATE_CLASS] = (const __FlashStringHelper *) state_class_to_string(this->sensor_->get_state_class()); +#else + root[MQTT_STATE_CLASS] = LOG_STR_ARG(state_class_to_string(this->sensor_->get_state_class())); +#endif + } config.command_topic = false; } diff --git a/esphome/components/sensor/sensor.cpp b/esphome/components/sensor/sensor.cpp index e2e8302d8b..4292b8c0bc 100644 --- a/esphome/components/sensor/sensor.cpp +++ b/esphome/components/sensor/sensor.cpp @@ -17,7 +17,8 @@ void log_sensor(const char *tag, const char *prefix, const char *type, Sensor *o "%s State Class: '%s'\n" "%s Unit of Measurement: '%s'\n" "%s Accuracy Decimals: %d", - prefix, type, obj->get_name().c_str(), prefix, state_class_to_string(obj->get_state_class()), prefix, + prefix, type, obj->get_name().c_str(), prefix, + LOG_STR_ARG(state_class_to_string(obj->get_state_class())), prefix, obj->get_unit_of_measurement_ref().c_str(), prefix, obj->get_accuracy_decimals()); if (!obj->get_device_class_ref().empty()) { @@ -33,17 +34,17 @@ void log_sensor(const char *tag, const char *prefix, const char *type, Sensor *o } } -const char *state_class_to_string(StateClass state_class) { +const LogString *state_class_to_string(StateClass state_class) { switch (state_class) { case STATE_CLASS_MEASUREMENT: - return "measurement"; + return LOG_STR("measurement"); case STATE_CLASS_TOTAL_INCREASING: - return "total_increasing"; + return LOG_STR("total_increasing"); case STATE_CLASS_TOTAL: - return "total"; + return LOG_STR("total"); case STATE_CLASS_NONE: default: - return ""; + return LOG_STR(""); } } diff --git a/esphome/components/sensor/sensor.h b/esphome/components/sensor/sensor.h index 507cb326b2..f3fa601a5e 100644 --- a/esphome/components/sensor/sensor.h +++ b/esphome/components/sensor/sensor.h @@ -33,7 +33,7 @@ enum StateClass : uint8_t { STATE_CLASS_TOTAL = 3, }; -const char *state_class_to_string(StateClass state_class); +const LogString *state_class_to_string(StateClass state_class); /** Base-class for all sensors. * From f1806046a94664b50d8106f4368ab9593b51fd36 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 5 Sep 2025 17:53:23 -0500 Subject: [PATCH 154/208] [web_server] ESP8266: Store OTA response strings in PROGMEM (saves 52 bytes RAM) (#10616) --- esphome/components/web_server/ota/ota_web_server.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/esphome/components/web_server/ota/ota_web_server.cpp b/esphome/components/web_server/ota/ota_web_server.cpp index 7211f707e9..672a9868c5 100644 --- a/esphome/components/web_server/ota/ota_web_server.cpp +++ b/esphome/components/web_server/ota/ota_web_server.cpp @@ -198,9 +198,20 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Strin void OTARequestHandler::handleRequest(AsyncWebServerRequest *request) { AsyncWebServerResponse *response; // Use the ota_success_ flag to determine the actual result +#ifdef USE_ESP8266 + static const char UPDATE_SUCCESS[] PROGMEM = "Update Successful!"; + static const char UPDATE_FAILED[] PROGMEM = "Update Failed!"; + static const char TEXT_PLAIN[] PROGMEM = "text/plain"; + static const char CONNECTION_STR[] PROGMEM = "Connection"; + static const char CLOSE_STR[] PROGMEM = "close"; + const char *msg = this->ota_success_ ? UPDATE_SUCCESS : UPDATE_FAILED; + response = request->beginResponse_P(200, TEXT_PLAIN, msg); + response->addHeader(CONNECTION_STR, CLOSE_STR); +#else const char *msg = this->ota_success_ ? "Update Successful!" : "Update Failed!"; response = request->beginResponse(200, "text/plain", msg); response->addHeader("Connection", "close"); +#endif request->send(response); } From 91b2f75d041cef453ba568a07a05b27c5d2ca27f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 5 Sep 2025 17:56:00 -0500 Subject: [PATCH 155/208] [script] ESP8266: Store log format strings in PROGMEM (saves 240 bytes RAM) (#10614) --- esphome/components/script/script.cpp | 6 ++++++ esphome/components/script/script.h | 24 +++++++++++++++++++----- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/esphome/components/script/script.cpp b/esphome/components/script/script.cpp index 331f7dcd65..81f652d26a 100644 --- a/esphome/components/script/script.cpp +++ b/esphome/components/script/script.cpp @@ -6,9 +6,15 @@ namespace script { static const char *const TAG = "script"; +#ifdef USE_STORE_LOG_STR_IN_FLASH +void ScriptLogger::esp_log_(int level, int line, const __FlashStringHelper *format, const char *param) { + esp_log_printf_(level, TAG, line, format, param); +} +#else void ScriptLogger::esp_log_(int level, int line, const char *format, const char *param) { esp_log_printf_(level, TAG, line, format, param); } +#endif } // namespace script } // namespace esphome diff --git a/esphome/components/script/script.h b/esphome/components/script/script.h index 60175ec933..b16bb53acc 100644 --- a/esphome/components/script/script.h +++ b/esphome/components/script/script.h @@ -10,6 +10,15 @@ namespace script { class ScriptLogger { protected: +#ifdef USE_STORE_LOG_STR_IN_FLASH + void esp_logw_(int line, const __FlashStringHelper *format, const char *param) { + esp_log_(ESPHOME_LOG_LEVEL_WARN, line, format, param); + } + void esp_logd_(int line, const __FlashStringHelper *format, const char *param) { + esp_log_(ESPHOME_LOG_LEVEL_DEBUG, line, format, param); + } + void esp_log_(int level, int line, const __FlashStringHelper *format, const char *param); +#else void esp_logw_(int line, const char *format, const char *param) { esp_log_(ESPHOME_LOG_LEVEL_WARN, line, format, param); } @@ -17,6 +26,7 @@ class ScriptLogger { esp_log_(ESPHOME_LOG_LEVEL_DEBUG, line, format, param); } void esp_log_(int level, int line, const char *format, const char *param); +#endif }; /// The abstract base class for all script types. @@ -57,7 +67,8 @@ template class SingleScript : public Script { public: void execute(Ts... x) override { if (this->is_action_running()) { - this->esp_logw_(__LINE__, "Script '%s' is already running! (mode: single)", this->name_.c_str()); + this->esp_logw_(__LINE__, ESPHOME_LOG_FORMAT("Script '%s' is already running! (mode: single)"), + this->name_.c_str()); return; } @@ -74,7 +85,7 @@ template class RestartScript : public Script { public: void execute(Ts... x) override { if (this->is_action_running()) { - this->esp_logd_(__LINE__, "Script '%s' restarting (mode: restart)", this->name_.c_str()); + this->esp_logd_(__LINE__, ESPHOME_LOG_FORMAT("Script '%s' restarting (mode: restart)"), this->name_.c_str()); this->stop_action(); } @@ -93,11 +104,13 @@ template class QueueingScript : public Script, public Com // num_runs_ is the number of *queued* instances, so total number of instances is // num_runs_ + 1 if (this->max_runs_ != 0 && this->num_runs_ + 1 >= this->max_runs_) { - this->esp_logw_(__LINE__, "Script '%s' maximum number of queued runs exceeded!", this->name_.c_str()); + this->esp_logw_(__LINE__, ESPHOME_LOG_FORMAT("Script '%s' maximum number of queued runs exceeded!"), + this->name_.c_str()); return; } - this->esp_logd_(__LINE__, "Script '%s' queueing new instance (mode: queued)", this->name_.c_str()); + this->esp_logd_(__LINE__, ESPHOME_LOG_FORMAT("Script '%s' queueing new instance (mode: queued)"), + this->name_.c_str()); this->num_runs_++; this->var_queue_.push(std::make_tuple(x...)); return; @@ -143,7 +156,8 @@ template class ParallelScript : public Script { public: void execute(Ts... x) override { if (this->max_runs_ != 0 && this->automation_parent_->num_running() >= this->max_runs_) { - this->esp_logw_(__LINE__, "Script '%s' maximum number of parallel runs exceeded!", this->name_.c_str()); + this->esp_logw_(__LINE__, ESPHOME_LOG_FORMAT("Script '%s' maximum number of parallel runs exceeded!"), + this->name_.c_str()); return; } this->trigger(x...); From 98e8a0c2016c74c913c11fd986658cf1e4ce81d7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 5 Sep 2025 17:57:24 -0500 Subject: [PATCH 156/208] [gpio] ESP8266: Store log strings in flash memory (#10610) --- .../gpio/binary_sensor/gpio_binary_sensor.cpp | 37 ++++++++++--------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp b/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp index 4b8369cd59..45544c185b 100644 --- a/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp +++ b/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp @@ -6,6 +6,23 @@ namespace gpio { static const char *const TAG = "gpio.binary_sensor"; +static const LogString *interrupt_type_to_string(gpio::InterruptType type) { + switch (type) { + case gpio::INTERRUPT_RISING_EDGE: + return LOG_STR("RISING_EDGE"); + case gpio::INTERRUPT_FALLING_EDGE: + return LOG_STR("FALLING_EDGE"); + case gpio::INTERRUPT_ANY_EDGE: + return LOG_STR("ANY_EDGE"); + default: + return LOG_STR("UNKNOWN"); + } +} + +static const LogString *gpio_mode_to_string(bool use_interrupt) { + return use_interrupt ? LOG_STR("interrupt") : LOG_STR("polling"); +} + void IRAM_ATTR GPIOBinarySensorStore::gpio_intr(GPIOBinarySensorStore *arg) { bool new_state = arg->isr_pin_.digital_read(); if (new_state != arg->last_state_) { @@ -51,25 +68,9 @@ void GPIOBinarySensor::setup() { void GPIOBinarySensor::dump_config() { LOG_BINARY_SENSOR("", "GPIO Binary Sensor", this); LOG_PIN(" Pin: ", this->pin_); - const char *mode = this->use_interrupt_ ? "interrupt" : "polling"; - ESP_LOGCONFIG(TAG, " Mode: %s", mode); + ESP_LOGCONFIG(TAG, " Mode: %s", LOG_STR_ARG(gpio_mode_to_string(this->use_interrupt_))); if (this->use_interrupt_) { - const char *interrupt_type; - switch (this->interrupt_type_) { - case gpio::INTERRUPT_RISING_EDGE: - interrupt_type = "RISING_EDGE"; - break; - case gpio::INTERRUPT_FALLING_EDGE: - interrupt_type = "FALLING_EDGE"; - break; - case gpio::INTERRUPT_ANY_EDGE: - interrupt_type = "ANY_EDGE"; - break; - default: - interrupt_type = "UNKNOWN"; - break; - } - ESP_LOGCONFIG(TAG, " Interrupt Type: %s", interrupt_type); + ESP_LOGCONFIG(TAG, " Interrupt Type: %s", LOG_STR_ARG(interrupt_type_to_string(this->interrupt_type_))); } } From b74463c3e6e87a82c815227abc780bb9453479e7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 5 Sep 2025 17:59:24 -0500 Subject: [PATCH 157/208] [light] ESP8266: Store log strings in flash memory (#10611) --- esphome/components/light/light_call.cpp | 42 +++++++++++++------------ 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index 60945531cf..cbe9ed0454 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -11,19 +11,21 @@ static const char *const TAG = "light"; // Helper functions to reduce code size for logging #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_WARN -static void log_validation_warning(const char *name, const char *param_name, float val, float min, float max) { - ESP_LOGW(TAG, "'%s': %s value %.2f is out of range [%.1f - %.1f]", name, param_name, val, min, max); +static void log_validation_warning(const char *name, const LogString *param_name, float val, float min, float max) { + ESP_LOGW(TAG, "'%s': %s value %.2f is out of range [%.1f - %.1f]", name, LOG_STR_ARG(param_name), val, min, max); } -static void log_feature_not_supported(const char *name, const char *feature) { - ESP_LOGW(TAG, "'%s': %s not supported", name, feature); +static void log_feature_not_supported(const char *name, const LogString *feature) { + ESP_LOGW(TAG, "'%s': %s not supported", name, LOG_STR_ARG(feature)); } -static void log_color_mode_not_supported(const char *name, const char *feature) { - ESP_LOGW(TAG, "'%s': color mode does not support setting %s", name, feature); +static void log_color_mode_not_supported(const char *name, const LogString *feature) { + ESP_LOGW(TAG, "'%s': color mode does not support setting %s", name, LOG_STR_ARG(feature)); } -static void log_invalid_parameter(const char *name, const char *message) { ESP_LOGW(TAG, "'%s': %s", name, message); } +static void log_invalid_parameter(const char *name, const LogString *message) { + ESP_LOGW(TAG, "'%s': %s", name, LOG_STR_ARG(message)); +} #else #define log_validation_warning(name, param_name, val, min, max) #define log_feature_not_supported(name, feature) @@ -201,19 +203,19 @@ LightColorValues LightCall::validate_() { // Brightness exists check if (this->has_brightness() && this->brightness_ > 0.0f && !(color_mode & ColorCapability::BRIGHTNESS)) { - log_feature_not_supported(name, "brightness"); + log_feature_not_supported(name, LOG_STR("brightness")); this->set_flag_(FLAG_HAS_BRIGHTNESS, false); } // Transition length possible check if (this->has_transition_() && this->transition_length_ != 0 && !(color_mode & ColorCapability::BRIGHTNESS)) { - log_feature_not_supported(name, "transitions"); + log_feature_not_supported(name, LOG_STR("transitions")); this->set_flag_(FLAG_HAS_TRANSITION, false); } // Color brightness exists check if (this->has_color_brightness() && this->color_brightness_ > 0.0f && !(color_mode & ColorCapability::RGB)) { - log_color_mode_not_supported(name, "RGB brightness"); + log_color_mode_not_supported(name, LOG_STR("RGB brightness")); this->set_flag_(FLAG_HAS_COLOR_BRIGHTNESS, false); } @@ -221,7 +223,7 @@ LightColorValues LightCall::validate_() { if ((this->has_red() && this->red_ > 0.0f) || (this->has_green() && this->green_ > 0.0f) || (this->has_blue() && this->blue_ > 0.0f)) { if (!(color_mode & ColorCapability::RGB)) { - log_color_mode_not_supported(name, "RGB color"); + log_color_mode_not_supported(name, LOG_STR("RGB color")); this->set_flag_(FLAG_HAS_RED, false); this->set_flag_(FLAG_HAS_GREEN, false); this->set_flag_(FLAG_HAS_BLUE, false); @@ -231,21 +233,21 @@ LightColorValues LightCall::validate_() { // White value exists check if (this->has_white() && this->white_ > 0.0f && !(color_mode & ColorCapability::WHITE || color_mode & ColorCapability::COLD_WARM_WHITE)) { - log_color_mode_not_supported(name, "white value"); + log_color_mode_not_supported(name, LOG_STR("white value")); this->set_flag_(FLAG_HAS_WHITE, false); } // Color temperature exists check if (this->has_color_temperature() && !(color_mode & ColorCapability::COLOR_TEMPERATURE || color_mode & ColorCapability::COLD_WARM_WHITE)) { - log_color_mode_not_supported(name, "color temperature"); + log_color_mode_not_supported(name, LOG_STR("color temperature")); this->set_flag_(FLAG_HAS_COLOR_TEMPERATURE, false); } // Cold/warm white value exists check if ((this->has_cold_white() && this->cold_white_ > 0.0f) || (this->has_warm_white() && this->warm_white_ > 0.0f)) { if (!(color_mode & ColorCapability::COLD_WARM_WHITE)) { - log_color_mode_not_supported(name, "cold/warm white value"); + log_color_mode_not_supported(name, LOG_STR("cold/warm white value")); this->set_flag_(FLAG_HAS_COLD_WHITE, false); this->set_flag_(FLAG_HAS_WARM_WHITE, false); } @@ -255,7 +257,7 @@ LightColorValues LightCall::validate_() { if (this->has_##name_()) { \ auto val = this->name_##_; \ if (val < (min) || val > (max)) { \ - log_validation_warning(name, LOG_STR_LITERAL(upper_name), val, (min), (max)); \ + log_validation_warning(name, LOG_STR(upper_name), val, (min), (max)); \ this->name_##_ = clamp(val, (min), (max)); \ } \ } @@ -319,7 +321,7 @@ LightColorValues LightCall::validate_() { // Flash length check if (this->has_flash_() && this->flash_length_ == 0) { - log_invalid_parameter(name, "flash length must be greater than zero"); + log_invalid_parameter(name, LOG_STR("flash length must be greater than zero")); this->set_flag_(FLAG_HAS_FLASH, false); } @@ -338,13 +340,13 @@ LightColorValues LightCall::validate_() { } if (this->has_effect_() && (this->has_transition_() || this->has_flash_())) { - log_invalid_parameter(name, "effect cannot be used with transition/flash"); + log_invalid_parameter(name, LOG_STR("effect cannot be used with transition/flash")); this->set_flag_(FLAG_HAS_TRANSITION, false); this->set_flag_(FLAG_HAS_FLASH, false); } if (this->has_flash_() && this->has_transition_()) { - log_invalid_parameter(name, "flash cannot be used with transition"); + log_invalid_parameter(name, LOG_STR("flash cannot be used with transition")); this->set_flag_(FLAG_HAS_TRANSITION, false); } @@ -361,7 +363,7 @@ LightColorValues LightCall::validate_() { } if (this->has_transition_() && !supports_transition) { - log_feature_not_supported(name, "transitions"); + log_feature_not_supported(name, LOG_STR("transitions")); this->set_flag_(FLAG_HAS_TRANSITION, false); } @@ -371,7 +373,7 @@ LightColorValues LightCall::validate_() { bool target_state = this->has_state() ? this->state_ : v.is_on(); if (!this->has_flash_() && !target_state) { if (this->has_effect_()) { - log_invalid_parameter(name, "cannot start effect when turning off"); + log_invalid_parameter(name, LOG_STR("cannot start effect when turning off")); this->set_flag_(FLAG_HAS_EFFECT, false); } else if (this->parent_->active_effect_index_ != 0 && explicit_turn_off_request) { // Auto turn off effect From 694c590eb6dc419c848c19cdd04f09b3442b51d0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 5 Sep 2025 18:02:12 -0500 Subject: [PATCH 158/208] [captive_portal] ESP8266: Move strings to PROGMEM (saves 192 bytes RAM) (#10600) --- .../captive_portal/captive_portal.cpp | 47 ++++++++++++++----- .../captive_portal/captive_portal.h | 6 +-- 2 files changed, 38 insertions(+), 15 deletions(-) diff --git a/esphome/components/captive_portal/captive_portal.cpp b/esphome/components/captive_portal/captive_portal.cpp index 25179fdacc..7eb0ffa99e 100644 --- a/esphome/components/captive_portal/captive_portal.cpp +++ b/esphome/components/captive_portal/captive_portal.cpp @@ -11,17 +11,35 @@ namespace captive_portal { static const char *const TAG = "captive_portal"; void CaptivePortal::handle_config(AsyncWebServerRequest *request) { - AsyncResponseStream *stream = request->beginResponseStream("application/json"); - stream->addHeader("cache-control", "public, max-age=0, must-revalidate"); + AsyncResponseStream *stream = request->beginResponseStream(F("application/json")); + stream->addHeader(F("cache-control"), F("public, max-age=0, must-revalidate")); +#ifdef USE_ESP8266 + stream->print(F("{\"mac\":\"")); + stream->print(get_mac_address_pretty().c_str()); + stream->print(F("\",\"name\":\"")); + stream->print(App.get_name().c_str()); + stream->print(F("\",\"aps\":[{}")); +#else stream->printf(R"({"mac":"%s","name":"%s","aps":[{})", get_mac_address_pretty().c_str(), App.get_name().c_str()); +#endif for (auto &scan : wifi::global_wifi_component->get_scan_result()) { if (scan.get_is_hidden()) continue; - // Assumes no " in ssid, possible unicode isses? + // Assumes no " in ssid, possible unicode isses? +#ifdef USE_ESP8266 + stream->print(F(",{\"ssid\":\"")); + stream->print(scan.get_ssid().c_str()); + stream->print(F("\",\"rssi\":")); + stream->print(scan.get_rssi()); + stream->print(F(",\"lock\":")); + stream->print(scan.get_with_auth()); + stream->print(F("}")); +#else stream->printf(R"(,{"ssid":"%s","rssi":%d,"lock":%d})", scan.get_ssid().c_str(), scan.get_rssi(), scan.get_with_auth()); +#endif } stream->print(F("]}")); request->send(stream); @@ -34,7 +52,7 @@ void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) { ESP_LOGI(TAG, " Password=" LOG_SECRET("'%s'"), psk.c_str()); wifi::global_wifi_component->save_wifi_sta(ssid, psk); wifi::global_wifi_component->start_scanning(); - request->redirect("/?save"); + request->redirect(F("/?save")); } void CaptivePortal::setup() { @@ -53,18 +71,23 @@ void CaptivePortal::start() { this->dns_server_ = make_unique(); this->dns_server_->setErrorReplyCode(DNSReplyCode::NoError); network::IPAddress ip = wifi::global_wifi_component->wifi_soft_ap_ip(); - this->dns_server_->start(53, "*", ip); + this->dns_server_->start(53, F("*"), ip); // Re-enable loop() when DNS server is started this->enable_loop(); #endif this->base_->get_server()->onNotFound([this](AsyncWebServerRequest *req) { if (!this->active_ || req->host().c_str() == wifi::global_wifi_component->wifi_soft_ap_ip().str()) { - req->send(404, "text/html", "File not found"); + req->send(404, F("text/html"), F("File not found")); return; } +#ifdef USE_ESP8266 + String url = F("http://"); + url += wifi::global_wifi_component->wifi_soft_ap_ip().str().c_str(); +#else auto url = "http://" + wifi::global_wifi_component->wifi_soft_ap_ip().str(); +#endif req->redirect(url.c_str()); }); @@ -73,19 +96,19 @@ void CaptivePortal::start() { } void CaptivePortal::handleRequest(AsyncWebServerRequest *req) { - if (req->url() == "/") { + if (req->url() == F("/")) { #ifndef USE_ESP8266 - auto *response = req->beginResponse(200, "text/html", INDEX_GZ, sizeof(INDEX_GZ)); + auto *response = req->beginResponse(200, F("text/html"), INDEX_GZ, sizeof(INDEX_GZ)); #else - auto *response = req->beginResponse_P(200, "text/html", INDEX_GZ, sizeof(INDEX_GZ)); + auto *response = req->beginResponse_P(200, F("text/html"), INDEX_GZ, sizeof(INDEX_GZ)); #endif - response->addHeader("Content-Encoding", "gzip"); + response->addHeader(F("Content-Encoding"), F("gzip")); req->send(response); return; - } else if (req->url() == "/config.json") { + } else if (req->url() == F("/config.json")) { this->handle_config(req); return; - } else if (req->url() == "/wifisave") { + } else if (req->url() == F("/wifisave")) { this->handle_wifisave(req); return; } diff --git a/esphome/components/captive_portal/captive_portal.h b/esphome/components/captive_portal/captive_portal.h index c78fff824a..382afe92f0 100644 --- a/esphome/components/captive_portal/captive_portal.h +++ b/esphome/components/captive_portal/captive_portal.h @@ -45,11 +45,11 @@ class CaptivePortal : public AsyncWebHandler, public Component { return false; if (request->method() == HTTP_GET) { - if (request->url() == "/") + if (request->url() == F("/")) return true; - if (request->url() == "/config.json") + if (request->url() == F("/config.json")) return true; - if (request->url() == "/wifisave") + if (request->url() == F("/wifisave")) return true; } From 487ba4dad09c75d651cba7f967404807a8fbd12f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 5 Sep 2025 18:08:25 -0500 Subject: [PATCH 159/208] [mdns] Move constant strings to flash on ESP8266 (#10599) --- esphome/components/mdns/mdns_component.cpp | 94 +++++++++++++++++----- 1 file changed, 72 insertions(+), 22 deletions(-) diff --git a/esphome/components/mdns/mdns_component.cpp b/esphome/components/mdns/mdns_component.cpp index 316a10596f..5d9788198f 100644 --- a/esphome/components/mdns/mdns_component.cpp +++ b/esphome/components/mdns/mdns_component.cpp @@ -5,6 +5,30 @@ #include "esphome/core/version.h" #include "mdns_component.h" +#ifdef USE_ESP8266 +#include +// Macro to define strings in PROGMEM on ESP8266, regular memory on other platforms +#define MDNS_STATIC_CONST_CHAR(name, value) static const char name[] PROGMEM = value +// Helper to get string from PROGMEM - returns a temporary std::string +// Only define this function if we have services that will use it +#if defined(USE_API) || defined(USE_PROMETHEUS) || defined(USE_WEBSERVER) || defined(USE_MDNS_EXTRA_SERVICES) +static std::string mdns_string_p(const char *src) { + char buf[64]; + strncpy_P(buf, src, sizeof(buf) - 1); + buf[sizeof(buf) - 1] = '\0'; + return std::string(buf); +} +#define MDNS_STR(name) mdns_string_p(name) +#else +// If no services are configured, we still need the fallback service but it uses string literals +#define MDNS_STR(name) std::string(name) +#endif +#else +// On non-ESP8266 platforms, use regular const char* +#define MDNS_STATIC_CONST_CHAR(name, value) static constexpr const char *name = value +#define MDNS_STR(name) name +#endif + #ifdef USE_API #include "esphome/components/api/api_server.h" #endif @@ -21,6 +45,32 @@ static const char *const TAG = "mdns"; #define USE_WEBSERVER_PORT 80 // NOLINT #endif +// Define all constant strings using the macro +MDNS_STATIC_CONST_CHAR(SERVICE_ESPHOMELIB, "_esphomelib"); +MDNS_STATIC_CONST_CHAR(SERVICE_TCP, "_tcp"); +MDNS_STATIC_CONST_CHAR(SERVICE_PROMETHEUS, "_prometheus-http"); +MDNS_STATIC_CONST_CHAR(SERVICE_HTTP, "_http"); + +MDNS_STATIC_CONST_CHAR(TXT_FRIENDLY_NAME, "friendly_name"); +MDNS_STATIC_CONST_CHAR(TXT_VERSION, "version"); +MDNS_STATIC_CONST_CHAR(TXT_MAC, "mac"); +MDNS_STATIC_CONST_CHAR(TXT_PLATFORM, "platform"); +MDNS_STATIC_CONST_CHAR(TXT_BOARD, "board"); +MDNS_STATIC_CONST_CHAR(TXT_NETWORK, "network"); +MDNS_STATIC_CONST_CHAR(TXT_API_ENCRYPTION, "api_encryption"); +MDNS_STATIC_CONST_CHAR(TXT_API_ENCRYPTION_SUPPORTED, "api_encryption_supported"); +MDNS_STATIC_CONST_CHAR(TXT_PROJECT_NAME, "project_name"); +MDNS_STATIC_CONST_CHAR(TXT_PROJECT_VERSION, "project_version"); +MDNS_STATIC_CONST_CHAR(TXT_PACKAGE_IMPORT_URL, "package_import_url"); + +MDNS_STATIC_CONST_CHAR(PLATFORM_ESP8266, "ESP8266"); +MDNS_STATIC_CONST_CHAR(PLATFORM_ESP32, "ESP32"); +MDNS_STATIC_CONST_CHAR(PLATFORM_RP2040, "RP2040"); + +MDNS_STATIC_CONST_CHAR(NETWORK_WIFI, "wifi"); +MDNS_STATIC_CONST_CHAR(NETWORK_ETHERNET, "ethernet"); +MDNS_STATIC_CONST_CHAR(NETWORK_THREAD, "thread"); + void MDNSComponent::compile_records_() { this->hostname_ = App.get_name(); @@ -50,8 +100,8 @@ void MDNSComponent::compile_records_() { if (api::global_api_server != nullptr) { this->services_.emplace_back(); auto &service = this->services_.back(); - service.service_type = "_esphomelib"; - service.proto = "_tcp"; + service.service_type = MDNS_STR(SERVICE_ESPHOMELIB); + service.proto = MDNS_STR(SERVICE_TCP); service.port = api::global_api_server->get_port(); const std::string &friendly_name = App.get_friendly_name(); @@ -82,47 +132,47 @@ void MDNSComponent::compile_records_() { txt_records.reserve(txt_count); if (!friendly_name_empty) { - txt_records.emplace_back(MDNSTXTRecord{"friendly_name", friendly_name}); + txt_records.push_back({MDNS_STR(TXT_FRIENDLY_NAME), friendly_name}); } - txt_records.emplace_back(MDNSTXTRecord{"version", ESPHOME_VERSION}); - txt_records.emplace_back(MDNSTXTRecord{"mac", get_mac_address()}); + txt_records.push_back({MDNS_STR(TXT_VERSION), ESPHOME_VERSION}); + txt_records.push_back({MDNS_STR(TXT_MAC), get_mac_address()}); #ifdef USE_ESP8266 - txt_records.emplace_back(MDNSTXTRecord{"platform", "ESP8266"}); + txt_records.push_back({MDNS_STR(TXT_PLATFORM), MDNS_STR(PLATFORM_ESP8266)}); #elif defined(USE_ESP32) - txt_records.emplace_back(MDNSTXTRecord{"platform", "ESP32"}); + txt_records.push_back({MDNS_STR(TXT_PLATFORM), MDNS_STR(PLATFORM_ESP32)}); #elif defined(USE_RP2040) - txt_records.emplace_back(MDNSTXTRecord{"platform", "RP2040"}); + txt_records.push_back({MDNS_STR(TXT_PLATFORM), MDNS_STR(PLATFORM_RP2040)}); #elif defined(USE_LIBRETINY) txt_records.emplace_back(MDNSTXTRecord{"platform", lt_cpu_get_model_name()}); #endif - txt_records.emplace_back(MDNSTXTRecord{"board", ESPHOME_BOARD}); + txt_records.push_back({MDNS_STR(TXT_BOARD), ESPHOME_BOARD}); #if defined(USE_WIFI) - txt_records.emplace_back(MDNSTXTRecord{"network", "wifi"}); + txt_records.push_back({MDNS_STR(TXT_NETWORK), MDNS_STR(NETWORK_WIFI)}); #elif defined(USE_ETHERNET) - txt_records.emplace_back(MDNSTXTRecord{"network", "ethernet"}); + txt_records.push_back({MDNS_STR(TXT_NETWORK), MDNS_STR(NETWORK_ETHERNET)}); #elif defined(USE_OPENTHREAD) - txt_records.emplace_back(MDNSTXTRecord{"network", "thread"}); + txt_records.push_back({MDNS_STR(TXT_NETWORK), MDNS_STR(NETWORK_THREAD)}); #endif #ifdef USE_API_NOISE - static constexpr const char *NOISE_ENCRYPTION = "Noise_NNpsk0_25519_ChaChaPoly_SHA256"; + MDNS_STATIC_CONST_CHAR(NOISE_ENCRYPTION, "Noise_NNpsk0_25519_ChaChaPoly_SHA256"); if (api::global_api_server->get_noise_ctx()->has_psk()) { - txt_records.emplace_back(MDNSTXTRecord{"api_encryption", NOISE_ENCRYPTION}); + txt_records.push_back({MDNS_STR(TXT_API_ENCRYPTION), MDNS_STR(NOISE_ENCRYPTION)}); } else { - txt_records.emplace_back(MDNSTXTRecord{"api_encryption_supported", NOISE_ENCRYPTION}); + txt_records.push_back({MDNS_STR(TXT_API_ENCRYPTION_SUPPORTED), MDNS_STR(NOISE_ENCRYPTION)}); } #endif #ifdef ESPHOME_PROJECT_NAME - txt_records.emplace_back(MDNSTXTRecord{"project_name", ESPHOME_PROJECT_NAME}); - txt_records.emplace_back(MDNSTXTRecord{"project_version", ESPHOME_PROJECT_VERSION}); + txt_records.push_back({MDNS_STR(TXT_PROJECT_NAME), ESPHOME_PROJECT_NAME}); + txt_records.push_back({MDNS_STR(TXT_PROJECT_VERSION), ESPHOME_PROJECT_VERSION}); #endif // ESPHOME_PROJECT_NAME #ifdef USE_DASHBOARD_IMPORT - txt_records.emplace_back(MDNSTXTRecord{"package_import_url", dashboard_import::get_package_import_url()}); + txt_records.push_back({MDNS_STR(TXT_PACKAGE_IMPORT_URL), dashboard_import::get_package_import_url()}); #endif } #endif // USE_API @@ -130,16 +180,16 @@ void MDNSComponent::compile_records_() { #ifdef USE_PROMETHEUS this->services_.emplace_back(); auto &prom_service = this->services_.back(); - prom_service.service_type = "_prometheus-http"; - prom_service.proto = "_tcp"; + prom_service.service_type = MDNS_STR(SERVICE_PROMETHEUS); + prom_service.proto = MDNS_STR(SERVICE_TCP); prom_service.port = USE_WEBSERVER_PORT; #endif #ifdef USE_WEBSERVER this->services_.emplace_back(); auto &web_service = this->services_.back(); - web_service.service_type = "_http"; - web_service.proto = "_tcp"; + web_service.service_type = MDNS_STR(SERVICE_HTTP); + web_service.proto = MDNS_STR(SERVICE_TCP); web_service.port = USE_WEBSERVER_PORT; #endif From 1359142106ca14334b49186f28aad0f01988d9c3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 5 Sep 2025 18:10:18 -0500 Subject: [PATCH 160/208] [api] Store Noise protocol prologue in flash on ESP8266 (#10598) --- esphome/components/api/api_frame_helper_noise.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index 35d1715931..37aba7ec13 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -10,10 +10,18 @@ #include #include +#ifdef USE_ESP8266 +#include +#endif + namespace esphome::api { static const char *const TAG = "api.noise"; +#ifdef USE_ESP8266 +static const char PROLOGUE_INIT[] PROGMEM = "NoiseAPIInit"; +#else static const char *const PROLOGUE_INIT = "NoiseAPIInit"; +#endif static constexpr size_t PROLOGUE_INIT_LEN = 12; // strlen("NoiseAPIInit") #define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, this->client_info_->get_combined_info().c_str(), ##__VA_ARGS__) @@ -75,7 +83,11 @@ APIError APINoiseFrameHelper::init() { // init prologue size_t old_size = prologue_.size(); prologue_.resize(old_size + PROLOGUE_INIT_LEN); +#ifdef USE_ESP8266 + memcpy_P(prologue_.data() + old_size, PROLOGUE_INIT, PROLOGUE_INIT_LEN); +#else std::memcpy(prologue_.data() + old_size, PROLOGUE_INIT, PROLOGUE_INIT_LEN); +#endif state_ = State::CLIENT_HELLO; return APIError::OK; From 3fd469cfe8a12927aa65497dab3bb195edf4e337 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 5 Sep 2025 18:16:43 -0500 Subject: [PATCH 161/208] [esp8266][api] Store error strings in PROGMEM to reduce RAM usage (#10568) --- esphome/components/api/api_connection.cpp | 17 +-- esphome/components/api/api_connection.h | 2 +- esphome/components/api/api_frame_helper.cpp | 50 ++++----- esphome/components/api/api_frame_helper.h | 2 +- .../components/api/api_frame_helper_noise.cpp | 103 +++++++++++------- .../components/api/api_frame_helper_noise.h | 4 +- 6 files changed, 102 insertions(+), 76 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 4b3a3e2fc8..02b1d61368 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -112,7 +112,7 @@ void APIConnection::start() { APIError err = this->helper_->init(); if (err != APIError::OK) { on_fatal_error(); - this->log_warning_("Helper init failed", err); + this->log_warning_(LOG_STR("Helper init failed"), err); return; } this->client_info_.peername = helper_->getpeername(); @@ -159,7 +159,7 @@ void APIConnection::loop() { break; } else if (err != APIError::OK) { on_fatal_error(); - this->log_warning_("Reading failed", err); + this->log_warning_(LOG_STR("Reading failed"), err); return; } else { this->last_traffic_ = now; @@ -1565,7 +1565,7 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) { return false; if (err != APIError::OK) { on_fatal_error(); - this->log_warning_("Packet write failed", err); + this->log_warning_(LOG_STR("Packet write failed"), err); return false; } // Do not set last_traffic_ on send @@ -1752,7 +1752,7 @@ void APIConnection::process_batch_() { std::span(packet_info, packet_count)); if (err != APIError::OK && err != APIError::WOULD_BLOCK) { on_fatal_error(); - this->log_warning_("Batch write failed", err); + this->log_warning_(LOG_STR("Batch write failed"), err); } #ifdef HAS_PROTO_MESSAGE_DUMP @@ -1830,11 +1830,14 @@ void APIConnection::process_state_subscriptions_() { } #endif // USE_API_HOMEASSISTANT_STATES -void APIConnection::log_warning_(const char *message, APIError err) { - ESP_LOGW(TAG, "%s: %s %s errno=%d", this->get_client_combined_info().c_str(), message, api_error_to_str(err), errno); +void APIConnection::log_warning_(const LogString *message, APIError err) { + ESP_LOGW(TAG, "%s: %s %s errno=%d", this->get_client_combined_info().c_str(), LOG_STR_ARG(message), + LOG_STR_ARG(api_error_to_logstr(err)), errno); } -void APIConnection::log_socket_operation_failed_(APIError err) { this->log_warning_("Socket operation failed", err); } +void APIConnection::log_socket_operation_failed_(APIError err) { + this->log_warning_(LOG_STR("Socket operation failed"), err); +} } // namespace esphome::api #endif diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 72254d1536..7ee82e0c68 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -732,7 +732,7 @@ class APIConnection final : public APIServerConnection { } // Helper function to log API errors with errno - void log_warning_(const char *message, APIError err); + void log_warning_(const LogString *message, APIError err); // Specific helper for duplicated error message void log_socket_operation_failed_(APIError err); }; diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp index dee3af2ac3..a284e09c4a 100644 --- a/esphome/components/api/api_frame_helper.cpp +++ b/esphome/components/api/api_frame_helper.cpp @@ -23,59 +23,59 @@ static const char *const TAG = "api.frame_helper"; #define LOG_PACKET_SENDING(data, len) ((void) 0) #endif -const char *api_error_to_str(APIError err) { +const LogString *api_error_to_logstr(APIError err) { // not using switch to ensure compiler doesn't try to build a big table out of it if (err == APIError::OK) { - return "OK"; + return LOG_STR("OK"); } else if (err == APIError::WOULD_BLOCK) { - return "WOULD_BLOCK"; + return LOG_STR("WOULD_BLOCK"); } else if (err == APIError::BAD_INDICATOR) { - return "BAD_INDICATOR"; + return LOG_STR("BAD_INDICATOR"); } else if (err == APIError::BAD_DATA_PACKET) { - return "BAD_DATA_PACKET"; + return LOG_STR("BAD_DATA_PACKET"); } else if (err == APIError::TCP_NODELAY_FAILED) { - return "TCP_NODELAY_FAILED"; + return LOG_STR("TCP_NODELAY_FAILED"); } else if (err == APIError::TCP_NONBLOCKING_FAILED) { - return "TCP_NONBLOCKING_FAILED"; + return LOG_STR("TCP_NONBLOCKING_FAILED"); } else if (err == APIError::CLOSE_FAILED) { - return "CLOSE_FAILED"; + return LOG_STR("CLOSE_FAILED"); } else if (err == APIError::SHUTDOWN_FAILED) { - return "SHUTDOWN_FAILED"; + return LOG_STR("SHUTDOWN_FAILED"); } else if (err == APIError::BAD_STATE) { - return "BAD_STATE"; + return LOG_STR("BAD_STATE"); } else if (err == APIError::BAD_ARG) { - return "BAD_ARG"; + return LOG_STR("BAD_ARG"); } else if (err == APIError::SOCKET_READ_FAILED) { - return "SOCKET_READ_FAILED"; + return LOG_STR("SOCKET_READ_FAILED"); } else if (err == APIError::SOCKET_WRITE_FAILED) { - return "SOCKET_WRITE_FAILED"; + return LOG_STR("SOCKET_WRITE_FAILED"); } else if (err == APIError::OUT_OF_MEMORY) { - return "OUT_OF_MEMORY"; + return LOG_STR("OUT_OF_MEMORY"); } else if (err == APIError::CONNECTION_CLOSED) { - return "CONNECTION_CLOSED"; + return LOG_STR("CONNECTION_CLOSED"); } #ifdef USE_API_NOISE else if (err == APIError::BAD_HANDSHAKE_PACKET_LEN) { - return "BAD_HANDSHAKE_PACKET_LEN"; + return LOG_STR("BAD_HANDSHAKE_PACKET_LEN"); } else if (err == APIError::HANDSHAKESTATE_READ_FAILED) { - return "HANDSHAKESTATE_READ_FAILED"; + return LOG_STR("HANDSHAKESTATE_READ_FAILED"); } else if (err == APIError::HANDSHAKESTATE_WRITE_FAILED) { - return "HANDSHAKESTATE_WRITE_FAILED"; + return LOG_STR("HANDSHAKESTATE_WRITE_FAILED"); } else if (err == APIError::HANDSHAKESTATE_BAD_STATE) { - return "HANDSHAKESTATE_BAD_STATE"; + return LOG_STR("HANDSHAKESTATE_BAD_STATE"); } else if (err == APIError::CIPHERSTATE_DECRYPT_FAILED) { - return "CIPHERSTATE_DECRYPT_FAILED"; + return LOG_STR("CIPHERSTATE_DECRYPT_FAILED"); } else if (err == APIError::CIPHERSTATE_ENCRYPT_FAILED) { - return "CIPHERSTATE_ENCRYPT_FAILED"; + return LOG_STR("CIPHERSTATE_ENCRYPT_FAILED"); } else if (err == APIError::HANDSHAKESTATE_SETUP_FAILED) { - return "HANDSHAKESTATE_SETUP_FAILED"; + return LOG_STR("HANDSHAKESTATE_SETUP_FAILED"); } else if (err == APIError::HANDSHAKESTATE_SPLIT_FAILED) { - return "HANDSHAKESTATE_SPLIT_FAILED"; + return LOG_STR("HANDSHAKESTATE_SPLIT_FAILED"); } else if (err == APIError::BAD_HANDSHAKE_ERROR_BYTE) { - return "BAD_HANDSHAKE_ERROR_BYTE"; + return LOG_STR("BAD_HANDSHAKE_ERROR_BYTE"); } #endif - return "UNKNOWN"; + return LOG_STR("UNKNOWN"); } // Default implementation for loop - handles sending buffered data diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index 43e9d95fbe..c11d701ffe 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -66,7 +66,7 @@ enum class APIError : uint16_t { #endif }; -const char *api_error_to_str(APIError err); +const LogString *api_error_to_logstr(APIError err); class APIFrameHelper { public: diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index 37aba7ec13..0e49f93db5 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -35,42 +35,42 @@ static constexpr size_t PROLOGUE_INIT_LEN = 12; // strlen("NoiseAPIInit") #endif /// Convert a noise error code to a readable error -std::string noise_err_to_str(int err) { +const LogString *noise_err_to_logstr(int err) { if (err == NOISE_ERROR_NO_MEMORY) - return "NO_MEMORY"; + return LOG_STR("NO_MEMORY"); if (err == NOISE_ERROR_UNKNOWN_ID) - return "UNKNOWN_ID"; + return LOG_STR("UNKNOWN_ID"); if (err == NOISE_ERROR_UNKNOWN_NAME) - return "UNKNOWN_NAME"; + return LOG_STR("UNKNOWN_NAME"); if (err == NOISE_ERROR_MAC_FAILURE) - return "MAC_FAILURE"; + return LOG_STR("MAC_FAILURE"); if (err == NOISE_ERROR_NOT_APPLICABLE) - return "NOT_APPLICABLE"; + return LOG_STR("NOT_APPLICABLE"); if (err == NOISE_ERROR_SYSTEM) - return "SYSTEM"; + return LOG_STR("SYSTEM"); if (err == NOISE_ERROR_REMOTE_KEY_REQUIRED) - return "REMOTE_KEY_REQUIRED"; + return LOG_STR("REMOTE_KEY_REQUIRED"); if (err == NOISE_ERROR_LOCAL_KEY_REQUIRED) - return "LOCAL_KEY_REQUIRED"; + return LOG_STR("LOCAL_KEY_REQUIRED"); if (err == NOISE_ERROR_PSK_REQUIRED) - return "PSK_REQUIRED"; + return LOG_STR("PSK_REQUIRED"); if (err == NOISE_ERROR_INVALID_LENGTH) - return "INVALID_LENGTH"; + return LOG_STR("INVALID_LENGTH"); if (err == NOISE_ERROR_INVALID_PARAM) - return "INVALID_PARAM"; + return LOG_STR("INVALID_PARAM"); if (err == NOISE_ERROR_INVALID_STATE) - return "INVALID_STATE"; + return LOG_STR("INVALID_STATE"); if (err == NOISE_ERROR_INVALID_NONCE) - return "INVALID_NONCE"; + return LOG_STR("INVALID_NONCE"); if (err == NOISE_ERROR_INVALID_PRIVATE_KEY) - return "INVALID_PRIVATE_KEY"; + return LOG_STR("INVALID_PRIVATE_KEY"); if (err == NOISE_ERROR_INVALID_PUBLIC_KEY) - return "INVALID_PUBLIC_KEY"; + return LOG_STR("INVALID_PUBLIC_KEY"); if (err == NOISE_ERROR_INVALID_FORMAT) - return "INVALID_FORMAT"; + return LOG_STR("INVALID_FORMAT"); if (err == NOISE_ERROR_INVALID_SIGNATURE) - return "INVALID_SIGNATURE"; - return to_string(err); + return LOG_STR("INVALID_SIGNATURE"); + return LOG_STR("UNKNOWN"); } /// Initialize the frame helper, returns OK if successful. @@ -95,18 +95,18 @@ APIError APINoiseFrameHelper::init() { // Helper for handling handshake frame errors APIError APINoiseFrameHelper::handle_handshake_frame_error_(APIError aerr) { if (aerr == APIError::BAD_INDICATOR) { - send_explicit_handshake_reject_("Bad indicator byte"); + send_explicit_handshake_reject_(LOG_STR("Bad indicator byte")); } else if (aerr == APIError::BAD_HANDSHAKE_PACKET_LEN) { - send_explicit_handshake_reject_("Bad handshake packet len"); + send_explicit_handshake_reject_(LOG_STR("Bad handshake packet len")); } return aerr; } // Helper for handling noise library errors -APIError APINoiseFrameHelper::handle_noise_error_(int err, const char *func_name, APIError api_err) { +APIError APINoiseFrameHelper::handle_noise_error_(int err, const LogString *func_name, APIError api_err) { if (err != 0) { state_ = State::FAILED; - HELPER_LOG("%s failed: %s", func_name, noise_err_to_str(err).c_str()); + HELPER_LOG("%s failed: %s", LOG_STR_ARG(func_name), LOG_STR_ARG(noise_err_to_logstr(err))); return api_err; } return APIError::OK; @@ -291,11 +291,11 @@ APIError APINoiseFrameHelper::state_action_() { } if (frame.empty()) { - send_explicit_handshake_reject_("Empty handshake message"); + send_explicit_handshake_reject_(LOG_STR("Empty handshake message")); return APIError::BAD_HANDSHAKE_ERROR_BYTE; } else if (frame[0] != 0x00) { HELPER_LOG("Bad handshake error byte: %u", frame[0]); - send_explicit_handshake_reject_("Bad handshake error byte"); + send_explicit_handshake_reject_(LOG_STR("Bad handshake error byte")); return APIError::BAD_HANDSHAKE_ERROR_BYTE; } @@ -305,8 +305,10 @@ APIError APINoiseFrameHelper::state_action_() { err = noise_handshakestate_read_message(handshake_, &mbuf, nullptr); if (err != 0) { // Special handling for MAC failure - send_explicit_handshake_reject_(err == NOISE_ERROR_MAC_FAILURE ? "Handshake MAC failure" : "Handshake error"); - return handle_noise_error_(err, "noise_handshakestate_read_message", APIError::HANDSHAKESTATE_READ_FAILED); + send_explicit_handshake_reject_(err == NOISE_ERROR_MAC_FAILURE ? LOG_STR("Handshake MAC failure") + : LOG_STR("Handshake error")); + return handle_noise_error_(err, LOG_STR("noise_handshakestate_read_message"), + APIError::HANDSHAKESTATE_READ_FAILED); } aerr = check_handshake_finished_(); @@ -319,8 +321,8 @@ APIError APINoiseFrameHelper::state_action_() { noise_buffer_set_output(mbuf, buffer + 1, sizeof(buffer) - 1); err = noise_handshakestate_write_message(handshake_, &mbuf, nullptr); - APIError aerr_write = - handle_noise_error_(err, "noise_handshakestate_write_message", APIError::HANDSHAKESTATE_WRITE_FAILED); + APIError aerr_write = handle_noise_error_(err, LOG_STR("noise_handshakestate_write_message"), + APIError::HANDSHAKESTATE_WRITE_FAILED); if (aerr_write != APIError::OK) return aerr_write; buffer[0] = 0x00; // success @@ -343,15 +345,31 @@ APIError APINoiseFrameHelper::state_action_() { } return APIError::OK; } -void APINoiseFrameHelper::send_explicit_handshake_reject_(const std::string &reason) { +void APINoiseFrameHelper::send_explicit_handshake_reject_(const LogString *reason) { +#ifdef USE_STORE_LOG_STR_IN_FLASH + // On ESP8266 with flash strings, we need to use PROGMEM-aware functions + size_t reason_len = strlen_P(reinterpret_cast(reason)); std::vector data; - data.resize(reason.length() + 1); + data.resize(reason_len + 1); + data[0] = 0x01; // failure + + // Copy error message from PROGMEM + if (reason_len > 0) { + memcpy_P(data.data() + 1, reinterpret_cast(reason), reason_len); + } +#else + // Normal memory access + const char *reason_str = LOG_STR_ARG(reason); + size_t reason_len = strlen(reason_str); + std::vector data; + data.resize(reason_len + 1); data[0] = 0x01; // failure // Copy error message in bulk - if (!reason.empty()) { - std::memcpy(data.data() + 1, reason.c_str(), reason.length()); + if (reason_len > 0) { + std::memcpy(data.data() + 1, reason_str, reason_len); } +#endif // temporarily remove failed state auto orig_state = state_; @@ -380,7 +398,8 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) { noise_buffer_init(mbuf); noise_buffer_set_inout(mbuf, frame.data(), frame.size(), frame.size()); err = noise_cipherstate_decrypt(recv_cipher_, &mbuf); - APIError decrypt_err = handle_noise_error_(err, "noise_cipherstate_decrypt", APIError::CIPHERSTATE_DECRYPT_FAILED); + APIError decrypt_err = + handle_noise_error_(err, LOG_STR("noise_cipherstate_decrypt"), APIError::CIPHERSTATE_DECRYPT_FAILED); if (decrypt_err != APIError::OK) return decrypt_err; @@ -462,7 +481,8 @@ APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, st 4 + packet.payload_size + frame_footer_size_); int err = noise_cipherstate_encrypt(send_cipher_, &mbuf); - APIError aerr = handle_noise_error_(err, "noise_cipherstate_encrypt", APIError::CIPHERSTATE_ENCRYPT_FAILED); + APIError aerr = + handle_noise_error_(err, LOG_STR("noise_cipherstate_encrypt"), APIError::CIPHERSTATE_ENCRYPT_FAILED); if (aerr != APIError::OK) return aerr; @@ -516,25 +536,27 @@ APIError APINoiseFrameHelper::init_handshake_() { nid_.modifier_ids[0] = NOISE_MODIFIER_PSK0; err = noise_handshakestate_new_by_id(&handshake_, &nid_, NOISE_ROLE_RESPONDER); - APIError aerr = handle_noise_error_(err, "noise_handshakestate_new_by_id", APIError::HANDSHAKESTATE_SETUP_FAILED); + APIError aerr = + handle_noise_error_(err, LOG_STR("noise_handshakestate_new_by_id"), APIError::HANDSHAKESTATE_SETUP_FAILED); if (aerr != APIError::OK) return aerr; const auto &psk = ctx_->get_psk(); err = noise_handshakestate_set_pre_shared_key(handshake_, psk.data(), psk.size()); - aerr = handle_noise_error_(err, "noise_handshakestate_set_pre_shared_key", APIError::HANDSHAKESTATE_SETUP_FAILED); + aerr = handle_noise_error_(err, LOG_STR("noise_handshakestate_set_pre_shared_key"), + APIError::HANDSHAKESTATE_SETUP_FAILED); if (aerr != APIError::OK) return aerr; err = noise_handshakestate_set_prologue(handshake_, prologue_.data(), prologue_.size()); - aerr = handle_noise_error_(err, "noise_handshakestate_set_prologue", APIError::HANDSHAKESTATE_SETUP_FAILED); + aerr = handle_noise_error_(err, LOG_STR("noise_handshakestate_set_prologue"), APIError::HANDSHAKESTATE_SETUP_FAILED); if (aerr != APIError::OK) return aerr; // set_prologue copies it into handshakestate, so we can get rid of it now prologue_ = {}; err = noise_handshakestate_start(handshake_); - aerr = handle_noise_error_(err, "noise_handshakestate_start", APIError::HANDSHAKESTATE_SETUP_FAILED); + aerr = handle_noise_error_(err, LOG_STR("noise_handshakestate_start"), APIError::HANDSHAKESTATE_SETUP_FAILED); if (aerr != APIError::OK) return aerr; return APIError::OK; @@ -552,7 +574,8 @@ APIError APINoiseFrameHelper::check_handshake_finished_() { return APIError::HANDSHAKESTATE_BAD_STATE; } int err = noise_handshakestate_split(handshake_, &send_cipher_, &recv_cipher_); - APIError aerr = handle_noise_error_(err, "noise_handshakestate_split", APIError::HANDSHAKESTATE_SPLIT_FAILED); + APIError aerr = + handle_noise_error_(err, LOG_STR("noise_handshakestate_split"), APIError::HANDSHAKESTATE_SPLIT_FAILED); if (aerr != APIError::OK) return aerr; diff --git a/esphome/components/api/api_frame_helper_noise.h b/esphome/components/api/api_frame_helper_noise.h index 49bc6f8854..71a217c4ca 100644 --- a/esphome/components/api/api_frame_helper_noise.h +++ b/esphome/components/api/api_frame_helper_noise.h @@ -32,9 +32,9 @@ class APINoiseFrameHelper final : public APIFrameHelper { APIError write_frame_(const uint8_t *data, uint16_t len); APIError init_handshake_(); APIError check_handshake_finished_(); - void send_explicit_handshake_reject_(const std::string &reason); + void send_explicit_handshake_reject_(const LogString *reason); APIError handle_handshake_frame_error_(APIError aerr); - APIError handle_noise_error_(int err, const char *func_name, APIError api_err); + APIError handle_noise_error_(int err, const LogString *func_name, APIError api_err); // Pointers first (4 bytes each) NoiseHandshakeState *handshake_{nullptr}; From e018b15641e47e1bf8662253ad29a34ba59284de Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Fri, 5 Sep 2025 20:10:48 -0500 Subject: [PATCH 162/208] [sen5x] Various optimizing & tidying up (#10602) --- esphome/components/sen5x/sen5x.cpp | 112 +++++++++++++++-------------- esphome/components/sen5x/sen5x.h | 36 ++++++---- 2 files changed, 78 insertions(+), 70 deletions(-) diff --git a/esphome/components/sen5x/sen5x.cpp b/esphome/components/sen5x/sen5x.cpp index f3222221a2..3298a5b8db 100644 --- a/esphome/components/sen5x/sen5x.cpp +++ b/esphome/components/sen5x/sen5x.cpp @@ -29,6 +29,19 @@ static const int8_t SEN5X_INDEX_SCALE_FACTOR = 10; // static const int8_t SEN5X_MIN_INDEX_VALUE = 1 * SEN5X_INDEX_SCALE_FACTOR; // must be adjusted by the scale factor static const int16_t SEN5X_MAX_INDEX_VALUE = 500 * SEN5X_INDEX_SCALE_FACTOR; // must be adjusted by the scale factor +static const LogString *rht_accel_mode_to_string(RhtAccelerationMode mode) { + switch (mode) { + case LOW_ACCELERATION: + return LOG_STR("LOW"); + case MEDIUM_ACCELERATION: + return LOG_STR("MEDIUM"); + case HIGH_ACCELERATION: + return LOG_STR("HIGH"); + default: + return LOG_STR("UNKNOWN"); + } +} + void SEN5XComponent::setup() { // the sensor needs 1000 ms to enter the idle state this->set_timeout(1000, [this]() { @@ -50,7 +63,7 @@ void SEN5XComponent::setup() { uint32_t stop_measurement_delay = 0; // In order to query the device periodic measurement must be ceased if (raw_read_status) { - ESP_LOGD(TAG, "Sensor has data available, stopping periodic measurement"); + ESP_LOGD(TAG, "Data is available; stopping periodic measurement"); if (!this->write_command(SEN5X_CMD_STOP_MEASUREMENTS)) { ESP_LOGE(TAG, "Failed to stop measurements"); this->mark_failed(); @@ -71,7 +84,8 @@ void SEN5XComponent::setup() { this->serial_number_[0] = static_cast(uint16_t(raw_serial_number[0]) & 0xFF); this->serial_number_[1] = static_cast(raw_serial_number[0] & 0xFF); this->serial_number_[2] = static_cast(raw_serial_number[1] >> 8); - ESP_LOGD(TAG, "Serial number %02d.%02d.%02d", serial_number_[0], serial_number_[1], serial_number_[2]); + ESP_LOGV(TAG, "Serial number %02d.%02d.%02d", this->serial_number_[0], this->serial_number_[1], + this->serial_number_[2]); uint16_t raw_product_name[16]; if (!this->get_register(SEN5X_CMD_GET_PRODUCT_NAME, raw_product_name, 16, 20)) { @@ -88,45 +102,43 @@ void SEN5XComponent::setup() { // first char current_char = *current_int >> 8; if (current_char) { - product_name_.push_back(current_char); + this->product_name_.push_back(current_char); // second char current_char = *current_int & 0xFF; if (current_char) { - product_name_.push_back(current_char); + this->product_name_.push_back(current_char); } } current_int++; } while (current_char && --max); Sen5xType sen5x_type = UNKNOWN; - if (product_name_ == "SEN50") { + if (this->product_name_ == "SEN50") { sen5x_type = SEN50; } else { - if (product_name_ == "SEN54") { + if (this->product_name_ == "SEN54") { sen5x_type = SEN54; } else { - if (product_name_ == "SEN55") { + if (this->product_name_ == "SEN55") { sen5x_type = SEN55; } } - ESP_LOGD(TAG, "Productname %s", product_name_.c_str()); + ESP_LOGD(TAG, "Product name: %s", this->product_name_.c_str()); } if (this->humidity_sensor_ && sen5x_type == SEN50) { - ESP_LOGE(TAG, "For Relative humidity a SEN54 OR SEN55 is required. You are using a <%s> sensor", - this->product_name_.c_str()); + ESP_LOGE(TAG, "Relative humidity requires a SEN54 or SEN55"); this->humidity_sensor_ = nullptr; // mark as not used } if (this->temperature_sensor_ && sen5x_type == SEN50) { - ESP_LOGE(TAG, "For Temperature a SEN54 OR SEN55 is required. You are using a <%s> sensor", - this->product_name_.c_str()); + ESP_LOGE(TAG, "Temperature requires a SEN54 or SEN55"); this->temperature_sensor_ = nullptr; // mark as not used } if (this->voc_sensor_ && sen5x_type == SEN50) { - ESP_LOGE(TAG, "For VOC a SEN54 OR SEN55 is required. You are using a <%s> sensor", this->product_name_.c_str()); + ESP_LOGE(TAG, "VOC requires a SEN54 or SEN55"); this->voc_sensor_ = nullptr; // mark as not used } if (this->nox_sensor_ && sen5x_type != SEN55) { - ESP_LOGE(TAG, "For NOx a SEN55 is required. You are using a <%s> sensor", this->product_name_.c_str()); + ESP_LOGE(TAG, "NOx requires a SEN55"); this->nox_sensor_ = nullptr; // mark as not used } @@ -137,7 +149,7 @@ void SEN5XComponent::setup() { return; } this->firmware_version_ >>= 8; - ESP_LOGD(TAG, "Firmware version %d", this->firmware_version_); + ESP_LOGV(TAG, "Firmware version %d", this->firmware_version_); if (this->voc_sensor_ && this->store_baseline_) { uint32_t combined_serial = @@ -150,7 +162,7 @@ void SEN5XComponent::setup() { if (this->pref_.load(&this->voc_baselines_storage_)) { ESP_LOGI(TAG, "Loaded VOC baseline state0: 0x%04" PRIX32 ", state1: 0x%04" PRIX32, - this->voc_baselines_storage_.state0, voc_baselines_storage_.state1); + this->voc_baselines_storage_.state0, this->voc_baselines_storage_.state1); } // Initialize storage timestamp @@ -158,13 +170,13 @@ void SEN5XComponent::setup() { if (this->voc_baselines_storage_.state0 > 0 && this->voc_baselines_storage_.state1 > 0) { ESP_LOGI(TAG, "Setting VOC baseline from save state0: 0x%04" PRIX32 ", state1: 0x%04" PRIX32, - this->voc_baselines_storage_.state0, voc_baselines_storage_.state1); + this->voc_baselines_storage_.state0, this->voc_baselines_storage_.state1); uint16_t states[4]; - states[0] = voc_baselines_storage_.state0 >> 16; - states[1] = voc_baselines_storage_.state0 & 0xFFFF; - states[2] = voc_baselines_storage_.state1 >> 16; - states[3] = voc_baselines_storage_.state1 & 0xFFFF; + states[0] = this->voc_baselines_storage_.state0 >> 16; + states[1] = this->voc_baselines_storage_.state0 & 0xFFFF; + states[2] = this->voc_baselines_storage_.state1 >> 16; + states[3] = this->voc_baselines_storage_.state1 & 0xFFFF; if (!this->write_command(SEN5X_CMD_VOC_ALGORITHM_STATE, states, 4)) { ESP_LOGE(TAG, "Failed to set VOC baseline from saved state"); @@ -182,11 +194,11 @@ void SEN5XComponent::setup() { delay(20); uint16_t secs[2]; if (this->read_data(secs, 2)) { - auto_cleaning_interval_ = secs[0] << 16 | secs[1]; + this->auto_cleaning_interval_ = secs[0] << 16 | secs[1]; } } - if (acceleration_mode_.has_value()) { - result = this->write_command(SEN5X_CMD_RHT_ACCELERATION_MODE, acceleration_mode_.value()); + if (this->acceleration_mode_.has_value()) { + result = this->write_command(SEN5X_CMD_RHT_ACCELERATION_MODE, this->acceleration_mode_.value()); } else { result = this->write_command(SEN5X_CMD_RHT_ACCELERATION_MODE); } @@ -197,7 +209,7 @@ void SEN5XComponent::setup() { return; } delay(20); - if (!acceleration_mode_.has_value()) { + if (!this->acceleration_mode_.has_value()) { uint16_t mode; if (this->read_data(mode)) { this->acceleration_mode_ = RhtAccelerationMode(mode); @@ -227,19 +239,18 @@ void SEN5XComponent::setup() { } if (!this->write_command(cmd)) { - ESP_LOGE(TAG, "Error starting continuous measurements."); + ESP_LOGE(TAG, "Error starting continuous measurements"); this->error_code_ = MEASUREMENT_INIT_FAILED; this->mark_failed(); return; } - initialized_ = true; - ESP_LOGD(TAG, "Sensor initialized"); + this->initialized_ = true; }); }); } void SEN5XComponent::dump_config() { - ESP_LOGCONFIG(TAG, "sen5x:"); + ESP_LOGCONFIG(TAG, "SEN5X:"); LOG_I2C_DEVICE(this); if (this->is_failed()) { switch (this->error_code_) { @@ -247,16 +258,16 @@ void SEN5XComponent::dump_config() { ESP_LOGW(TAG, ESP_LOG_MSG_COMM_FAIL); break; case MEASUREMENT_INIT_FAILED: - ESP_LOGW(TAG, "Measurement Initialization failed"); + ESP_LOGW(TAG, "Measurement initialization failed"); break; case SERIAL_NUMBER_IDENTIFICATION_FAILED: - ESP_LOGW(TAG, "Unable to read sensor serial id"); + ESP_LOGW(TAG, "Unable to read serial ID"); break; case PRODUCT_NAME_FAILED: ESP_LOGW(TAG, "Unable to read product name"); break; case FIRMWARE_FAILED: - ESP_LOGW(TAG, "Unable to read sensor firmware version"); + ESP_LOGW(TAG, "Unable to read firmware version"); break; default: ESP_LOGW(TAG, "Unknown setup error"); @@ -264,26 +275,17 @@ void SEN5XComponent::dump_config() { } } ESP_LOGCONFIG(TAG, - " Productname: %s\n" + " Product name: %s\n" " Firmware version: %d\n" " Serial number %02d.%02d.%02d", - this->product_name_.c_str(), this->firmware_version_, serial_number_[0], serial_number_[1], - serial_number_[2]); + this->product_name_.c_str(), this->firmware_version_, this->serial_number_[0], this->serial_number_[1], + this->serial_number_[2]); if (this->auto_cleaning_interval_.has_value()) { - ESP_LOGCONFIG(TAG, " Auto cleaning interval %" PRId32 " seconds", auto_cleaning_interval_.value()); + ESP_LOGCONFIG(TAG, " Auto cleaning interval: %" PRId32 "s", this->auto_cleaning_interval_.value()); } if (this->acceleration_mode_.has_value()) { - switch (this->acceleration_mode_.value()) { - case LOW_ACCELERATION: - ESP_LOGCONFIG(TAG, " Low RH/T acceleration mode"); - break; - case MEDIUM_ACCELERATION: - ESP_LOGCONFIG(TAG, " Medium RH/T acceleration mode"); - break; - case HIGH_ACCELERATION: - ESP_LOGCONFIG(TAG, " High RH/T acceleration mode"); - break; - } + ESP_LOGCONFIG(TAG, " RH/T acceleration mode: %s", + LOG_STR_ARG(rht_accel_mode_to_string(this->acceleration_mode_.value()))); } LOG_UPDATE_INTERVAL(this); LOG_SENSOR(" ", "PM 1.0", this->pm_1_0_sensor_); @@ -297,7 +299,7 @@ void SEN5XComponent::dump_config() { } void SEN5XComponent::update() { - if (!initialized_) { + if (!this->initialized_) { return; } @@ -320,8 +322,8 @@ void SEN5XComponent::update() { this->voc_baselines_storage_.state1 = state1; if (this->pref_.save(&this->voc_baselines_storage_)) { - ESP_LOGI(TAG, "Stored VOC baseline state0: 0x%04" PRIX32 " ,state1: 0x%04" PRIX32, - this->voc_baselines_storage_.state0, voc_baselines_storage_.state1); + ESP_LOGI(TAG, "Stored VOC baseline state0: 0x%04" PRIX32 ", state1: 0x%04" PRIX32, + this->voc_baselines_storage_.state0, this->voc_baselines_storage_.state1); } else { ESP_LOGW(TAG, "Could not store VOC baselines"); } @@ -333,7 +335,7 @@ void SEN5XComponent::update() { if (!this->write_command(SEN5X_CMD_READ_MEASUREMENT)) { this->status_set_warning(); - ESP_LOGD(TAG, "write error read measurement (%d)", this->last_error_); + ESP_LOGD(TAG, "Write error: read measurement (%d)", this->last_error_); return; } this->set_timeout(20, [this]() { @@ -341,7 +343,7 @@ void SEN5XComponent::update() { if (!this->read_data(measurements, 8)) { this->status_set_warning(); - ESP_LOGD(TAG, "read data error (%d)", this->last_error_); + ESP_LOGD(TAG, "Read data error (%d)", this->last_error_); return; } @@ -413,7 +415,7 @@ bool SEN5XComponent::write_tuning_parameters_(uint16_t i2c_command, const GasTun params[5] = tuning.gain_factor; auto result = write_command(i2c_command, params, 6); if (!result) { - ESP_LOGE(TAG, "set tuning parameters failed. i2c command=%0xX, err=%d", i2c_command, this->last_error_); + ESP_LOGE(TAG, "Set tuning parameters failed (command=%0xX, err=%d)", i2c_command, this->last_error_); } return result; } @@ -424,7 +426,7 @@ bool SEN5XComponent::write_temperature_compensation_(const TemperatureCompensati params[1] = compensation.normalized_offset_slope; params[2] = compensation.time_constant; if (!write_command(SEN5X_CMD_TEMPERATURE_COMPENSATION, params, 3)) { - ESP_LOGE(TAG, "set temperature_compensation failed. Err=%d", this->last_error_); + ESP_LOGE(TAG, "Set temperature_compensation failed (%d)", this->last_error_); return false; } return true; @@ -433,7 +435,7 @@ bool SEN5XComponent::write_temperature_compensation_(const TemperatureCompensati bool SEN5XComponent::start_fan_cleaning() { if (!write_command(SEN5X_CMD_START_CLEANING_FAN)) { this->status_set_warning(); - ESP_LOGE(TAG, "write error start fan (%d)", this->last_error_); + ESP_LOGE(TAG, "Start fan cleaning failed (%d)", this->last_error_); return false; } else { ESP_LOGD(TAG, "Fan auto clean started"); diff --git a/esphome/components/sen5x/sen5x.h b/esphome/components/sen5x/sen5x.h index 0fa31605e6..9e5b6bf231 100644 --- a/esphome/components/sen5x/sen5x.h +++ b/esphome/components/sen5x/sen5x.h @@ -9,7 +9,7 @@ namespace esphome { namespace sen5x { -enum ERRORCODE { +enum ERRORCODE : uint8_t { COMMUNICATION_FAILED, SERIAL_NUMBER_IDENTIFICATION_FAILED, MEASUREMENT_INIT_FAILED, @@ -18,19 +18,17 @@ enum ERRORCODE { UNKNOWN }; -// Shortest time interval of 3H for storing baseline values. -// Prevents wear of the flash because of too many write operations -const uint32_t SHORTEST_BASELINE_STORE_INTERVAL = 10800; -// Store anyway if the baseline difference exceeds the max storage diff value -const uint32_t MAXIMUM_STORAGE_DIFF = 50; +enum RhtAccelerationMode : uint16_t { + LOW_ACCELERATION = 0, + MEDIUM_ACCELERATION = 1, + HIGH_ACCELERATION = 2, +}; struct Sen5xBaselines { int32_t state0; int32_t state1; } PACKED; // NOLINT -enum RhtAccelerationMode : uint16_t { LOW_ACCELERATION = 0, MEDIUM_ACCELERATION = 1, HIGH_ACCELERATION = 2 }; - struct GasTuning { uint16_t index_offset; uint16_t learning_time_offset_hours; @@ -46,6 +44,12 @@ struct TemperatureCompensation { uint16_t time_constant; }; +// Shortest time interval of 3H for storing baseline values. +// Prevents wear of the flash because of too many write operations +static const uint32_t SHORTEST_BASELINE_STORE_INTERVAL = 10800; +// Store anyway if the baseline difference exceeds the max storage diff value +static const uint32_t MAXIMUM_STORAGE_DIFF = 50; + class SEN5XComponent : public PollingComponent, public sensirion_common::SensirionI2CDevice { public: void setup() override; @@ -102,8 +106,14 @@ class SEN5XComponent : public PollingComponent, public sensirion_common::Sensiri protected: bool write_tuning_parameters_(uint16_t i2c_command, const GasTuning &tuning); bool write_temperature_compensation_(const TemperatureCompensation &compensation); + + uint32_t seconds_since_last_store_; + uint16_t firmware_version_; ERRORCODE error_code_; + uint8_t serial_number_[4]; bool initialized_{false}; + bool store_baseline_; + sensor::Sensor *pm_1_0_sensor_{nullptr}; sensor::Sensor *pm_2_5_sensor_{nullptr}; sensor::Sensor *pm_4_0_sensor_{nullptr}; @@ -115,18 +125,14 @@ class SEN5XComponent : public PollingComponent, public sensirion_common::Sensiri // SEN55 only sensor::Sensor *nox_sensor_{nullptr}; - std::string product_name_; - uint8_t serial_number_[4]; - uint16_t firmware_version_; - Sen5xBaselines voc_baselines_storage_; - bool store_baseline_; - uint32_t seconds_since_last_store_; - ESPPreferenceObject pref_; optional acceleration_mode_; optional auto_cleaning_interval_; optional voc_tuning_params_; optional nox_tuning_params_; optional temperature_compensation_; + ESPPreferenceObject pref_; + std::string product_name_; + Sen5xBaselines voc_baselines_storage_; }; } // namespace sen5x From 4d0993232083f9eb1154539977d309ca9614f608 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Sat, 6 Sep 2025 13:51:44 +1000 Subject: [PATCH 163/208] [kmeteriso] Fix i2c call (#10618) --- esphome/components/kmeteriso/kmeteriso.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/kmeteriso/kmeteriso.cpp b/esphome/components/kmeteriso/kmeteriso.cpp index d20e07460b..36f6d74ba0 100644 --- a/esphome/components/kmeteriso/kmeteriso.cpp +++ b/esphome/components/kmeteriso/kmeteriso.cpp @@ -33,7 +33,7 @@ void KMeterISOComponent::setup() { } uint8_t read_buf[4] = {1}; - if (!this->read_register(KMETER_ERROR_STATUS_REG, read_buf, 1)) { + if (!this->read_bytes(KMETER_ERROR_STATUS_REG, read_buf, 1)) { ESP_LOGCONFIG(TAG, "Could not read from the device."); this->error_code_ = COMMUNICATION_FAILED; this->mark_failed(); From c33bb3a8a9daa6aa4ac6aa34c115b4d2635f6708 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 6 Sep 2025 23:56:45 -0500 Subject: [PATCH 164/208] [esp8266] Store component warning strings in flash to reduce RAM usage (#10623) --- .../absolute_humidity/absolute_humidity.cpp | 2 +- esphome/components/aht10/aht10.cpp | 6 +++--- .../touchscreen/axs15231_touchscreen.cpp | 2 +- esphome/components/bl0942/bl0942.cpp | 2 +- .../components/dallas_temp/dallas_temp.cpp | 4 ++-- .../ethernet/ethernet_component.cpp | 2 +- esphome/components/gdk101/gdk101.cpp | 8 ++++---- .../gt911/touchscreen/gt911_touchscreen.cpp | 2 +- .../honeywellabp2_i2c/honeywellabp2.cpp | 8 ++++---- .../m5stack_8angle_binary_sensor.cpp | 2 +- .../sensor/m5stack_8angle_sensor.cpp | 2 +- esphome/components/max17043/max17043.cpp | 4 ++-- .../mcp23x08_base/mcp23x08_base.cpp | 2 +- .../mcp23x17_base/mcp23x17_base.cpp | 4 ++-- .../components/pi4ioe5v6408/pi4ioe5v6408.cpp | 14 +++++++------- esphome/components/sgp4x/sgp4x.cpp | 4 ++-- esphome/components/sht4x/sht4x.cpp | 2 +- .../components/sound_level/sound_level.cpp | 2 +- esphome/components/tca9555/tca9555.cpp | 10 +++++----- esphome/components/tmp1075/tmp1075.cpp | 2 +- esphome/components/udp/udp_component.cpp | 6 +++--- esphome/components/usb_uart/usb_uart.cpp | 2 +- .../components/wake_on_lan/wake_on_lan.cpp | 4 ++-- esphome/components/wifi/wifi_component.cpp | 6 +++--- esphome/core/component.cpp | 19 +++++++++++++++---- esphome/core/component.h | 4 ++++ 26 files changed, 70 insertions(+), 55 deletions(-) diff --git a/esphome/components/absolute_humidity/absolute_humidity.cpp b/esphome/components/absolute_humidity/absolute_humidity.cpp index 7ba3c5a1ab..2c5603ee3d 100644 --- a/esphome/components/absolute_humidity/absolute_humidity.cpp +++ b/esphome/components/absolute_humidity/absolute_humidity.cpp @@ -64,7 +64,7 @@ void AbsoluteHumidityComponent::loop() { ESP_LOGW(TAG, "No valid state from humidity sensor!"); } this->publish_state(NAN); - this->status_set_warning("Unable to calculate absolute humidity."); + this->status_set_warning(LOG_STR("Unable to calculate absolute humidity.")); return; } diff --git a/esphome/components/aht10/aht10.cpp b/esphome/components/aht10/aht10.cpp index 6202a27c42..53c712a7a7 100644 --- a/esphome/components/aht10/aht10.cpp +++ b/esphome/components/aht10/aht10.cpp @@ -96,7 +96,7 @@ void AHT10Component::read_data_() { ESP_LOGD(TAG, "Read attempt %d at %ums", this->read_count_, (unsigned) (millis() - this->start_time_)); } if (this->read(data, 6) != i2c::ERROR_OK) { - this->status_set_warning("Read failed, will retry"); + this->status_set_warning(LOG_STR("Read failed, will retry")); this->restart_read_(); return; } @@ -113,7 +113,7 @@ void AHT10Component::read_data_() { } else { ESP_LOGD(TAG, "Invalid humidity, retrying"); if (this->write(AHT10_MEASURE_CMD, sizeof(AHT10_MEASURE_CMD)) != i2c::ERROR_OK) { - this->status_set_warning(ESP_LOG_MSG_COMM_FAIL); + this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); } this->restart_read_(); return; @@ -144,7 +144,7 @@ void AHT10Component::update() { return; this->start_time_ = millis(); if (this->write(AHT10_MEASURE_CMD, sizeof(AHT10_MEASURE_CMD)) != i2c::ERROR_OK) { - this->status_set_warning(ESP_LOG_MSG_COMM_FAIL); + this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); return; } this->restart_read_(); diff --git a/esphome/components/axs15231/touchscreen/axs15231_touchscreen.cpp b/esphome/components/axs15231/touchscreen/axs15231_touchscreen.cpp index 6304516164..ab3f1dad4f 100644 --- a/esphome/components/axs15231/touchscreen/axs15231_touchscreen.cpp +++ b/esphome/components/axs15231/touchscreen/axs15231_touchscreen.cpp @@ -12,7 +12,7 @@ constexpr static const uint8_t AXS_READ_TOUCHPAD[11] = {0xb5, 0xab, 0xa5, 0x5a, #define ERROR_CHECK(err) \ if ((err) != i2c::ERROR_OK) { \ - this->status_set_warning("Failed to communicate"); \ + this->status_set_warning(LOG_STR("Failed to communicate")); \ return; \ } diff --git a/esphome/components/bl0942/bl0942.cpp b/esphome/components/bl0942/bl0942.cpp index 86eff57147..894fcbfbb7 100644 --- a/esphome/components/bl0942/bl0942.cpp +++ b/esphome/components/bl0942/bl0942.cpp @@ -149,7 +149,7 @@ void BL0942::setup() { this->write_reg_(BL0942_REG_USR_WRPROT, 0); if (this->read_reg_(BL0942_REG_MODE) != mode) - this->status_set_warning("BL0942 setup failed!"); + this->status_set_warning(LOG_STR("BL0942 setup failed!")); this->flush(); } diff --git a/esphome/components/dallas_temp/dallas_temp.cpp b/esphome/components/dallas_temp/dallas_temp.cpp index 5cd6063893..a518c96489 100644 --- a/esphome/components/dallas_temp/dallas_temp.cpp +++ b/esphome/components/dallas_temp/dallas_temp.cpp @@ -64,7 +64,7 @@ bool DallasTemperatureSensor::read_scratch_pad_() { } } else { ESP_LOGW(TAG, "'%s' - reading scratch pad failed bus reset", this->get_name().c_str()); - this->status_set_warning("bus reset failed"); + this->status_set_warning(LOG_STR("bus reset failed")); } return success; } @@ -124,7 +124,7 @@ bool DallasTemperatureSensor::check_scratch_pad_() { crc8(this->scratch_pad_, 8)); #endif if (!chksum_validity) { - this->status_set_warning("scratch pad checksum invalid"); + this->status_set_warning(LOG_STR("scratch pad checksum invalid")); ESP_LOGD(TAG, "Scratch pad: %02X.%02X.%02X.%02X.%02X.%02X.%02X.%02X.%02X (%02X)", this->scratch_pad_[0], this->scratch_pad_[1], this->scratch_pad_[2], this->scratch_pad_[3], this->scratch_pad_[4], this->scratch_pad_[5], this->scratch_pad_[6], this->scratch_pad_[7], this->scratch_pad_[8], diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index 87913488da..844a30bd8b 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -492,7 +492,7 @@ void EthernetComponent::start_connect_() { global_eth_component->ipv6_count_ = 0; #endif /* USE_NETWORK_IPV6 */ this->connect_begin_ = millis(); - this->status_set_warning("waiting for IP configuration"); + this->status_set_warning(LOG_STR("waiting for IP configuration")); esp_err_t err; err = esp_netif_set_hostname(this->eth_netif_, App.get_name().c_str()); diff --git a/esphome/components/gdk101/gdk101.cpp b/esphome/components/gdk101/gdk101.cpp index 096b06917a..4c156ab24b 100644 --- a/esphome/components/gdk101/gdk101.cpp +++ b/esphome/components/gdk101/gdk101.cpp @@ -11,22 +11,22 @@ static const uint8_t NUMBER_OF_READ_RETRIES = 5; void GDK101Component::update() { uint8_t data[2]; if (!this->read_dose_1m_(data)) { - this->status_set_warning("Failed to read dose 1m"); + this->status_set_warning(LOG_STR("Failed to read dose 1m")); return; } if (!this->read_dose_10m_(data)) { - this->status_set_warning("Failed to read dose 10m"); + this->status_set_warning(LOG_STR("Failed to read dose 10m")); return; } if (!this->read_status_(data)) { - this->status_set_warning("Failed to read status"); + this->status_set_warning(LOG_STR("Failed to read status")); return; } if (!this->read_measurement_duration_(data)) { - this->status_set_warning("Failed to read measurement duration"); + this->status_set_warning(LOG_STR("Failed to read measurement duration")); return; } this->status_clear_warning(); diff --git a/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp b/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp index 07218843dd..4810867d4b 100644 --- a/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp +++ b/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp @@ -20,7 +20,7 @@ static const size_t MAX_BUTTONS = 4; // max number of buttons scanned #define ERROR_CHECK(err) \ if ((err) != i2c::ERROR_OK) { \ - this->status_set_warning(ESP_LOG_MSG_COMM_FAIL); \ + this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); \ return; \ } diff --git a/esphome/components/honeywellabp2_i2c/honeywellabp2.cpp b/esphome/components/honeywellabp2_i2c/honeywellabp2.cpp index 11f5dbc314..f173a1afbd 100644 --- a/esphome/components/honeywellabp2_i2c/honeywellabp2.cpp +++ b/esphome/components/honeywellabp2_i2c/honeywellabp2.cpp @@ -15,7 +15,7 @@ static const char *const TAG = "honeywellabp2"; void HONEYWELLABP2Sensor::read_sensor_data() { if (this->read(raw_data_, 7) != i2c::ERROR_OK) { ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); - this->status_set_warning("couldn't read sensor data"); + this->status_set_warning(LOG_STR("couldn't read sensor data")); return; } float press_counts = encode_uint24(raw_data_[1], raw_data_[2], raw_data_[3]); // calculate digital pressure counts @@ -31,7 +31,7 @@ void HONEYWELLABP2Sensor::read_sensor_data() { void HONEYWELLABP2Sensor::start_measurement() { if (this->write(i2c_cmd_, 3) != i2c::ERROR_OK) { ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); - this->status_set_warning("couldn't start measurement"); + this->status_set_warning(LOG_STR("couldn't start measurement")); return; } this->measurement_running_ = true; @@ -40,7 +40,7 @@ void HONEYWELLABP2Sensor::start_measurement() { bool HONEYWELLABP2Sensor::is_measurement_ready() { if (this->read(raw_data_, 1) != i2c::ERROR_OK) { ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); - this->status_set_warning("couldn't check measurement"); + this->status_set_warning(LOG_STR("couldn't check measurement")); return false; } if ((raw_data_[0] & (0x1 << STATUS_BIT_BUSY)) > 0) { @@ -53,7 +53,7 @@ bool HONEYWELLABP2Sensor::is_measurement_ready() { void HONEYWELLABP2Sensor::measurement_timeout() { ESP_LOGE(TAG, "Timeout!"); this->measurement_running_ = false; - this->status_set_warning("measurement timed out"); + this->status_set_warning(LOG_STR("measurement timed out")); } float HONEYWELLABP2Sensor::get_pressure() { return this->last_pressure_; } diff --git a/esphome/components/m5stack_8angle/binary_sensor/m5stack_8angle_binary_sensor.cpp b/esphome/components/m5stack_8angle/binary_sensor/m5stack_8angle_binary_sensor.cpp index 2f68d9f254..3eeba4a644 100644 --- a/esphome/components/m5stack_8angle/binary_sensor/m5stack_8angle_binary_sensor.cpp +++ b/esphome/components/m5stack_8angle/binary_sensor/m5stack_8angle_binary_sensor.cpp @@ -6,7 +6,7 @@ namespace m5stack_8angle { void M5Stack8AngleSwitchBinarySensor::update() { int8_t out = this->parent_->read_switch(); if (out == -1) { - this->status_set_warning("Could not read binary sensor state from M5Stack 8Angle."); + this->status_set_warning(LOG_STR("Could not read binary sensor state from M5Stack 8Angle.")); return; } this->publish_state(out != 0); diff --git a/esphome/components/m5stack_8angle/sensor/m5stack_8angle_sensor.cpp b/esphome/components/m5stack_8angle/sensor/m5stack_8angle_sensor.cpp index 5e034f1dd3..d22b345141 100644 --- a/esphome/components/m5stack_8angle/sensor/m5stack_8angle_sensor.cpp +++ b/esphome/components/m5stack_8angle/sensor/m5stack_8angle_sensor.cpp @@ -7,7 +7,7 @@ void M5Stack8AngleKnobSensor::update() { if (this->parent_ != nullptr) { int32_t raw_pos = this->parent_->read_knob_pos_raw(this->channel_, this->bits_); if (raw_pos == -1) { - this->status_set_warning("Could not read knob position from M5Stack 8Angle."); + this->status_set_warning(LOG_STR("Could not read knob position from M5Stack 8Angle.")); return; } if (this->raw_) { diff --git a/esphome/components/max17043/max17043.cpp b/esphome/components/max17043/max17043.cpp index 8f486de6b7..f605fb1324 100644 --- a/esphome/components/max17043/max17043.cpp +++ b/esphome/components/max17043/max17043.cpp @@ -22,7 +22,7 @@ void MAX17043Component::update() { if (this->voltage_sensor_ != nullptr) { if (!this->read_byte_16(MAX17043_VCELL, &raw_voltage)) { - this->status_set_warning("Unable to read MAX17043_VCELL"); + this->status_set_warning(LOG_STR("Unable to read MAX17043_VCELL")); } else { float voltage = (1.25 * (float) (raw_voltage >> 4)) / 1000.0; this->voltage_sensor_->publish_state(voltage); @@ -31,7 +31,7 @@ void MAX17043Component::update() { } if (this->battery_remaining_sensor_ != nullptr) { if (!this->read_byte_16(MAX17043_SOC, &raw_percent)) { - this->status_set_warning("Unable to read MAX17043_SOC"); + this->status_set_warning(LOG_STR("Unable to read MAX17043_SOC")); } else { float percent = (float) ((raw_percent >> 8) + 0.003906f * (raw_percent & 0x00ff)); this->battery_remaining_sensor_->publish_state(percent); diff --git a/esphome/components/mcp23x08_base/mcp23x08_base.cpp b/esphome/components/mcp23x08_base/mcp23x08_base.cpp index e4fb51174b..1593c376cd 100644 --- a/esphome/components/mcp23x08_base/mcp23x08_base.cpp +++ b/esphome/components/mcp23x08_base/mcp23x08_base.cpp @@ -8,7 +8,7 @@ static const char *const TAG = "mcp23x08_base"; bool MCP23X08Base::digital_read_hw(uint8_t pin) { if (!this->read_reg(mcp23x08_base::MCP23X08_GPIO, &this->input_mask_)) { - this->status_set_warning(ESP_LOG_MSG_COMM_FAIL); + this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); return false; } return true; diff --git a/esphome/components/mcp23x17_base/mcp23x17_base.cpp b/esphome/components/mcp23x17_base/mcp23x17_base.cpp index 020b8a5ddf..b1f1f260b4 100644 --- a/esphome/components/mcp23x17_base/mcp23x17_base.cpp +++ b/esphome/components/mcp23x17_base/mcp23x17_base.cpp @@ -11,13 +11,13 @@ bool MCP23X17Base::digital_read_hw(uint8_t pin) { uint8_t data; if (pin < 8) { if (!this->read_reg(mcp23x17_base::MCP23X17_GPIOA, &data)) { - this->status_set_warning(ESP_LOG_MSG_COMM_FAIL); + this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); return false; } this->input_mask_ = encode_uint16(this->input_mask_ >> 8, data); } else { if (!this->read_reg(mcp23x17_base::MCP23X17_GPIOB, &data)) { - this->status_set_warning(ESP_LOG_MSG_COMM_FAIL); + this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); return false; } this->input_mask_ = encode_uint16(data, this->input_mask_ & 0xFF); diff --git a/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp index 18acfda934..517ca833e6 100644 --- a/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp +++ b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp @@ -68,7 +68,7 @@ bool PI4IOE5V6408Component::read_gpio_outputs_() { uint8_t data; if (!this->read_byte(PI4IOE5V6408_REGISTER_OUT_SET, &data)) { - this->status_set_warning("Failed to read output register"); + this->status_set_warning(LOG_STR("Failed to read output register")); return false; } this->output_mask_ = data; @@ -82,7 +82,7 @@ bool PI4IOE5V6408Component::read_gpio_modes_() { uint8_t data; if (!this->read_byte(PI4IOE5V6408_REGISTER_IO_DIR, &data)) { - this->status_set_warning("Failed to read GPIO modes"); + this->status_set_warning(LOG_STR("Failed to read GPIO modes")); return false; } #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE @@ -99,7 +99,7 @@ bool PI4IOE5V6408Component::digital_read_hw(uint8_t pin) { uint8_t data; if (!this->read_byte(PI4IOE5V6408_REGISTER_IN_STATE, &data)) { - this->status_set_warning("Failed to read GPIO state"); + this->status_set_warning(LOG_STR("Failed to read GPIO state")); return false; } this->input_mask_ = data; @@ -117,7 +117,7 @@ void PI4IOE5V6408Component::digital_write_hw(uint8_t pin, bool value) { this->output_mask_ &= ~(1 << pin); } if (!this->write_byte(PI4IOE5V6408_REGISTER_OUT_SET, this->output_mask_)) { - this->status_set_warning("Failed to write output register"); + this->status_set_warning(LOG_STR("Failed to write output register")); return; } #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE @@ -131,15 +131,15 @@ bool PI4IOE5V6408Component::write_gpio_modes_() { return false; if (!this->write_byte(PI4IOE5V6408_REGISTER_IO_DIR, this->mode_mask_)) { - this->status_set_warning("Failed to write GPIO modes"); + this->status_set_warning(LOG_STR("Failed to write GPIO modes")); return false; } if (!this->write_byte(PI4IOE5V6408_REGISTER_PULL_SELECT, this->pull_up_down_mask_)) { - this->status_set_warning("Failed to write GPIO pullup/pulldown"); + this->status_set_warning(LOG_STR("Failed to write GPIO pullup/pulldown")); return false; } if (!this->write_byte(PI4IOE5V6408_REGISTER_PULL_ENABLE, this->pull_enable_mask_)) { - this->status_set_warning("Failed to write GPIO pull enable"); + this->status_set_warning(LOG_STR("Failed to write GPIO pull enable")); return false; } #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE diff --git a/esphome/components/sgp4x/sgp4x.cpp b/esphome/components/sgp4x/sgp4x.cpp index da52993a87..99d88006f7 100644 --- a/esphome/components/sgp4x/sgp4x.cpp +++ b/esphome/components/sgp4x/sgp4x.cpp @@ -211,7 +211,7 @@ void SGP4xComponent::measure_raw_() { if (!this->write_command(command, data, 2)) { ESP_LOGD(TAG, "write error (%d)", this->last_error_); - this->status_set_warning("measurement request failed"); + this->status_set_warning(LOG_STR("measurement request failed")); return; } @@ -220,7 +220,7 @@ void SGP4xComponent::measure_raw_() { raw_data[1] = 0; if (!this->read_data(raw_data, response_words)) { ESP_LOGD(TAG, "read error (%d)", this->last_error_); - this->status_set_warning("measurement read failed"); + this->status_set_warning(LOG_STR("measurement read failed")); this->voc_index_ = this->nox_index_ = UINT16_MAX; return; } diff --git a/esphome/components/sht4x/sht4x.cpp b/esphome/components/sht4x/sht4x.cpp index 637c8c1a9d..62b8717ded 100644 --- a/esphome/components/sht4x/sht4x.cpp +++ b/esphome/components/sht4x/sht4x.cpp @@ -65,7 +65,7 @@ void SHT4XComponent::update() { // Send command if (!this->write_command(MEASURECOMMANDS[this->precision_])) { // Warning will be printed only if warning status is not set yet - this->status_set_warning("Failed to send measurement command"); + this->status_set_warning(LOG_STR("Failed to send measurement command")); return; } diff --git a/esphome/components/sound_level/sound_level.cpp b/esphome/components/sound_level/sound_level.cpp index decf630aba..db6b168bbc 100644 --- a/esphome/components/sound_level/sound_level.cpp +++ b/esphome/components/sound_level/sound_level.cpp @@ -56,7 +56,7 @@ void SoundLevelComponent::loop() { } } else { if (!this->status_has_warning()) { - this->status_set_warning("Microphone isn't running, can't compute statistics"); + this->status_set_warning(LOG_STR("Microphone isn't running, can't compute statistics")); // Deallocate buffers, if necessary this->stop_(); diff --git a/esphome/components/tca9555/tca9555.cpp b/esphome/components/tca9555/tca9555.cpp index b4a04d5b0b..c3449ce254 100644 --- a/esphome/components/tca9555/tca9555.cpp +++ b/esphome/components/tca9555/tca9555.cpp @@ -50,7 +50,7 @@ bool TCA9555Component::read_gpio_outputs_() { return false; uint8_t data[2]; if (!this->read_bytes(TCA9555_OUTPUT_PORT_REGISTER_0, data, 2)) { - this->status_set_warning("Failed to read output register"); + this->status_set_warning(LOG_STR("Failed to read output register")); return false; } this->output_mask_ = (uint16_t(data[1]) << 8) | (uint16_t(data[0]) << 0); @@ -64,7 +64,7 @@ bool TCA9555Component::read_gpio_modes_() { uint8_t data[2]; bool success = this->read_bytes(TCA9555_CONFIGURATION_PORT_0, data, 2); if (!success) { - this->status_set_warning("Failed to read mode register"); + this->status_set_warning(LOG_STR("Failed to read mode register")); return false; } this->mode_mask_ = (uint16_t(data[1]) << 8) | (uint16_t(data[0]) << 0); @@ -79,7 +79,7 @@ bool TCA9555Component::digital_read_hw(uint8_t pin) { uint8_t bank_number = pin < 8 ? 0 : 1; uint8_t register_to_read = bank_number ? TCA9555_INPUT_PORT_REGISTER_1 : TCA9555_INPUT_PORT_REGISTER_0; if (!this->read_bytes(register_to_read, &data, 1)) { - this->status_set_warning("Failed to read input register"); + this->status_set_warning(LOG_STR("Failed to read input register")); return false; } uint8_t second_half = this->input_mask_ >> 8; @@ -108,7 +108,7 @@ void TCA9555Component::digital_write_hw(uint8_t pin, bool value) { data[0] = this->output_mask_; data[1] = this->output_mask_ >> 8; if (!this->write_bytes(TCA9555_OUTPUT_PORT_REGISTER_0, data, 2)) { - this->status_set_warning("Failed to write output register"); + this->status_set_warning(LOG_STR("Failed to write output register")); return; } @@ -123,7 +123,7 @@ bool TCA9555Component::write_gpio_modes_() { data[0] = this->mode_mask_; data[1] = this->mode_mask_ >> 8; if (!this->write_bytes(TCA9555_CONFIGURATION_PORT_0, data, 2)) { - this->status_set_warning("Failed to write mode register"); + this->status_set_warning(LOG_STR("Failed to write mode register")); return false; } this->status_clear_warning(); diff --git a/esphome/components/tmp1075/tmp1075.cpp b/esphome/components/tmp1075/tmp1075.cpp index 831f905bd2..1d9b384c66 100644 --- a/esphome/components/tmp1075/tmp1075.cpp +++ b/esphome/components/tmp1075/tmp1075.cpp @@ -32,7 +32,7 @@ void TMP1075Sensor::update() { uint16_t regvalue; if (!read_byte_16(REG_TEMP, ®value)) { ESP_LOGW(TAG, "'%s' - unable to read temperature register", this->name_.c_str()); - this->status_set_warning("can't read"); + this->status_set_warning(LOG_STR("can't read")); return; } this->status_clear_warning(); diff --git a/esphome/components/udp/udp_component.cpp b/esphome/components/udp/udp_component.cpp index 62a1189355..8a9ce612b4 100644 --- a/esphome/components/udp/udp_component.cpp +++ b/esphome/components/udp/udp_component.cpp @@ -28,12 +28,12 @@ void UDPComponent::setup() { int enable = 1; auto err = this->broadcast_socket_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)); if (err != 0) { - this->status_set_warning("Socket unable to set reuseaddr"); + this->status_set_warning(LOG_STR("Socket unable to set reuseaddr")); // we can still continue } err = this->broadcast_socket_->setsockopt(SOL_SOCKET, SO_BROADCAST, &enable, sizeof(int)); if (err != 0) { - this->status_set_warning("Socket unable to set broadcast"); + this->status_set_warning(LOG_STR("Socket unable to set broadcast")); } } // create listening socket if we either want to subscribe to providers, or need to listen @@ -55,7 +55,7 @@ void UDPComponent::setup() { int enable = 1; err = this->listen_socket_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(enable)); if (err != 0) { - this->status_set_warning("Socket unable to set reuseaddr"); + this->status_set_warning(LOG_STR("Socket unable to set reuseaddr")); // we can still continue } struct sockaddr_in server {}; diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index 934306f480..bf1c9086f1 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -266,7 +266,7 @@ void USBUartTypeCdcAcm::on_connected() { for (auto *channel : this->channels_) { if (i == cdc_devs.size()) { ESP_LOGE(TAG, "No configuration found for channel %d", channel->index_); - this->status_set_warning("No configuration found for channel"); + this->status_set_warning(LOG_STR("No configuration found for channel")); break; } channel->cdc_dev_ = cdc_devs[i++]; diff --git a/esphome/components/wake_on_lan/wake_on_lan.cpp b/esphome/components/wake_on_lan/wake_on_lan.cpp index bed098755a..adf5a080e5 100644 --- a/esphome/components/wake_on_lan/wake_on_lan.cpp +++ b/esphome/components/wake_on_lan/wake_on_lan.cpp @@ -74,12 +74,12 @@ void WakeOnLanButton::setup() { int enable = 1; auto err = this->broadcast_socket_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)); if (err != 0) { - this->status_set_warning("Socket unable to set reuseaddr"); + this->status_set_warning(LOG_STR("Socket unable to set reuseaddr")); // we can still continue } err = this->broadcast_socket_->setsockopt(SOL_SOCKET, SO_BROADCAST, &enable, sizeof(int)); if (err != 0) { - this->status_set_warning("Socket unable to set broadcast"); + this->status_set_warning(LOG_STR("Socket unable to set broadcast")); } #endif } diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index d16c94fa13..e57bf25b8c 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -148,7 +148,7 @@ void WiFiComponent::loop() { switch (this->state_) { case WIFI_COMPONENT_STATE_COOLDOWN: { - this->status_set_warning("waiting to reconnect"); + this->status_set_warning(LOG_STR("waiting to reconnect")); if (millis() - this->action_started_ > 5000) { if (this->fast_connect_ || this->retry_hidden_) { if (!this->selected_ap_.get_bssid().has_value()) @@ -161,13 +161,13 @@ void WiFiComponent::loop() { break; } case WIFI_COMPONENT_STATE_STA_SCANNING: { - this->status_set_warning("scanning for networks"); + this->status_set_warning(LOG_STR("scanning for networks")); this->check_scanning_finished(); break; } case WIFI_COMPONENT_STATE_STA_CONNECTING: case WIFI_COMPONENT_STATE_STA_CONNECTING_2: { - this->status_set_warning("associating to network"); + this->status_set_warning(LOG_STR("associating to network")); this->check_connecting_finished(); break; } diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 40cda17ca3..e30dab2508 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -16,7 +16,6 @@ namespace esphome { static const char *const TAG = "component"; -static const char *const UNSPECIFIED_MESSAGE = "unspecified"; // Global vectors for component data that doesn't belong in every instance. // Using vector instead of unordered_map for both because: @@ -143,7 +142,7 @@ void Component::call_dump_config() { } } ESP_LOGE(TAG, " %s is marked FAILED: %s", this->get_component_source(), - error_msg ? error_msg : UNSPECIFIED_MESSAGE); + error_msg ? error_msg : LOG_STR_LITERAL("unspecified")); } } @@ -280,20 +279,32 @@ bool Component::is_ready() const { bool Component::can_proceed() { return true; } bool Component::status_has_warning() const { return this->component_state_ & STATUS_LED_WARNING; } bool Component::status_has_error() const { return this->component_state_ & STATUS_LED_ERROR; } + void Component::status_set_warning(const char *message) { // Don't spam the log. This risks missing different warning messages though. if ((this->component_state_ & STATUS_LED_WARNING) != 0) return; this->component_state_ |= STATUS_LED_WARNING; App.app_state_ |= STATUS_LED_WARNING; - ESP_LOGW(TAG, "%s set Warning flag: %s", this->get_component_source(), message ? message : UNSPECIFIED_MESSAGE); + ESP_LOGW(TAG, "%s set Warning flag: %s", this->get_component_source(), + message ? message : LOG_STR_LITERAL("unspecified")); +} +void Component::status_set_warning(const LogString *message) { + // Don't spam the log. This risks missing different warning messages though. + if ((this->component_state_ & STATUS_LED_WARNING) != 0) + return; + this->component_state_ |= STATUS_LED_WARNING; + App.app_state_ |= STATUS_LED_WARNING; + ESP_LOGW(TAG, "%s set Warning flag: %s", this->get_component_source(), + message ? LOG_STR_ARG(message) : LOG_STR_LITERAL("unspecified")); } void Component::status_set_error(const char *message) { if ((this->component_state_ & STATUS_LED_ERROR) != 0) return; this->component_state_ |= STATUS_LED_ERROR; App.app_state_ |= STATUS_LED_ERROR; - ESP_LOGE(TAG, "%s set Error flag: %s", this->get_component_source(), message ? message : UNSPECIFIED_MESSAGE); + ESP_LOGE(TAG, "%s set Error flag: %s", this->get_component_source(), + message ? message : LOG_STR_LITERAL("unspecified")); if (message != nullptr) { // Lazy allocate the error messages vector if needed if (!component_error_messages) { diff --git a/esphome/core/component.h b/esphome/core/component.h index 096c6f9c69..a363fceb85 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -9,6 +9,9 @@ namespace esphome { +// Forward declaration for LogString +struct LogString; + /** Default setup priorities for components of different types. * * Components should return one of these setup priorities in get_setup_priority. @@ -203,6 +206,7 @@ class Component { bool status_has_error() const; void status_set_warning(const char *message = nullptr); + void status_set_warning(const LogString *message); void status_set_error(const char *message = nullptr); From a8b8507ffcfd43ddeb09bd8f2b51daef94681290 Mon Sep 17 00:00:00 2001 From: davidmonro Date: Mon, 8 Sep 2025 06:06:10 +1000 Subject: [PATCH 165/208] Atm90e32/26 device class fixes (#10629) --- esphome/components/atm90e26/sensor.py | 2 ++ esphome/components/atm90e32/sensor.py | 6 ++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/esphome/components/atm90e26/sensor.py b/esphome/components/atm90e26/sensor.py index 42ef259100..4522e94846 100644 --- a/esphome/components/atm90e26/sensor.py +++ b/esphome/components/atm90e26/sensor.py @@ -16,6 +16,7 @@ from esphome.const import ( DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, DEVICE_CLASS_POWER_FACTOR, + DEVICE_CLASS_REACTIVE_POWER, DEVICE_CLASS_VOLTAGE, ICON_CURRENT_AC, ICON_LIGHTBULB, @@ -78,6 +79,7 @@ CONFIG_SCHEMA = ( unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE, icon=ICON_LIGHTBULB, accuracy_decimals=2, + device_class=DEVICE_CLASS_REACTIVE_POWER, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_POWER_FACTOR): sensor.sensor_schema( diff --git a/esphome/components/atm90e32/sensor.py b/esphome/components/atm90e32/sensor.py index 7cdbd69f56..a510095217 100644 --- a/esphome/components/atm90e32/sensor.py +++ b/esphome/components/atm90e32/sensor.py @@ -17,10 +17,12 @@ from esphome.const import ( CONF_REACTIVE_POWER, CONF_REVERSE_ACTIVE_ENERGY, CONF_VOLTAGE, + DEVICE_CLASS_APPARENT_POWER, DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, DEVICE_CLASS_POWER_FACTOR, + DEVICE_CLASS_REACTIVE_POWER, DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_VOLTAGE, ENTITY_CATEGORY_DIAGNOSTIC, @@ -100,13 +102,13 @@ ATM90E32_PHASE_SCHEMA = cv.Schema( unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE, icon=ICON_LIGHTBULB, accuracy_decimals=2, - device_class=DEVICE_CLASS_POWER, + device_class=DEVICE_CLASS_REACTIVE_POWER, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_APPARENT_POWER): sensor.sensor_schema( unit_of_measurement=UNIT_VOLT_AMPS, accuracy_decimals=2, - device_class=DEVICE_CLASS_POWER, + device_class=DEVICE_CLASS_APPARENT_POWER, state_class=STATE_CLASS_MEASUREMENT, ), cv.Optional(CONF_POWER_FACTOR): sensor.sensor_schema( From 0c737fc4df3af24d985dcea10e5ea7d3e250f806 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 7 Sep 2025 16:09:15 -0500 Subject: [PATCH 166/208] [core] Convert LOG_UPDATE_INTERVAL macro to function to reduce flash usage (#10636) --- esphome/components/bedjet/bedjet_hub.cpp | 2 +- esphome/components/ccs811/ccs811.cpp | 2 +- .../grove_gas_mc_v2/grove_gas_mc_v2.cpp | 2 +- esphome/components/hlw8012/hlw8012.cpp | 2 +- esphome/components/pulse_width/pulse_width.cpp | 2 +- esphome/components/ufire_ec/ufire_ec.cpp | 2 +- esphome/components/ufire_ise/ufire_ise.cpp | 2 +- .../waveshare_epaper/waveshare_213v3.cpp | 2 +- esphome/core/component.cpp | 12 ++++++++++++ esphome/core/component.h | 15 +++++++-------- 10 files changed, 27 insertions(+), 16 deletions(-) diff --git a/esphome/components/bedjet/bedjet_hub.cpp b/esphome/components/bedjet/bedjet_hub.cpp index 007ca1ca7d..38fcf29b3b 100644 --- a/esphome/components/bedjet/bedjet_hub.cpp +++ b/esphome/components/bedjet/bedjet_hub.cpp @@ -493,7 +493,7 @@ void BedJetHub::dump_config() { " ble_client.app_id: %d\n" " ble_client.conn_id: %d", this->get_name().c_str(), this->parent()->app_id, this->parent()->get_conn_id()); - LOG_UPDATE_INTERVAL(this) + LOG_UPDATE_INTERVAL(this); ESP_LOGCONFIG(TAG, " Child components (%d):", this->children_.size()); for (auto *child : this->children_) { ESP_LOGCONFIG(TAG, " - %s", child->describe().c_str()); diff --git a/esphome/components/ccs811/ccs811.cpp b/esphome/components/ccs811/ccs811.cpp index 2617d7577a..40c5318339 100644 --- a/esphome/components/ccs811/ccs811.cpp +++ b/esphome/components/ccs811/ccs811.cpp @@ -152,7 +152,7 @@ void CCS811Component::send_env_data_() { void CCS811Component::dump_config() { ESP_LOGCONFIG(TAG, "CCS811"); LOG_I2C_DEVICE(this) - LOG_UPDATE_INTERVAL(this) + LOG_UPDATE_INTERVAL(this); LOG_SENSOR(" ", "CO2 Sensor", this->co2_); LOG_SENSOR(" ", "TVOC Sensor", this->tvoc_); LOG_TEXT_SENSOR(" ", "Firmware Version Sensor", this->version_) diff --git a/esphome/components/grove_gas_mc_v2/grove_gas_mc_v2.cpp b/esphome/components/grove_gas_mc_v2/grove_gas_mc_v2.cpp index 52ec8433a2..b0f3429314 100644 --- a/esphome/components/grove_gas_mc_v2/grove_gas_mc_v2.cpp +++ b/esphome/components/grove_gas_mc_v2/grove_gas_mc_v2.cpp @@ -57,7 +57,7 @@ void GroveGasMultichannelV2Component::update() { void GroveGasMultichannelV2Component::dump_config() { ESP_LOGCONFIG(TAG, "Grove Multichannel Gas Sensor V2"); LOG_I2C_DEVICE(this) - LOG_UPDATE_INTERVAL(this) + LOG_UPDATE_INTERVAL(this); LOG_SENSOR(" ", "Nitrogen Dioxide", this->nitrogen_dioxide_sensor_); LOG_SENSOR(" ", "Ethanol", this->ethanol_sensor_); LOG_SENSOR(" ", "Carbon Monoxide", this->carbon_monoxide_sensor_); diff --git a/esphome/components/hlw8012/hlw8012.cpp b/esphome/components/hlw8012/hlw8012.cpp index f293185cce..73696bd2a5 100644 --- a/esphome/components/hlw8012/hlw8012.cpp +++ b/esphome/components/hlw8012/hlw8012.cpp @@ -42,7 +42,7 @@ void HLW8012Component::dump_config() { " Current resistor: %.1f mΩ\n" " Voltage Divider: %.1f", this->change_mode_every_, this->current_resistor_ * 1000.0f, this->voltage_divider_); - LOG_UPDATE_INTERVAL(this) + LOG_UPDATE_INTERVAL(this); LOG_SENSOR(" ", "Voltage", this->voltage_sensor_); LOG_SENSOR(" ", "Current", this->current_sensor_); LOG_SENSOR(" ", "Power", this->power_sensor_); diff --git a/esphome/components/pulse_width/pulse_width.cpp b/esphome/components/pulse_width/pulse_width.cpp index c086ceaa23..d083d48b32 100644 --- a/esphome/components/pulse_width/pulse_width.cpp +++ b/esphome/components/pulse_width/pulse_width.cpp @@ -18,7 +18,7 @@ void IRAM_ATTR PulseWidthSensorStore::gpio_intr(PulseWidthSensorStore *arg) { void PulseWidthSensor::dump_config() { LOG_SENSOR("", "Pulse Width", this); - LOG_UPDATE_INTERVAL(this) + LOG_UPDATE_INTERVAL(this); LOG_PIN(" Pin: ", this->pin_); } void PulseWidthSensor::update() { diff --git a/esphome/components/ufire_ec/ufire_ec.cpp b/esphome/components/ufire_ec/ufire_ec.cpp index 9e0055a2cc..0a57ecc67b 100644 --- a/esphome/components/ufire_ec/ufire_ec.cpp +++ b/esphome/components/ufire_ec/ufire_ec.cpp @@ -104,7 +104,7 @@ void UFireECComponent::write_data_(uint8_t reg, float data) { void UFireECComponent::dump_config() { ESP_LOGCONFIG(TAG, "uFire-EC"); LOG_I2C_DEVICE(this) - LOG_UPDATE_INTERVAL(this) + LOG_UPDATE_INTERVAL(this); LOG_SENSOR(" ", "EC Sensor", this->ec_sensor_); LOG_SENSOR(" ", "Temperature Sensor", this->temperature_sensor_); LOG_SENSOR(" ", "Temperature Sensor external", this->temperature_sensor_external_); diff --git a/esphome/components/ufire_ise/ufire_ise.cpp b/esphome/components/ufire_ise/ufire_ise.cpp index 9e0e7e265d..486a506391 100644 --- a/esphome/components/ufire_ise/ufire_ise.cpp +++ b/esphome/components/ufire_ise/ufire_ise.cpp @@ -141,7 +141,7 @@ void UFireISEComponent::write_data_(uint8_t reg, float data) { void UFireISEComponent::dump_config() { ESP_LOGCONFIG(TAG, "uFire-ISE"); LOG_I2C_DEVICE(this) - LOG_UPDATE_INTERVAL(this) + LOG_UPDATE_INTERVAL(this); LOG_SENSOR(" ", "PH Sensor", this->ph_sensor_); LOG_SENSOR(" ", "Temperature Sensor", this->temperature_sensor_); LOG_SENSOR(" ", "Temperature Sensor external", this->temperature_sensor_external_); diff --git a/esphome/components/waveshare_epaper/waveshare_213v3.cpp b/esphome/components/waveshare_epaper/waveshare_213v3.cpp index 316cd80ccd..068cb91d31 100644 --- a/esphome/components/waveshare_epaper/waveshare_213v3.cpp +++ b/esphome/components/waveshare_epaper/waveshare_213v3.cpp @@ -181,7 +181,7 @@ void WaveshareEPaper2P13InV3::dump_config() { LOG_PIN(" Reset Pin: ", this->reset_pin_) LOG_PIN(" DC Pin: ", this->dc_pin_) LOG_PIN(" Busy Pin: ", this->busy_pin_) - LOG_UPDATE_INTERVAL(this) + LOG_UPDATE_INTERVAL(this); } void WaveshareEPaper2P13InV3::set_full_update_every(uint32_t full_update_every) { diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index e30dab2508..44a86a4aa5 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -342,6 +342,18 @@ void Component::status_momentary_error(const std::string &name, uint32_t length) this->set_timeout(name, length, [this]() { this->status_clear_error(); }); } void Component::dump_config() {} + +// Function implementation of LOG_UPDATE_INTERVAL macro to reduce code size +void log_update_interval(const char *tag, PollingComponent *component) { + uint32_t update_interval = component->get_update_interval(); + if (update_interval == SCHEDULER_DONT_RUN) { + ESP_LOGCONFIG(tag, " Update Interval: never"); + } else if (update_interval < 100) { + ESP_LOGCONFIG(tag, " Update Interval: %.3fs", update_interval / 1000.0f); + } else { + ESP_LOGCONFIG(tag, " Update Interval: %.1fs", update_interval / 1000.0f); + } +} float Component::get_actual_setup_priority() const { // Check if there's an override in the global vector if (setup_priority_overrides) { diff --git a/esphome/core/component.h b/esphome/core/component.h index a363fceb85..9dfcbb92fa 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -47,14 +47,13 @@ extern const float LATE; static const uint32_t SCHEDULER_DONT_RUN = 4294967295UL; -#define LOG_UPDATE_INTERVAL(this) \ - if (this->get_update_interval() == SCHEDULER_DONT_RUN) { \ - ESP_LOGCONFIG(TAG, " Update Interval: never"); \ - } else if (this->get_update_interval() < 100) { \ - ESP_LOGCONFIG(TAG, " Update Interval: %.3fs", this->get_update_interval() / 1000.0f); \ - } else { \ - ESP_LOGCONFIG(TAG, " Update Interval: %.1fs", this->get_update_interval() / 1000.0f); \ - } +// Forward declaration +class PollingComponent; + +// Function declaration for LOG_UPDATE_INTERVAL +void log_update_interval(const char *tag, PollingComponent *component); + +#define LOG_UPDATE_INTERVAL(this) log_update_interval(TAG, this) extern const uint8_t COMPONENT_STATE_MASK; extern const uint8_t COMPONENT_STATE_CONSTRUCTION; From b25506b04508a118171db29daefe40a7b487f3b4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 7 Sep 2025 16:10:55 -0500 Subject: [PATCH 167/208] [core] Skip redundant process_to_add() call when no scheduler items added (#10630) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/core/scheduler.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index a907b89b02..d0230d46fc 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -326,6 +326,9 @@ void HOT Scheduler::call(uint32_t now) { const auto now_64 = this->millis_64_(now); // 'now' from parameter - fresh from Application::loop() this->process_to_add(); + // Track if any items were added to to_add_ during this call (intervals or from callbacks) + bool has_added_items = false; + #ifdef ESPHOME_DEBUG_SCHEDULER static uint64_t last_print = 0; @@ -470,10 +473,14 @@ void HOT Scheduler::call(uint32_t now) { // since we have the lock held this->to_add_.push_back(std::move(item)); } + + has_added_items |= !this->to_add_.empty(); } } - this->process_to_add(); + if (has_added_items) { + this->process_to_add(); + } } void HOT Scheduler::process_to_add() { LockGuard guard{this->lock_}; From 148fa698cc82d4aea7c928e309c29dc2537651bc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 7 Sep 2025 17:25:22 -0500 Subject: [PATCH 168/208] Fix DNS resolution inconsistency between logs and OTA operations (#10595) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/__main__.py | 36 ++-- esphome/espota2.py | 10 +- esphome/helpers.py | 141 ++++++------- esphome/resolver.py | 67 +++++++ tests/unit_tests/test_helpers.py | 317 ++++++++++++++++++++++++++++++ tests/unit_tests/test_resolver.py | 169 ++++++++++++++++ 6 files changed, 640 insertions(+), 100 deletions(-) create mode 100644 esphome/resolver.py create mode 100644 tests/unit_tests/test_resolver.py diff --git a/esphome/__main__.py b/esphome/__main__.py index aab3035a5e..70d5cacd72 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -396,7 +396,10 @@ def check_permissions(port: str): ) -def upload_program(config: ConfigType, args: ArgsProtocol, host: str) -> int | str: +def upload_program( + config: ConfigType, args: ArgsProtocol, devices: list[str] +) -> int | str: + host = devices[0] try: module = importlib.import_module("esphome.components." + CORE.target_platform) if getattr(module, "upload_program")(config, args, host): @@ -433,10 +436,10 @@ def upload_program(config: ConfigType, args: ArgsProtocol, host: str) -> int | s remote_port = int(ota_conf[CONF_PORT]) password = ota_conf.get(CONF_PASSWORD, "") + binary = args.file if getattr(args, "file", None) is not None else CORE.firmware_bin # Check if we should use MQTT for address resolution # This happens when no device was specified, or the current host is "MQTT"/"OTA" - devices: list[str] = args.device or [] if ( CONF_MQTT in config # pylint: disable=too-many-boolean-expressions and (not devices or host in ("MQTT", "OTA")) @@ -447,14 +450,13 @@ def upload_program(config: ConfigType, args: ArgsProtocol, host: str) -> int | s ): from esphome import mqtt - host = mqtt.get_esphome_device_ip( - config, args.username, args.password, args.client_id - ) + devices = [ + mqtt.get_esphome_device_ip( + config, args.username, args.password, args.client_id + ) + ] - if getattr(args, "file", None) is not None: - return espota2.run_ota(host, remote_port, password, args.file) - - return espota2.run_ota(host, remote_port, password, CORE.firmware_bin) + return espota2.run_ota(devices, remote_port, password, binary) def show_logs(config: ConfigType, args: ArgsProtocol, devices: list[str]) -> int | None: @@ -551,17 +553,11 @@ def command_upload(args: ArgsProtocol, config: ConfigType) -> int | None: purpose="uploading", ) - # Try each device until one succeeds - exit_code = 1 - for device in devices: - _LOGGER.info("Uploading to %s", device) - exit_code = upload_program(config, args, device) - if exit_code == 0: - _LOGGER.info("Successfully uploaded program.") - return 0 - if len(devices) > 1: - _LOGGER.warning("Failed to upload to %s", device) - + exit_code = upload_program(config, args, devices) + if exit_code == 0: + _LOGGER.info("Successfully uploaded program.") + else: + _LOGGER.warning("Failed to upload to %s", devices) return exit_code diff --git a/esphome/espota2.py b/esphome/espota2.py index 279bafee8e..d83f25a303 100644 --- a/esphome/espota2.py +++ b/esphome/espota2.py @@ -308,8 +308,12 @@ def perform_ota( time.sleep(1) -def run_ota_impl_(remote_host, remote_port, password, filename): +def run_ota_impl_( + remote_host: str | list[str], remote_port: int, password: str, filename: str +) -> int: + # Handle both single host and list of hosts try: + # Resolve all hosts at once for parallel DNS resolution res = resolve_ip_address(remote_host, remote_port) except EsphomeError as err: _LOGGER.error( @@ -350,7 +354,9 @@ def run_ota_impl_(remote_host, remote_port, password, filename): return 1 -def run_ota(remote_host, remote_port, password, filename): +def run_ota( + remote_host: str | list[str], remote_port: int, password: str, filename: str +) -> int: try: return run_ota_impl_(remote_host, remote_port, password, filename) except OTAError as err: diff --git a/esphome/helpers.py b/esphome/helpers.py index 377a4e1717..6beaa24a96 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import codecs from contextlib import suppress import ipaddress @@ -11,6 +13,18 @@ from urllib.parse import urlparse from esphome.const import __version__ as ESPHOME_VERSION +# Type aliases for socket address information +AddrInfo = tuple[ + int, # family (AF_INET, AF_INET6, etc.) + int, # type (SOCK_STREAM, SOCK_DGRAM, etc.) + int, # proto (IPPROTO_TCP, etc.) + str, # canonname + tuple[str, int] | tuple[str, int, int, int], # sockaddr (IPv4 or IPv6) +] +IPv4SockAddr = tuple[str, int] # (host, port) +IPv6SockAddr = tuple[str, int, int, int] # (host, port, flowinfo, scope_id) +SockAddr = IPv4SockAddr | IPv6SockAddr + _LOGGER = logging.getLogger(__name__) IS_MACOS = platform.system() == "Darwin" @@ -147,32 +161,7 @@ def is_ip_address(host): return False -def _resolve_with_zeroconf(host): - from esphome.core import EsphomeError - from esphome.zeroconf import EsphomeZeroconf - - try: - zc = EsphomeZeroconf() - except Exception as err: - raise EsphomeError( - "Cannot start mDNS sockets, is this a docker container without " - "host network mode?" - ) from err - try: - info = zc.resolve_host(f"{host}.") - except Exception as err: - raise EsphomeError(f"Error resolving mDNS hostname: {err}") from err - finally: - zc.close() - if info is None: - raise EsphomeError( - "Error resolving address with mDNS: Did not respond. " - "Maybe the device is offline." - ) - return info - - -def addr_preference_(res): +def addr_preference_(res: AddrInfo) -> int: # Trivial alternative to RFC6724 sorting. Put sane IPv6 first, then # Legacy IP, then IPv6 link-local addresses without an actual link. sa = res[4] @@ -184,66 +173,70 @@ def addr_preference_(res): return 1 -def resolve_ip_address(host, port): +def resolve_ip_address(host: str | list[str], port: int) -> list[AddrInfo]: import socket - from esphome.core import EsphomeError - # There are five cases here. The host argument could be one of: # • a *list* of IP addresses discovered by MQTT, # • a single IP address specified by the user, # • a .local hostname to be resolved by mDNS, # • a normal hostname to be resolved in DNS, or # • A URL from which we should extract the hostname. - # - # In each of the first three cases, we end up with IP addresses in - # string form which need to be converted to a 5-tuple to be used - # for the socket connection attempt. The easiest way to construct - # those is to pass the IP address string to getaddrinfo(). Which, - # coincidentally, is how we do hostname lookups in the other cases - # too. So first build a list which contains either IP addresses or - # a single hostname, then call getaddrinfo() on each element of - # that list. - errs = [] + hosts: list[str] if isinstance(host, list): - addr_list = host - elif is_ip_address(host): - addr_list = [host] + hosts = host else: - url = urlparse(host) - if url.scheme != "": - host = url.hostname + if not is_ip_address(host): + url = urlparse(host) + if url.scheme != "": + host = url.hostname + hosts = [host] - addr_list = [] - if host.endswith(".local"): + res: list[AddrInfo] = [] + if all(is_ip_address(h) for h in hosts): + # Fast path: all are IP addresses, use socket.getaddrinfo with AI_NUMERICHOST + for addr in hosts: try: - _LOGGER.info("Resolving IP address of %s in mDNS", host) - addr_list = _resolve_with_zeroconf(host) - except EsphomeError as err: - errs.append(str(err)) + res += socket.getaddrinfo( + addr, port, proto=socket.IPPROTO_TCP, flags=socket.AI_NUMERICHOST + ) + except OSError: + _LOGGER.debug("Failed to parse IP address '%s'", addr) + # Sort by preference + res.sort(key=addr_preference_) + return res - # If not mDNS, or if mDNS failed, use normal DNS - if not addr_list: - addr_list = [host] + from esphome.resolver import AsyncResolver - # Now we have a list containing either IP addresses or a hostname - res = [] - for addr in addr_list: - if not is_ip_address(addr): - _LOGGER.info("Resolving IP address of %s", host) - try: - r = socket.getaddrinfo(addr, port, proto=socket.IPPROTO_TCP) - except OSError as err: - errs.append(str(err)) - raise EsphomeError( - f"Error resolving IP address: {', '.join(errs)}" - ) from err + resolver = AsyncResolver(hosts, port) + addr_infos = resolver.resolve() + # Convert aioesphomeapi AddrInfo to our format + for addr_info in addr_infos: + sockaddr = addr_info.sockaddr + if addr_info.family == socket.AF_INET6: + # IPv6 + sockaddr_tuple = ( + sockaddr.address, + sockaddr.port, + sockaddr.flowinfo, + sockaddr.scope_id, + ) + else: + # IPv4 + sockaddr_tuple = (sockaddr.address, sockaddr.port) - res = res + r + res.append( + ( + addr_info.family, + addr_info.type, + addr_info.proto, + "", # canonname + sockaddr_tuple, + ) + ) - # Zeroconf tends to give us link-local IPv6 addresses without specifying - # the link. Put those last in the list to be attempted. + # Sort by preference res.sort(key=addr_preference_) return res @@ -262,15 +255,7 @@ def sort_ip_addresses(address_list: list[str]) -> list[str]: # First "resolve" all the IP addresses to getaddrinfo() tuples of the form # (family, type, proto, canonname, sockaddr) - res: list[ - tuple[ - int, - int, - int, - str | None, - tuple[str, int] | tuple[str, int, int, int], - ] - ] = [] + res: list[AddrInfo] = [] for addr in address_list: # This should always work as these are supposed to be IP addresses try: diff --git a/esphome/resolver.py b/esphome/resolver.py new file mode 100644 index 0000000000..99482aa20e --- /dev/null +++ b/esphome/resolver.py @@ -0,0 +1,67 @@ +"""DNS resolver for ESPHome using aioesphomeapi.""" + +from __future__ import annotations + +import asyncio +import threading + +from aioesphomeapi.core import ResolveAPIError, ResolveTimeoutAPIError +import aioesphomeapi.host_resolver as hr + +from esphome.core import EsphomeError + +RESOLVE_TIMEOUT = 10.0 # seconds + + +class AsyncResolver(threading.Thread): + """Resolver using aioesphomeapi that runs in a thread for faster results. + + This resolver uses aioesphomeapi's async_resolve_host to handle DNS resolution, + including proper .local domain fallback. Running in a thread allows us to get + the result immediately without waiting for asyncio.run() to complete its + cleanup cycle, which can take significant time. + """ + + def __init__(self, hosts: list[str], port: int) -> None: + """Initialize the resolver.""" + super().__init__(daemon=True) + self.hosts = hosts + self.port = port + self.result: list[hr.AddrInfo] | None = None + self.exception: Exception | None = None + self.event = threading.Event() + + async def _resolve(self) -> None: + """Resolve hostnames to IP addresses.""" + try: + self.result = await hr.async_resolve_host( + self.hosts, self.port, timeout=RESOLVE_TIMEOUT + ) + except Exception as e: # pylint: disable=broad-except + # We need to catch all exceptions to ensure the event is set + # Otherwise the thread could hang forever + self.exception = e + finally: + self.event.set() + + def run(self) -> None: + """Run the DNS resolution.""" + asyncio.run(self._resolve()) + + def resolve(self) -> list[hr.AddrInfo]: + """Start the thread and wait for the result.""" + self.start() + + if not self.event.wait( + timeout=RESOLVE_TIMEOUT + 1.0 + ): # Give it 1 second more than the resolver timeout + raise EsphomeError("Timeout resolving IP address") + + if exc := self.exception: + if isinstance(exc, ResolveTimeoutAPIError): + raise EsphomeError(f"Timeout resolving IP address: {exc}") from exc + if isinstance(exc, ResolveAPIError): + raise EsphomeError(f"Error resolving IP address: {exc}") from exc + raise exc + + return self.result diff --git a/tests/unit_tests/test_helpers.py b/tests/unit_tests/test_helpers.py index b353d1aa99..9f51206ff9 100644 --- a/tests/unit_tests/test_helpers.py +++ b/tests/unit_tests/test_helpers.py @@ -1,8 +1,14 @@ +import logging +import socket +from unittest.mock import patch + +from aioesphomeapi.host_resolver import AddrInfo, IPv4Sockaddr, IPv6Sockaddr from hypothesis import given from hypothesis.strategies import ip_addresses import pytest from esphome import helpers +from esphome.core import EsphomeError @pytest.mark.parametrize( @@ -277,3 +283,314 @@ def test_sort_ip_addresses(text: list[str], expected: list[str]) -> None: actual = helpers.sort_ip_addresses(text) assert actual == expected + + +# DNS resolution tests +def test_is_ip_address_ipv4() -> None: + """Test is_ip_address with IPv4 addresses.""" + assert helpers.is_ip_address("192.168.1.1") is True + assert helpers.is_ip_address("127.0.0.1") is True + assert helpers.is_ip_address("255.255.255.255") is True + assert helpers.is_ip_address("0.0.0.0") is True + + +def test_is_ip_address_ipv6() -> None: + """Test is_ip_address with IPv6 addresses.""" + assert helpers.is_ip_address("::1") is True + assert helpers.is_ip_address("2001:db8::1") is True + assert helpers.is_ip_address("fe80::1") is True + assert helpers.is_ip_address("::") is True + + +def test_is_ip_address_invalid() -> None: + """Test is_ip_address with non-IP strings.""" + assert helpers.is_ip_address("hostname") is False + assert helpers.is_ip_address("hostname.local") is False + assert helpers.is_ip_address("256.256.256.256") is False + assert helpers.is_ip_address("192.168.1") is False + assert helpers.is_ip_address("") is False + + +def test_resolve_ip_address_single_ipv4() -> None: + """Test resolving a single IPv4 address (fast path).""" + result = helpers.resolve_ip_address("192.168.1.100", 6053) + + assert len(result) == 1 + assert result[0][0] == socket.AF_INET # family + assert result[0][1] in ( + 0, + socket.SOCK_STREAM, + ) # type (0 on Windows with AI_NUMERICHOST) + assert result[0][2] in ( + 0, + socket.IPPROTO_TCP, + ) # proto (0 on Windows with AI_NUMERICHOST) + assert result[0][3] == "" # canonname + assert result[0][4] == ("192.168.1.100", 6053) # sockaddr + + +def test_resolve_ip_address_single_ipv6() -> None: + """Test resolving a single IPv6 address (fast path).""" + result = helpers.resolve_ip_address("::1", 6053) + + assert len(result) == 1 + assert result[0][0] == socket.AF_INET6 # family + assert result[0][1] in ( + 0, + socket.SOCK_STREAM, + ) # type (0 on Windows with AI_NUMERICHOST) + assert result[0][2] in ( + 0, + socket.IPPROTO_TCP, + ) # proto (0 on Windows with AI_NUMERICHOST) + assert result[0][3] == "" # canonname + # IPv6 sockaddr has 4 elements + assert len(result[0][4]) == 4 + assert result[0][4][0] == "::1" # address + assert result[0][4][1] == 6053 # port + + +def test_resolve_ip_address_list_of_ips() -> None: + """Test resolving a list of IP addresses (fast path).""" + ips = ["192.168.1.100", "10.0.0.1", "::1"] + result = helpers.resolve_ip_address(ips, 6053) + + # Should return results sorted by preference (IPv6 first, then IPv4) + assert len(result) >= 2 # At least IPv4 addresses should work + + # Check that results are properly formatted + for addr_info in result: + assert addr_info[0] in (socket.AF_INET, socket.AF_INET6) + assert addr_info[1] in ( + 0, + socket.SOCK_STREAM, + ) # 0 on Windows with AI_NUMERICHOST + assert addr_info[2] in ( + 0, + socket.IPPROTO_TCP, + ) # 0 on Windows with AI_NUMERICHOST + assert addr_info[3] == "" + + +def test_resolve_ip_address_with_getaddrinfo_failure(caplog) -> None: + """Test that getaddrinfo OSError is handled gracefully in fast path.""" + with ( + caplog.at_level(logging.DEBUG), + patch("socket.getaddrinfo") as mock_getaddrinfo, + ): + # First IP succeeds + mock_getaddrinfo.side_effect = [ + [ + ( + socket.AF_INET, + socket.SOCK_STREAM, + socket.IPPROTO_TCP, + "", + ("192.168.1.100", 6053), + ) + ], + OSError("Failed to resolve"), # Second IP fails + ] + + # Should continue despite one failure + result = helpers.resolve_ip_address(["192.168.1.100", "192.168.1.101"], 6053) + + # Should have result from first IP only + assert len(result) == 1 + assert result[0][4][0] == "192.168.1.100" + + # Verify both IPs were attempted + assert mock_getaddrinfo.call_count == 2 + mock_getaddrinfo.assert_any_call( + "192.168.1.100", 6053, proto=socket.IPPROTO_TCP, flags=socket.AI_NUMERICHOST + ) + mock_getaddrinfo.assert_any_call( + "192.168.1.101", 6053, proto=socket.IPPROTO_TCP, flags=socket.AI_NUMERICHOST + ) + + # Verify the debug log was called for the failed IP + assert "Failed to parse IP address '192.168.1.101'" in caplog.text + + +def test_resolve_ip_address_hostname() -> None: + """Test resolving a hostname (async resolver path).""" + mock_addr_info = AddrInfo( + family=socket.AF_INET, + type=socket.SOCK_STREAM, + proto=socket.IPPROTO_TCP, + sockaddr=IPv4Sockaddr(address="192.168.1.100", port=6053), + ) + + with patch("esphome.resolver.AsyncResolver") as MockResolver: + mock_resolver = MockResolver.return_value + mock_resolver.resolve.return_value = [mock_addr_info] + + result = helpers.resolve_ip_address("test.local", 6053) + + assert len(result) == 1 + assert result[0][0] == socket.AF_INET + assert result[0][4] == ("192.168.1.100", 6053) + MockResolver.assert_called_once_with(["test.local"], 6053) + mock_resolver.resolve.assert_called_once() + + +def test_resolve_ip_address_mixed_list() -> None: + """Test resolving a mix of IPs and hostnames.""" + mock_addr_info = AddrInfo( + family=socket.AF_INET, + type=socket.SOCK_STREAM, + proto=socket.IPPROTO_TCP, + sockaddr=IPv4Sockaddr(address="192.168.1.200", port=6053), + ) + + with patch("esphome.resolver.AsyncResolver") as MockResolver: + mock_resolver = MockResolver.return_value + mock_resolver.resolve.return_value = [mock_addr_info] + + # Mix of IP and hostname - should use async resolver + result = helpers.resolve_ip_address(["192.168.1.100", "test.local"], 6053) + + assert len(result) == 1 + assert result[0][4][0] == "192.168.1.200" + MockResolver.assert_called_once_with(["192.168.1.100", "test.local"], 6053) + mock_resolver.resolve.assert_called_once() + + +def test_resolve_ip_address_url() -> None: + """Test extracting hostname from URL.""" + mock_addr_info = AddrInfo( + family=socket.AF_INET, + type=socket.SOCK_STREAM, + proto=socket.IPPROTO_TCP, + sockaddr=IPv4Sockaddr(address="192.168.1.100", port=6053), + ) + + with patch("esphome.resolver.AsyncResolver") as MockResolver: + mock_resolver = MockResolver.return_value + mock_resolver.resolve.return_value = [mock_addr_info] + + result = helpers.resolve_ip_address("http://test.local", 6053) + + assert len(result) == 1 + MockResolver.assert_called_once_with(["test.local"], 6053) + mock_resolver.resolve.assert_called_once() + + +def test_resolve_ip_address_ipv6_conversion() -> None: + """Test proper IPv6 address info conversion.""" + mock_addr_info = AddrInfo( + family=socket.AF_INET6, + type=socket.SOCK_STREAM, + proto=socket.IPPROTO_TCP, + sockaddr=IPv6Sockaddr(address="2001:db8::1", port=6053, flowinfo=1, scope_id=2), + ) + + with patch("esphome.resolver.AsyncResolver") as MockResolver: + mock_resolver = MockResolver.return_value + mock_resolver.resolve.return_value = [mock_addr_info] + + result = helpers.resolve_ip_address("test.local", 6053) + + assert len(result) == 1 + assert result[0][0] == socket.AF_INET6 + assert result[0][4] == ("2001:db8::1", 6053, 1, 2) + + +def test_resolve_ip_address_error_handling() -> None: + """Test error handling from AsyncResolver.""" + with patch("esphome.resolver.AsyncResolver") as MockResolver: + mock_resolver = MockResolver.return_value + mock_resolver.resolve.side_effect = EsphomeError("Resolution failed") + + with pytest.raises(EsphomeError, match="Resolution failed"): + helpers.resolve_ip_address("test.local", 6053) + + +def test_addr_preference_ipv4() -> None: + """Test address preference for IPv4.""" + addr_info = ( + socket.AF_INET, + socket.SOCK_STREAM, + socket.IPPROTO_TCP, + "", + ("192.168.1.1", 6053), + ) + assert helpers.addr_preference_(addr_info) == 2 + + +def test_addr_preference_ipv6() -> None: + """Test address preference for regular IPv6.""" + addr_info = ( + socket.AF_INET6, + socket.SOCK_STREAM, + socket.IPPROTO_TCP, + "", + ("2001:db8::1", 6053, 0, 0), + ) + assert helpers.addr_preference_(addr_info) == 1 + + +def test_addr_preference_ipv6_link_local_no_scope() -> None: + """Test address preference for link-local IPv6 without scope.""" + addr_info = ( + socket.AF_INET6, + socket.SOCK_STREAM, + socket.IPPROTO_TCP, + "", + ("fe80::1", 6053, 0, 0), # link-local with scope_id=0 + ) + assert helpers.addr_preference_(addr_info) == 3 + + +def test_addr_preference_ipv6_link_local_with_scope() -> None: + """Test address preference for link-local IPv6 with scope.""" + addr_info = ( + socket.AF_INET6, + socket.SOCK_STREAM, + socket.IPPROTO_TCP, + "", + ("fe80::1", 6053, 0, 2), # link-local with scope_id=2 + ) + assert helpers.addr_preference_(addr_info) == 1 # Has scope, so it's usable + + +def test_resolve_ip_address_sorting() -> None: + """Test that results are sorted by preference.""" + # Create multiple address infos with different preferences + mock_addr_infos = [ + AddrInfo( + family=socket.AF_INET6, + type=socket.SOCK_STREAM, + proto=socket.IPPROTO_TCP, + sockaddr=IPv6Sockaddr( + address="fe80::1", port=6053, flowinfo=0, scope_id=0 + ), # Preference 3 (link-local no scope) + ), + AddrInfo( + family=socket.AF_INET, + type=socket.SOCK_STREAM, + proto=socket.IPPROTO_TCP, + sockaddr=IPv4Sockaddr( + address="192.168.1.100", port=6053 + ), # Preference 2 (IPv4) + ), + AddrInfo( + family=socket.AF_INET6, + type=socket.SOCK_STREAM, + proto=socket.IPPROTO_TCP, + sockaddr=IPv6Sockaddr( + address="2001:db8::1", port=6053, flowinfo=0, scope_id=0 + ), # Preference 1 (IPv6) + ), + ] + + with patch("esphome.resolver.AsyncResolver") as MockResolver: + mock_resolver = MockResolver.return_value + mock_resolver.resolve.return_value = mock_addr_infos + + result = helpers.resolve_ip_address("test.local", 6053) + + # Should be sorted: IPv6 first, then IPv4, then link-local without scope + assert result[0][4][0] == "2001:db8::1" # IPv6 (preference 1) + assert result[1][4][0] == "192.168.1.100" # IPv4 (preference 2) + assert result[2][4][0] == "fe80::1" # Link-local no scope (preference 3) diff --git a/tests/unit_tests/test_resolver.py b/tests/unit_tests/test_resolver.py new file mode 100644 index 0000000000..b4cca05d9f --- /dev/null +++ b/tests/unit_tests/test_resolver.py @@ -0,0 +1,169 @@ +"""Tests for the DNS resolver module.""" + +from __future__ import annotations + +import re +import socket +from unittest.mock import patch + +from aioesphomeapi.core import ResolveAPIError, ResolveTimeoutAPIError +from aioesphomeapi.host_resolver import AddrInfo, IPv4Sockaddr, IPv6Sockaddr +import pytest + +from esphome.core import EsphomeError +from esphome.resolver import RESOLVE_TIMEOUT, AsyncResolver + + +@pytest.fixture +def mock_addr_info_ipv4() -> AddrInfo: + """Create a mock IPv4 AddrInfo.""" + return AddrInfo( + family=socket.AF_INET, + type=socket.SOCK_STREAM, + proto=socket.IPPROTO_TCP, + sockaddr=IPv4Sockaddr(address="192.168.1.100", port=6053), + ) + + +@pytest.fixture +def mock_addr_info_ipv6() -> AddrInfo: + """Create a mock IPv6 AddrInfo.""" + return AddrInfo( + family=socket.AF_INET6, + type=socket.SOCK_STREAM, + proto=socket.IPPROTO_TCP, + sockaddr=IPv6Sockaddr(address="2001:db8::1", port=6053, flowinfo=0, scope_id=0), + ) + + +def test_async_resolver_successful_resolution(mock_addr_info_ipv4: AddrInfo) -> None: + """Test successful DNS resolution.""" + with patch( + "esphome.resolver.hr.async_resolve_host", + return_value=[mock_addr_info_ipv4], + ) as mock_resolve: + resolver = AsyncResolver(["test.local"], 6053) + result = resolver.resolve() + + assert result == [mock_addr_info_ipv4] + mock_resolve.assert_called_once_with( + ["test.local"], 6053, timeout=RESOLVE_TIMEOUT + ) + + +def test_async_resolver_multiple_hosts( + mock_addr_info_ipv4: AddrInfo, mock_addr_info_ipv6: AddrInfo +) -> None: + """Test resolving multiple hosts.""" + mock_results = [mock_addr_info_ipv4, mock_addr_info_ipv6] + + with patch( + "esphome.resolver.hr.async_resolve_host", + return_value=mock_results, + ) as mock_resolve: + resolver = AsyncResolver(["test1.local", "test2.local"], 6053) + result = resolver.resolve() + + assert result == mock_results + mock_resolve.assert_called_once_with( + ["test1.local", "test2.local"], 6053, timeout=RESOLVE_TIMEOUT + ) + + +def test_async_resolver_resolve_api_error() -> None: + """Test handling of ResolveAPIError.""" + error_msg = "Failed to resolve" + with patch( + "esphome.resolver.hr.async_resolve_host", + side_effect=ResolveAPIError(error_msg), + ): + resolver = AsyncResolver(["test.local"], 6053) + with pytest.raises( + EsphomeError, match=re.escape(f"Error resolving IP address: {error_msg}") + ): + resolver.resolve() + + +def test_async_resolver_timeout_error() -> None: + """Test handling of ResolveTimeoutAPIError.""" + error_msg = "Resolution timed out" + + with patch( + "esphome.resolver.hr.async_resolve_host", + side_effect=ResolveTimeoutAPIError(error_msg), + ): + resolver = AsyncResolver(["test.local"], 6053) + # Match either "Timeout" or "Error" since ResolveTimeoutAPIError is a subclass of ResolveAPIError + # and depending on import order/test execution context, it might be caught as either + with pytest.raises( + EsphomeError, + match=f"(Timeout|Error) resolving IP address: {re.escape(error_msg)}", + ): + resolver.resolve() + + +def test_async_resolver_generic_exception() -> None: + """Test handling of generic exceptions.""" + error = RuntimeError("Unexpected error") + with patch( + "esphome.resolver.hr.async_resolve_host", + side_effect=error, + ): + resolver = AsyncResolver(["test.local"], 6053) + with pytest.raises(RuntimeError, match="Unexpected error"): + resolver.resolve() + + +def test_async_resolver_thread_timeout() -> None: + """Test timeout when thread doesn't complete in time.""" + # Mock the start method to prevent actual thread execution + with ( + patch.object(AsyncResolver, "start"), + patch("esphome.resolver.hr.async_resolve_host"), + ): + resolver = AsyncResolver(["test.local"], 6053) + # Override event.wait to simulate timeout (return False = timeout occurred) + with ( + patch.object(resolver.event, "wait", return_value=False), + pytest.raises( + EsphomeError, match=re.escape("Timeout resolving IP address") + ), + ): + resolver.resolve() + + # Verify thread start was called + resolver.start.assert_called_once() + + +def test_async_resolver_ip_addresses(mock_addr_info_ipv4: AddrInfo) -> None: + """Test resolving IP addresses.""" + with patch( + "esphome.resolver.hr.async_resolve_host", + return_value=[mock_addr_info_ipv4], + ) as mock_resolve: + resolver = AsyncResolver(["192.168.1.100"], 6053) + result = resolver.resolve() + + assert result == [mock_addr_info_ipv4] + mock_resolve.assert_called_once_with( + ["192.168.1.100"], 6053, timeout=RESOLVE_TIMEOUT + ) + + +def test_async_resolver_mixed_addresses( + mock_addr_info_ipv4: AddrInfo, mock_addr_info_ipv6: AddrInfo +) -> None: + """Test resolving mix of hostnames and IP addresses.""" + mock_results = [mock_addr_info_ipv4, mock_addr_info_ipv6] + + with patch( + "esphome.resolver.hr.async_resolve_host", + return_value=mock_results, + ) as mock_resolve: + resolver = AsyncResolver(["test.local", "192.168.1.100", "::1"], 6053) + result = resolver.resolve() + + assert result == mock_results + mock_resolve.assert_called_once_with( + ["test.local", "192.168.1.100", "::1"], 6053, timeout=RESOLVE_TIMEOUT + ) From 0065fe15163b37c67cc70d637bef41596bd5e172 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 7 Sep 2025 17:26:06 -0500 Subject: [PATCH 169/208] Bump zeroconf from 0.147.0 to 0.147.2 (#10642) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index bac5622708..1a836bd64f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,7 @@ esptool==5.0.2 click==8.1.7 esphome-dashboard==20250904.0 aioesphomeapi==40.0.1 -zeroconf==0.147.0 +zeroconf==0.147.2 puremagic==1.30 ruamel.yaml==0.18.15 # dashboard_import esphome-glyphsets==0.2.0 From f24a182ba2a4f8024914a50d4f6cb09a6ab5ccc3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 7 Sep 2025 17:26:31 -0500 Subject: [PATCH 170/208] Bump pytest-cov from 6.2.1 to 6.3.0 (#10640) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index eba14fc0b1..1760af75ba 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -6,7 +6,7 @@ pre-commit # Unit tests pytest==8.4.2 -pytest-cov==6.2.1 +pytest-cov==6.3.0 pytest-mock==3.15.0 pytest-asyncio==1.1.0 pytest-xdist==3.8.0 From 28d16728d3f9b396b819105bffc943425ca23f8f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 7 Sep 2025 17:27:58 -0500 Subject: [PATCH 171/208] [core] Add memory pool to scheduler to reduce heap fragmentation (#10536) --- esphome/core/scheduler.cpp | 130 ++++++-- esphome/core/scheduler.h | 65 ++-- .../integration/fixtures/scheduler_pool.yaml | 282 ++++++++++++++++++ tests/integration/test_scheduler_pool.py | 209 +++++++++++++ 4 files changed, 633 insertions(+), 53 deletions(-) create mode 100644 tests/integration/fixtures/scheduler_pool.yaml create mode 100644 tests/integration/test_scheduler_pool.py diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index d0230d46fc..9a9e61d579 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -14,7 +14,20 @@ namespace esphome { static const char *const TAG = "scheduler"; -static const uint32_t MAX_LOGICALLY_DELETED_ITEMS = 10; +// Memory pool configuration constants +// Pool size of 5 matches typical usage patterns (2-4 active timers) +// - Minimal memory overhead (~250 bytes on ESP32) +// - Sufficient for most configs with a couple sensors/components +// - Still prevents heap fragmentation and allocation stalls +// - Complex setups with many timers will just allocate beyond the pool +// See https://github.com/esphome/backlog/issues/52 +static constexpr size_t MAX_POOL_SIZE = 5; + +// Maximum number of logically deleted (cancelled) items before forcing cleanup. +// Set to 5 to match the pool size - when we have as many cancelled items as our +// pool can hold, it's time to clean up and recycle them. +static constexpr uint32_t MAX_LOGICALLY_DELETED_ITEMS = 5; + // Half the 32-bit range - used to detect rollovers vs normal time progression static constexpr uint32_t HALF_MAX_UINT32 = std::numeric_limits::max() / 2; // max delay to start an interval sequence @@ -79,8 +92,28 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type return; } + // Get fresh timestamp BEFORE taking lock - millis_64_ may need to acquire lock itself + const uint64_t now = this->millis_64_(millis()); + + // Take lock early to protect scheduler_item_pool_ access + LockGuard guard{this->lock_}; + // Create and populate the scheduler item - auto item = make_unique(); + std::unique_ptr item; + if (!this->scheduler_item_pool_.empty()) { + // Reuse from pool + item = std::move(this->scheduler_item_pool_.back()); + this->scheduler_item_pool_.pop_back(); +#ifdef ESPHOME_DEBUG_SCHEDULER + ESP_LOGD(TAG, "Reused item from pool (pool size now: %zu)", this->scheduler_item_pool_.size()); +#endif + } else { + // Allocate new if pool is empty + item = make_unique(); +#ifdef ESPHOME_DEBUG_SCHEDULER + ESP_LOGD(TAG, "Allocated new item (pool empty)"); +#endif + } item->component = component; item->set_name(name_cstr, !is_static_string); item->type = type; @@ -99,7 +132,6 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type // Single-core platforms don't need thread-safe defer handling if (delay == 0 && type == SchedulerItem::TIMEOUT) { // Put in defer queue for guaranteed FIFO execution - LockGuard guard{this->lock_}; if (!skip_cancel) { this->cancel_item_locked_(component, name_cstr, type); } @@ -108,9 +140,6 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type } #endif /* not ESPHOME_THREAD_SINGLE */ - // Get fresh timestamp for new timer/interval - ensures accurate scheduling - const auto now = this->millis_64_(millis()); // Fresh millis() call - // Type-specific setup if (type == SchedulerItem::INTERVAL) { item->interval = delay; @@ -142,8 +171,6 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type } #endif /* ESPHOME_DEBUG_SCHEDULER */ - LockGuard guard{this->lock_}; - // For retries, check if there's a cancelled timeout first if (is_retry && name_cstr != nullptr && type == SchedulerItem::TIMEOUT && (has_cancelled_timeout_in_container_(this->items_, component, name_cstr, /* match_retry= */ true) || @@ -319,6 +346,8 @@ void HOT Scheduler::call(uint32_t now) { if (!this->should_skip_item_(item.get())) { this->execute_item_(item.get(), now); } + // Recycle the defer item after execution + this->recycle_item_(std::move(item)); } #endif /* not ESPHOME_THREAD_SINGLE */ @@ -338,11 +367,11 @@ void HOT Scheduler::call(uint32_t now) { #ifdef ESPHOME_THREAD_MULTI_ATOMICS const auto last_dbg = this->last_millis_.load(std::memory_order_relaxed); const auto major_dbg = this->millis_major_.load(std::memory_order_relaxed); - ESP_LOGD(TAG, "Items: count=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(), now_64, - major_dbg, last_dbg); + ESP_LOGD(TAG, "Items: count=%zu, pool=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(), + this->scheduler_item_pool_.size(), now_64, major_dbg, last_dbg); #else /* not ESPHOME_THREAD_MULTI_ATOMICS */ - ESP_LOGD(TAG, "Items: count=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(), now_64, - this->millis_major_, this->last_millis_); + ESP_LOGD(TAG, "Items: count=%zu, pool=%zu, now=%" PRIu64 " (%" PRIu16 ", %" PRIu32 ")", this->items_.size(), + this->scheduler_item_pool_.size(), now_64, this->millis_major_, this->last_millis_); #endif /* else ESPHOME_THREAD_MULTI_ATOMICS */ // Cleanup before debug output this->cleanup_(); @@ -355,9 +384,10 @@ void HOT Scheduler::call(uint32_t now) { } const char *name = item->get_name(); - ESP_LOGD(TAG, " %s '%s/%s' interval=%" PRIu32 " next_execution in %" PRIu64 "ms at %" PRIu64, + bool is_cancelled = is_item_removed_(item.get()); + ESP_LOGD(TAG, " %s '%s/%s' interval=%" PRIu32 " next_execution in %" PRIu64 "ms at %" PRIu64 "%s", item->get_type_str(), item->get_source(), name ? name : "(null)", item->interval, - item->next_execution_ - now_64, item->next_execution_); + item->next_execution_ - now_64, item->next_execution_, is_cancelled ? " [CANCELLED]" : ""); old_items.push_back(std::move(item)); } @@ -372,8 +402,13 @@ void HOT Scheduler::call(uint32_t now) { } #endif /* ESPHOME_DEBUG_SCHEDULER */ - // If we have too many items to remove - if (this->to_remove_ > MAX_LOGICALLY_DELETED_ITEMS) { + // Cleanup removed items before processing + // First try to clean items from the top of the heap (fast path) + this->cleanup_(); + + // If we still have too many cancelled items, do a full cleanup + // This only happens if cancelled items are stuck in the middle/bottom of the heap + if (this->to_remove_ >= MAX_LOGICALLY_DELETED_ITEMS) { // We hold the lock for the entire cleanup operation because: // 1. We're rebuilding the entire items_ list, so we need exclusive access throughout // 2. Other threads must see either the old state or the new state, not intermediate states @@ -383,10 +418,13 @@ void HOT Scheduler::call(uint32_t now) { std::vector> valid_items; - // Move all non-removed items to valid_items + // Move all non-removed items to valid_items, recycle removed ones for (auto &item : this->items_) { - if (!item->remove) { + if (!is_item_removed_(item.get())) { valid_items.push_back(std::move(item)); + } else { + // Recycle removed items + this->recycle_item_(std::move(item)); } } @@ -396,9 +434,6 @@ void HOT Scheduler::call(uint32_t now) { std::make_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp); this->to_remove_ = 0; } - - // Cleanup removed items before processing - this->cleanup_(); while (!this->items_.empty()) { // use scoping to indicate visibility of `item` variable { @@ -472,6 +507,9 @@ void HOT Scheduler::call(uint32_t now) { // Add new item directly to to_add_ // since we have the lock held this->to_add_.push_back(std::move(item)); + } else { + // Timeout completed - recycle it + this->recycle_item_(std::move(item)); } has_added_items |= !this->to_add_.empty(); @@ -485,7 +523,9 @@ void HOT Scheduler::call(uint32_t now) { void HOT Scheduler::process_to_add() { LockGuard guard{this->lock_}; for (auto &it : this->to_add_) { - if (it->remove) { + if (is_item_removed_(it.get())) { + // Recycle cancelled items + this->recycle_item_(std::move(it)); continue; } @@ -525,6 +565,10 @@ size_t HOT Scheduler::cleanup_() { } void HOT Scheduler::pop_raw_() { std::pop_heap(this->items_.begin(), this->items_.end(), SchedulerItem::cmp); + + // Instead of destroying, recycle the item + this->recycle_item_(std::move(this->items_.back())); + this->items_.pop_back(); } @@ -559,7 +603,7 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c // Check all containers for matching items #ifndef ESPHOME_THREAD_SINGLE - // Only check defer queue for timeouts (intervals never go there) + // Mark items in defer queue as cancelled (they'll be skipped when processed) if (type == SchedulerItem::TIMEOUT) { for (auto &item : this->defer_queue_) { if (this->matches_item_(item, component, name_cstr, type, match_retry)) { @@ -571,11 +615,22 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c #endif /* not ESPHOME_THREAD_SINGLE */ // Cancel items in the main heap - for (auto &item : this->items_) { - if (this->matches_item_(item, component, name_cstr, type, match_retry)) { - this->mark_item_removed_(item.get()); + // Special case: if the last item in the heap matches, we can remove it immediately + // (removing the last element doesn't break heap structure) + if (!this->items_.empty()) { + auto &last_item = this->items_.back(); + if (this->matches_item_(last_item, component, name_cstr, type, match_retry)) { + this->recycle_item_(std::move(this->items_.back())); + this->items_.pop_back(); total_cancelled++; - this->to_remove_++; // Track removals for heap items + } + // For other items in heap, we can only mark for removal (can't remove from middle of heap) + for (auto &item : this->items_) { + if (this->matches_item_(item, component, name_cstr, type, match_retry)) { + this->mark_item_removed_(item.get()); + total_cancelled++; + this->to_remove_++; // Track removals for heap items + } } } @@ -754,4 +809,25 @@ bool HOT Scheduler::SchedulerItem::cmp(const std::unique_ptr &a, return a->next_execution_ > b->next_execution_; } +void Scheduler::recycle_item_(std::unique_ptr item) { + if (!item) + return; + + if (this->scheduler_item_pool_.size() < MAX_POOL_SIZE) { + // Clear callback to release captured resources + item->callback = nullptr; + // Clear dynamic name if any + item->clear_dynamic_name(); + this->scheduler_item_pool_.push_back(std::move(item)); +#ifdef ESPHOME_DEBUG_SCHEDULER + ESP_LOGD(TAG, "Recycled item to pool (pool size now: %zu)", this->scheduler_item_pool_.size()); +#endif + } else { +#ifdef ESPHOME_DEBUG_SCHEDULER + ESP_LOGD(TAG, "Pool full (size: %zu), deleting item", this->scheduler_item_pool_.size()); +#endif + } + // else: unique_ptr will delete the item when it goes out of scope +} + } // namespace esphome diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index f469a60d5c..85cfaab2e0 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -142,11 +142,7 @@ class Scheduler { } // Destructor to clean up dynamic names - ~SchedulerItem() { - if (name_is_dynamic) { - delete[] name_.dynamic_name; - } - } + ~SchedulerItem() { clear_dynamic_name(); } // Delete copy operations to prevent accidental copies SchedulerItem(const SchedulerItem &) = delete; @@ -159,13 +155,19 @@ class Scheduler { // Helper to get the name regardless of storage type const char *get_name() const { return name_is_dynamic ? name_.dynamic_name : name_.static_name; } + // Helper to clear dynamic name if allocated + void clear_dynamic_name() { + if (name_is_dynamic && name_.dynamic_name) { + delete[] name_.dynamic_name; + name_.dynamic_name = nullptr; + name_is_dynamic = false; + } + } + // Helper to set name with proper ownership void set_name(const char *name, bool make_copy = false) { // Clean up old dynamic name if any - if (name_is_dynamic && name_.dynamic_name) { - delete[] name_.dynamic_name; - name_is_dynamic = false; - } + clear_dynamic_name(); if (!name) { // nullptr case - no name provided @@ -214,6 +216,15 @@ class Scheduler { // Common implementation for cancel operations bool cancel_item_(Component *component, bool is_static_string, const void *name_ptr, SchedulerItem::Type type); + // Helper to check if two scheduler item names match + inline bool HOT names_match_(const char *name1, const char *name2) const { + // Check pointer equality first (common for static strings), then string contents + // The core ESPHome codebase uses static strings (const char*) for component names, + // making pointer comparison effective. The std::string overloads exist only for + // compatibility with external components but are rarely used in practice. + return (name1 != nullptr && name2 != nullptr) && ((name1 == name2) || (strcmp(name1, name2) == 0)); + } + // Helper function to check if item matches criteria for cancellation inline bool HOT matches_item_(const std::unique_ptr &item, Component *component, const char *name_cstr, SchedulerItem::Type type, bool match_retry, bool skip_removed = true) const { @@ -221,29 +232,20 @@ class Scheduler { (match_retry && !item->is_retry)) { return false; } - const char *item_name = item->get_name(); - if (item_name == nullptr) { - return false; - } - // Fast path: if pointers are equal - // This is effective because the core ESPHome codebase uses static strings (const char*) - // for component names. The std::string overloads exist only for compatibility with - // external components, but are rarely used in practice. - if (item_name == name_cstr) { - return true; - } - // Slow path: compare string contents - return strcmp(name_cstr, item_name) == 0; + return this->names_match_(item->get_name(), name_cstr); } // Helper to execute a scheduler item void execute_item_(SchedulerItem *item, uint32_t now); // Helper to check if item should be skipped - bool should_skip_item_(const SchedulerItem *item) const { - return item->remove || (item->component != nullptr && item->component->is_failed()); + bool should_skip_item_(SchedulerItem *item) const { + return is_item_removed_(item) || (item->component != nullptr && item->component->is_failed()); } + // Helper to recycle a SchedulerItem + void recycle_item_(std::unique_ptr item); + // Helper to check if item is marked for removal (platform-specific) // Returns true if item should be skipped, handles platform-specific synchronization // For ESPHOME_THREAD_MULTI_NO_ATOMICS platforms, the caller must hold the scheduler lock before calling this @@ -280,8 +282,9 @@ class Scheduler { bool has_cancelled_timeout_in_container_(const Container &container, Component *component, const char *name_cstr, bool match_retry) const { for (const auto &item : container) { - if (item->remove && this->matches_item_(item, component, name_cstr, SchedulerItem::TIMEOUT, match_retry, - /* skip_removed= */ false)) { + if (is_item_removed_(item.get()) && + this->matches_item_(item, component, name_cstr, SchedulerItem::TIMEOUT, match_retry, + /* skip_removed= */ false)) { return true; } } @@ -297,6 +300,16 @@ class Scheduler { #endif /* ESPHOME_THREAD_SINGLE */ uint32_t to_remove_{0}; + // Memory pool for recycling SchedulerItem objects to reduce heap churn. + // Design decisions: + // - std::vector is used instead of a fixed array because many systems only need 1-2 scheduler items + // - The vector grows dynamically up to MAX_POOL_SIZE (5) only when needed, saving memory on simple setups + // - Pool size of 5 matches typical usage (2-4 timers) while keeping memory overhead low (~250 bytes on ESP32) + // - The pool significantly reduces heap fragmentation which is critical because heap allocation/deallocation + // can stall the entire system, causing timing issues and dropped events for any components that need + // to synchronize between tasks (see https://github.com/esphome/backlog/issues/52) + std::vector> scheduler_item_pool_; + #ifdef ESPHOME_THREAD_MULTI_ATOMICS /* * Multi-threaded platforms with atomic support: last_millis_ needs atomic for lock-free updates diff --git a/tests/integration/fixtures/scheduler_pool.yaml b/tests/integration/fixtures/scheduler_pool.yaml new file mode 100644 index 0000000000..5389125188 --- /dev/null +++ b/tests/integration/fixtures/scheduler_pool.yaml @@ -0,0 +1,282 @@ +esphome: + name: scheduler-pool-test + on_boot: + priority: -100 + then: + - logger.log: "Starting scheduler pool tests" + debug_scheduler: true # Enable scheduler debug logging + +host: +api: + services: + - service: run_phase_1 + then: + - script.execute: test_pool_recycling + - service: run_phase_2 + then: + - script.execute: test_sensor_polling + - service: run_phase_3 + then: + - script.execute: test_communication_patterns + - service: run_phase_4 + then: + - script.execute: test_defer_patterns + - service: run_phase_5 + then: + - script.execute: test_pool_reuse_verification + - service: run_phase_6 + then: + - script.execute: test_full_pool_reuse + - service: run_phase_7 + then: + - script.execute: test_same_defer_optimization + - service: run_complete + then: + - script.execute: complete_test +logger: + level: VERY_VERBOSE # Need VERY_VERBOSE to see pool debug messages + +globals: + - id: create_count + type: int + initial_value: '0' + - id: cancel_count + type: int + initial_value: '0' + - id: interval_counter + type: int + initial_value: '0' + - id: pool_test_done + type: bool + initial_value: 'false' + +script: + - id: test_pool_recycling + then: + - logger.log: "Testing scheduler pool recycling with realistic usage patterns" + - lambda: |- + auto *component = id(test_sensor); + + // Simulate realistic component behavior with timeouts that complete naturally + ESP_LOGI("test", "Phase 1: Simulating normal component lifecycle"); + + // Sensor update timeouts (common pattern) + App.scheduler.set_timeout(component, "sensor_init", 10, []() { + ESP_LOGD("test", "Sensor initialized"); + id(create_count)++; + }); + + // Retry timeout (gets cancelled if successful) + App.scheduler.set_timeout(component, "retry_timeout", 50, []() { + ESP_LOGD("test", "Retry timeout executed"); + id(create_count)++; + }); + + // Simulate successful operation - cancel retry + App.scheduler.set_timeout(component, "success_sim", 20, []() { + ESP_LOGD("test", "Operation succeeded, cancelling retry"); + App.scheduler.cancel_timeout(id(test_sensor), "retry_timeout"); + id(cancel_count)++; + }); + + id(create_count) += 3; + ESP_LOGI("test", "Phase 1 complete"); + + - id: test_sensor_polling + then: + - lambda: |- + // Simulate sensor polling pattern + ESP_LOGI("test", "Phase 2: Simulating sensor polling patterns"); + auto *component = id(test_sensor); + + // Multiple sensors with different update intervals + // These should only allocate once and reuse the same item for each interval execution + App.scheduler.set_interval(component, "temp_sensor", 10, []() { + ESP_LOGD("test", "Temperature sensor update"); + id(interval_counter)++; + if (id(interval_counter) >= 3) { + App.scheduler.cancel_interval(id(test_sensor), "temp_sensor"); + ESP_LOGD("test", "Temperature sensor stopped"); + } + }); + + App.scheduler.set_interval(component, "humidity_sensor", 15, []() { + ESP_LOGD("test", "Humidity sensor update"); + id(interval_counter)++; + if (id(interval_counter) >= 5) { + App.scheduler.cancel_interval(id(test_sensor), "humidity_sensor"); + ESP_LOGD("test", "Humidity sensor stopped"); + } + }); + + // Only 2 allocations for the intervals, no matter how many times they execute + id(create_count) += 2; + ESP_LOGD("test", "Created 2 intervals - they will reuse same items for each execution"); + ESP_LOGI("test", "Phase 2 complete"); + + - id: test_communication_patterns + then: + - lambda: |- + // Simulate communication patterns (WiFi/API reconnects, etc) + ESP_LOGI("test", "Phase 3: Simulating communication patterns"); + auto *component = id(test_sensor); + + // Connection timeout pattern + App.scheduler.set_timeout(component, "connect_timeout", 200, []() { + ESP_LOGD("test", "Connection timeout - would retry"); + id(create_count)++; + + // Schedule retry + App.scheduler.set_timeout(id(test_sensor), "connect_retry", 100, []() { + ESP_LOGD("test", "Retrying connection"); + id(create_count)++; + }); + }); + + // Heartbeat pattern + App.scheduler.set_interval(component, "heartbeat", 50, []() { + ESP_LOGD("test", "Heartbeat"); + id(interval_counter)++; + if (id(interval_counter) >= 10) { + App.scheduler.cancel_interval(id(test_sensor), "heartbeat"); + ESP_LOGD("test", "Heartbeat stopped"); + } + }); + + id(create_count) += 2; + ESP_LOGI("test", "Phase 3 complete"); + + - id: test_defer_patterns + then: + - lambda: |- + // Simulate defer patterns (state changes, async operations) + ESP_LOGI("test", "Phase 4: Simulating heavy defer patterns like ratgdo"); + + auto *component = id(test_sensor); + + // Simulate a burst of defer operations like ratgdo does with state updates + // These should execute immediately and recycle quickly to the pool + for (int i = 0; i < 10; i++) { + std::string defer_name = "defer_" + std::to_string(i); + App.scheduler.set_timeout(component, defer_name, 0, [i]() { + ESP_LOGD("test", "Defer %d executed", i); + // Force a small delay between defer executions to see recycling + if (i == 5) { + ESP_LOGI("test", "Half of defers executed, checking pool status"); + } + }); + } + + id(create_count) += 10; + ESP_LOGD("test", "Created 10 defer operations (0ms timeouts)"); + + // Also create some named defers that might get replaced + App.scheduler.set_timeout(component, "state_update", 0, []() { + ESP_LOGD("test", "State update 1"); + }); + + // Replace the same named defer (should cancel previous) + App.scheduler.set_timeout(component, "state_update", 0, []() { + ESP_LOGD("test", "State update 2 (replaced)"); + }); + + id(create_count) += 2; + id(cancel_count) += 1; // One cancelled due to replacement + + ESP_LOGI("test", "Phase 4 complete"); + + - id: test_pool_reuse_verification + then: + - lambda: |- + ESP_LOGI("test", "Phase 5: Verifying pool reuse after everything settles"); + + // Cancel any remaining intervals + auto *component = id(test_sensor); + App.scheduler.cancel_interval(component, "temp_sensor"); + App.scheduler.cancel_interval(component, "humidity_sensor"); + App.scheduler.cancel_interval(component, "heartbeat"); + + ESP_LOGD("test", "Cancelled any remaining intervals"); + + // The pool should have items from completed timeouts in earlier phases. + // Phase 1 had 3 timeouts that completed and were recycled. + // Phase 3 had 1 timeout that completed and was recycled. + // Phase 4 had 3 defers that completed and were recycled. + // So we should have a decent pool size already from naturally completed items. + + // Now create 8 new timeouts - they should reuse from pool when available + int reuse_test_count = 8; + + for (int i = 0; i < reuse_test_count; i++) { + std::string name = "reuse_test_" + std::to_string(i); + App.scheduler.set_timeout(component, name, 10 + i * 5, [i]() { + ESP_LOGD("test", "Reuse test %d completed", i); + }); + } + + ESP_LOGI("test", "Created %d items for reuse verification", reuse_test_count); + id(create_count) += reuse_test_count; + ESP_LOGI("test", "Phase 5 complete"); + + - id: test_full_pool_reuse + then: + - lambda: |- + ESP_LOGI("test", "Phase 6: Testing pool size limits after Phase 5 items complete"); + + // At this point, all Phase 5 timeouts should have completed and been recycled. + // The pool should be at its maximum size (5). + // Creating 10 new items tests that: + // - First 5 items reuse from the pool + // - Remaining 5 items allocate new (pool empty) + // - Pool doesn't grow beyond MAX_POOL_SIZE of 5 + + auto *component = id(test_sensor); + int full_reuse_count = 10; + + for (int i = 0; i < full_reuse_count; i++) { + std::string name = "full_reuse_" + std::to_string(i); + App.scheduler.set_timeout(component, name, 10 + i * 5, [i]() { + ESP_LOGD("test", "Full reuse test %d completed", i); + }); + } + + ESP_LOGI("test", "Created %d items for full pool reuse verification", full_reuse_count); + id(create_count) += full_reuse_count; + ESP_LOGI("test", "Phase 6 complete"); + + - id: test_same_defer_optimization + then: + - lambda: |- + ESP_LOGI("test", "Phase 7: Testing same-named defer optimization"); + + auto *component = id(test_sensor); + + // Create 10 defers with the same name - should optimize to update callback in-place + // This pattern is common in components like ratgdo that repeatedly defer state updates + for (int i = 0; i < 10; i++) { + App.scheduler.set_timeout(component, "repeated_defer", 0, [i]() { + ESP_LOGD("test", "Repeated defer executed with value: %d", i); + }); + } + + // Only the first should allocate, the rest should update in-place + // We expect only 1 allocation for all 10 operations + id(create_count) += 1; // Only count 1 since others should be optimized + + ESP_LOGD("test", "Created 10 same-named defers (should only allocate once)"); + ESP_LOGI("test", "Phase 7 complete"); + + - id: complete_test + then: + - lambda: |- + ESP_LOGI("test", "Pool recycling test complete - created %d items, cancelled %d, intervals %d", + id(create_count), id(cancel_count), id(interval_counter)); + +sensor: + - platform: template + name: Test Sensor + id: test_sensor + lambda: return 1.0; + update_interval: never + +# No interval - tests will be triggered from Python via API services diff --git a/tests/integration/test_scheduler_pool.py b/tests/integration/test_scheduler_pool.py new file mode 100644 index 0000000000..b5f9f12631 --- /dev/null +++ b/tests/integration/test_scheduler_pool.py @@ -0,0 +1,209 @@ +"""Integration test for scheduler memory pool functionality.""" + +from __future__ import annotations + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_scheduler_pool( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that the scheduler memory pool is working correctly with realistic usage. + + This test simulates real-world scheduler usage patterns and verifies that: + 1. Items are recycled to the pool when timeouts complete naturally + 2. Items are recycled when intervals/timeouts are cancelled + 3. Items are reused from the pool for new scheduler operations + 4. The pool grows gradually based on actual usage patterns + 5. Pool operations are logged correctly with debug scheduler enabled + """ + # Track log messages to verify pool behavior + log_lines: list[str] = [] + pool_reuse_count = 0 + pool_recycle_count = 0 + pool_full_count = 0 + new_alloc_count = 0 + + # Patterns to match pool operations + reuse_pattern = re.compile(r"Reused item from pool \(pool size now: (\d+)\)") + recycle_pattern = re.compile(r"Recycled item to pool \(pool size now: (\d+)\)") + pool_full_pattern = re.compile(r"Pool full \(size: (\d+)\), deleting item") + new_alloc_pattern = re.compile(r"Allocated new item \(pool empty\)") + + # Futures to track when test phases complete + loop = asyncio.get_running_loop() + test_complete_future: asyncio.Future[bool] = loop.create_future() + phase_futures = { + 1: loop.create_future(), + 2: loop.create_future(), + 3: loop.create_future(), + 4: loop.create_future(), + 5: loop.create_future(), + 6: loop.create_future(), + 7: loop.create_future(), + } + + def check_output(line: str) -> None: + """Check log output for pool operations and phase completion.""" + nonlocal pool_reuse_count, pool_recycle_count, pool_full_count, new_alloc_count + log_lines.append(line) + + # Track pool operations + if reuse_pattern.search(line): + pool_reuse_count += 1 + + elif recycle_pattern.search(line): + pool_recycle_count += 1 + + elif pool_full_pattern.search(line): + pool_full_count += 1 + + elif new_alloc_pattern.search(line): + new_alloc_count += 1 + + # Track phase completion + for phase_num in range(1, 8): + if ( + f"Phase {phase_num} complete" in line + and phase_num in phase_futures + and not phase_futures[phase_num].done() + ): + phase_futures[phase_num].set_result(True) + + # Check for test completion + if "Pool recycling test complete" in line and not test_complete_future.done(): + test_complete_future.set_result(True) + + # Run the test with log monitoring + async with ( + run_compiled(yaml_config, line_callback=check_output), + api_client_connected() as client, + ): + # Verify device is running + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "scheduler-pool-test" + + # Get list of services + entities, services = await client.list_entities_services() + service_names = {s.name for s in services} + + # Verify all test services are available + expected_services = { + "run_phase_1", + "run_phase_2", + "run_phase_3", + "run_phase_4", + "run_phase_5", + "run_phase_6", + "run_phase_7", + "run_complete", + } + assert expected_services.issubset(service_names), ( + f"Missing services: {expected_services - service_names}" + ) + + # Get service objects + phase_services = { + num: next(s for s in services if s.name == f"run_phase_{num}") + for num in range(1, 8) + } + complete_service = next(s for s in services if s.name == "run_complete") + + try: + # Phase 1: Component lifecycle + client.execute_service(phase_services[1], {}) + await asyncio.wait_for(phase_futures[1], timeout=1.0) + await asyncio.sleep(0.05) # Let timeouts complete + + # Phase 2: Sensor polling + client.execute_service(phase_services[2], {}) + await asyncio.wait_for(phase_futures[2], timeout=1.0) + await asyncio.sleep(0.1) # Let intervals run a bit + + # Phase 3: Communication patterns + client.execute_service(phase_services[3], {}) + await asyncio.wait_for(phase_futures[3], timeout=1.0) + await asyncio.sleep(0.1) # Let heartbeat run + + # Phase 4: Defer patterns + client.execute_service(phase_services[4], {}) + await asyncio.wait_for(phase_futures[4], timeout=1.0) + await asyncio.sleep(0.2) # Let everything settle and recycle + + # Phase 5: Pool reuse verification + client.execute_service(phase_services[5], {}) + await asyncio.wait_for(phase_futures[5], timeout=1.0) + await asyncio.sleep(0.1) # Let Phase 5 timeouts complete and recycle + + # Phase 6: Full pool reuse verification + client.execute_service(phase_services[6], {}) + await asyncio.wait_for(phase_futures[6], timeout=1.0) + await asyncio.sleep(0.1) # Let Phase 6 timeouts complete + + # Phase 7: Same-named defer optimization + client.execute_service(phase_services[7], {}) + await asyncio.wait_for(phase_futures[7], timeout=1.0) + await asyncio.sleep(0.05) # Let the single defer execute + + # Complete test + client.execute_service(complete_service, {}) + await asyncio.wait_for(test_complete_future, timeout=0.5) + + except TimeoutError as e: + # Print debug info if test times out + recent_logs = "\n".join(log_lines[-30:]) + phases_completed = [num for num, fut in phase_futures.items() if fut.done()] + pytest.fail( + f"Test timed out waiting for phase/completion. Error: {e}\n" + f" Phases completed: {phases_completed}\n" + f" Pool stats:\n" + f" Reuse count: {pool_reuse_count}\n" + f" Recycle count: {pool_recycle_count}\n" + f" Pool full count: {pool_full_count}\n" + f" New alloc count: {new_alloc_count}\n" + f"Recent logs:\n{recent_logs}" + ) + + # Verify all test phases ran + for phase_num in range(1, 8): + assert phase_futures[phase_num].done(), f"Phase {phase_num} did not complete" + + # Verify pool behavior + assert pool_recycle_count > 0, "Should have recycled items to pool" + + # Check pool metrics + if pool_recycle_count > 0: + max_pool_size = 0 + for line in log_lines: + if match := recycle_pattern.search(line): + size = int(match.group(1)) + max_pool_size = max(max_pool_size, size) + + # Pool can grow up to its maximum of 5 + assert max_pool_size <= 5, f"Pool grew beyond maximum ({max_pool_size})" + + # Log summary for debugging + print("\nScheduler Pool Test Summary (Python Orchestrated):") + print(f" Items recycled to pool: {pool_recycle_count}") + print(f" Items reused from pool: {pool_reuse_count}") + print(f" Pool full events: {pool_full_count}") + print(f" New allocations: {new_alloc_count}") + print(" All phases completed successfully") + + # Verify reuse happened + if pool_reuse_count == 0 and pool_recycle_count > 3: + pytest.fail("Pool had items recycled but none were reused") + + # Success - pool is working + assert pool_recycle_count > 0 or new_alloc_count < 15, ( + "Pool should either recycle items or limit new allocations" + ) From 91228c82e659b42c6d5e3540e2af174e4573e3ee Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 7 Sep 2025 17:29:45 -0500 Subject: [PATCH 172/208] [esp8266][logger] Store LOG_LEVELS strings in PROGMEM to reduce RAM usage (#10569) --- esphome/components/logger/logger.cpp | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 0ade9cedae..5f0e78fc0d 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -246,14 +246,35 @@ void Logger::add_on_log_callback(std::functionlog_callback_.add(std::move(callback)); } float Logger::get_setup_priority() const { return setup_priority::BUS + 500.0f; } + +#ifdef USE_STORE_LOG_STR_IN_FLASH +// ESP8266: PSTR() cannot be used in array initializers, so we need to declare +// each string separately as a global constant first +static const char LOG_LEVEL_NONE[] PROGMEM = "NONE"; +static const char LOG_LEVEL_ERROR[] PROGMEM = "ERROR"; +static const char LOG_LEVEL_WARN[] PROGMEM = "WARN"; +static const char LOG_LEVEL_INFO[] PROGMEM = "INFO"; +static const char LOG_LEVEL_CONFIG[] PROGMEM = "CONFIG"; +static const char LOG_LEVEL_DEBUG[] PROGMEM = "DEBUG"; +static const char LOG_LEVEL_VERBOSE[] PROGMEM = "VERBOSE"; +static const char LOG_LEVEL_VERY_VERBOSE[] PROGMEM = "VERY_VERBOSE"; + +static const LogString *const LOG_LEVELS[] = { + reinterpret_cast(LOG_LEVEL_NONE), reinterpret_cast(LOG_LEVEL_ERROR), + reinterpret_cast(LOG_LEVEL_WARN), reinterpret_cast(LOG_LEVEL_INFO), + reinterpret_cast(LOG_LEVEL_CONFIG), reinterpret_cast(LOG_LEVEL_DEBUG), + reinterpret_cast(LOG_LEVEL_VERBOSE), reinterpret_cast(LOG_LEVEL_VERY_VERBOSE), +}; +#else static const char *const LOG_LEVELS[] = {"NONE", "ERROR", "WARN", "INFO", "CONFIG", "DEBUG", "VERBOSE", "VERY_VERBOSE"}; +#endif void Logger::dump_config() { ESP_LOGCONFIG(TAG, "Logger:\n" " Max Level: %s\n" " Initial Level: %s", - LOG_LEVELS[ESPHOME_LOG_LEVEL], LOG_LEVELS[this->current_level_]); + LOG_STR_ARG(LOG_LEVELS[ESPHOME_LOG_LEVEL]), LOG_STR_ARG(LOG_LEVELS[this->current_level_])); #ifndef USE_HOST ESP_LOGCONFIG(TAG, " Log Baud Rate: %" PRIu32 "\n" @@ -267,14 +288,14 @@ void Logger::dump_config() { #endif for (auto &it : this->log_levels_) { - ESP_LOGCONFIG(TAG, " Level for '%s': %s", it.first.c_str(), LOG_LEVELS[it.second]); + ESP_LOGCONFIG(TAG, " Level for '%s': %s", it.first.c_str(), LOG_STR_ARG(LOG_LEVELS[it.second])); } } void Logger::set_log_level(uint8_t level) { if (level > ESPHOME_LOG_LEVEL) { level = ESPHOME_LOG_LEVEL; - ESP_LOGW(TAG, "Cannot set log level higher than pre-compiled %s", LOG_LEVELS[ESPHOME_LOG_LEVEL]); + ESP_LOGW(TAG, "Cannot set log level higher than pre-compiled %s", LOG_STR_ARG(LOG_LEVELS[ESPHOME_LOG_LEVEL])); } this->current_level_ = level; this->level_callback_.call(level); From 1ac07c96b18f0f8d2402f6c9ccdaa7ea3e3ef5d6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 7 Sep 2025 17:30:39 -0500 Subject: [PATCH 173/208] [esphome] Store OTA component log strings in flash on ESP8266 (#10570) --- esphome/components/esphome/ota/ota_esphome.h | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/esphome/ota/ota_esphome.h b/esphome/components/esphome/ota/ota_esphome.h index 3a5d9f4f7a..55adec351d 100644 --- a/esphome/components/esphome/ota/ota_esphome.h +++ b/esphome/components/esphome/ota/ota_esphome.h @@ -5,6 +5,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" #include "esphome/core/preferences.h" +#include "esphome/core/log.h" #include "esphome/components/ota/ota_backend.h" #include "esphome/components/socket/socket.h" From 3cf36e2f94d56a6062fd5d1ea654e9e6b922b9fd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 7 Sep 2025 17:32:21 -0500 Subject: [PATCH 174/208] Bump aioesphomeapi from 40.0.1 to 40.0.2 (#10641) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 1a836bd64f..6021b6de70 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile esptool==5.0.2 click==8.1.7 esphome-dashboard==20250904.0 -aioesphomeapi==40.0.1 +aioesphomeapi==40.0.2 zeroconf==0.147.2 puremagic==1.30 ruamel.yaml==0.18.15 # dashboard_import From 8c28f346c75883713ea00d68c7a077fbd40ac7a9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 7 Sep 2025 17:57:02 -0500 Subject: [PATCH 175/208] [gpio_expander] Add intelligent pin type selection to CachedGpioExpander template (#10577) --- .../components/gpio_expander/cached_gpio.h | 52 +++++++++++++------ .../gpio_expander_test_component.cpp | 2 + .../__init__.py | 24 +++++++++ .../gpio_expander_test_component_uint16.cpp | 43 +++++++++++++++ .../gpio_expander_test_component_uint16.h | 23 ++++++++ .../fixtures/gpio_expander_cache.yaml | 6 ++- tests/integration/test_gpio_expander_cache.py | 43 +++++++++++---- 7 files changed, 166 insertions(+), 27 deletions(-) create mode 100644 tests/integration/fixtures/external_components/gpio_expander_test_component_uint16/__init__.py create mode 100644 tests/integration/fixtures/external_components/gpio_expander_test_component_uint16/gpio_expander_test_component_uint16.cpp create mode 100644 tests/integration/fixtures/external_components/gpio_expander_test_component_uint16/gpio_expander_test_component_uint16.h diff --git a/esphome/components/gpio_expander/cached_gpio.h b/esphome/components/gpio_expander/cached_gpio.h index d7230eb0b3..eeff98cb6e 100644 --- a/esphome/components/gpio_expander/cached_gpio.h +++ b/esphome/components/gpio_expander/cached_gpio.h @@ -4,6 +4,7 @@ #include #include #include +#include #include "esphome/core/hal.h" namespace esphome::gpio_expander { @@ -11,18 +12,27 @@ namespace esphome::gpio_expander { /// @brief A class to cache the read state of a GPIO expander. /// This class caches reads between GPIO Pins which are on the same bank. /// This means that for reading whole Port (ex. 8 pins) component needs only one -/// I2C/SPI read per main loop call. It assumes, that one bit in byte identifies one GPIO pin +/// I2C/SPI read per main loop call. It assumes that one bit in byte identifies one GPIO pin. +/// /// Template parameters: -/// T - Type which represents internal register. Could be uint8_t or uint16_t. Adjust to -/// match size of your internal GPIO bank register. -/// N - Number of pins -template class CachedGpioExpander { +/// T - Type which represents internal bank register. Could be uint8_t or uint16_t. +/// Choose based on how your I/O expander reads pins: +/// * uint8_t: For chips that read banks separately (8 pins at a time) +/// Examples: MCP23017 (2x8-bit banks), TCA9555 (2x8-bit banks) +/// * uint16_t: For chips that read all pins at once (up to 16 pins) +/// Examples: PCF8574/8575 (8/16 pins), PCA9554/9555 (8/16 pins) +/// N - Total number of pins (maximum 65535) +/// P - Type for pin number parameters (automatically selected based on N: +/// uint8_t for N<=256, uint16_t for N>256). Can be explicitly specified +/// if needed (e.g., for components like SN74HC165 with >256 pins) +template 256), uint16_t, uint8_t>::type> +class CachedGpioExpander { public: /// @brief Read the state of the given pin. This will invalidate the cache for the given pin number. /// @param pin Pin number to read /// @return Pin state - bool digital_read(T pin) { - const uint8_t bank = pin / BANK_SIZE; + bool digital_read(P pin) { + const P bank = pin / BANK_SIZE; const T pin_mask = (1 << (pin % BANK_SIZE)); // Check if specific pin cache is valid if (this->read_cache_valid_[bank] & pin_mask) { @@ -38,21 +48,31 @@ template class CachedGpioExpander { return this->digital_read_cache(pin); } - void digital_write(T pin, bool value) { this->digital_write_hw(pin, value); } + void digital_write(P pin, bool value) { this->digital_write_hw(pin, value); } protected: - /// @brief Call component low level function to read GPIO state from device - virtual bool digital_read_hw(T pin) = 0; - /// @brief Call component read function from internal cache. - virtual bool digital_read_cache(T pin) = 0; - /// @brief Call component low level function to write GPIO state to device - virtual void digital_write_hw(T pin, bool value) = 0; + /// @brief Read GPIO bank from hardware into internal state + /// @param pin Pin number (used to determine which bank to read) + /// @return true if read succeeded, false on communication error + /// @note This does NOT return the pin state. It returns whether the read operation succeeded. + /// The actual pin state should be returned by digital_read_cache(). + virtual bool digital_read_hw(P pin) = 0; + + /// @brief Get cached pin value from internal state + /// @param pin Pin number to read + /// @return Pin state (true = HIGH, false = LOW) + virtual bool digital_read_cache(P pin) = 0; + + /// @brief Write GPIO state to hardware + /// @param pin Pin number to write + /// @param value Pin state to write (true = HIGH, false = LOW) + virtual void digital_write_hw(P pin, bool value) = 0; /// @brief Invalidate cache. This function should be called in component loop(). void reset_pin_cache_() { memset(this->read_cache_valid_, 0x00, CACHE_SIZE_BYTES); } - static constexpr uint8_t BITS_PER_BYTE = 8; - static constexpr uint8_t BANK_SIZE = sizeof(T) * BITS_PER_BYTE; + static constexpr uint16_t BITS_PER_BYTE = 8; + static constexpr uint16_t BANK_SIZE = sizeof(T) * BITS_PER_BYTE; static constexpr size_t BANKS = N / BANK_SIZE; static constexpr size_t CACHE_SIZE_BYTES = BANKS * sizeof(T); diff --git a/tests/integration/fixtures/external_components/gpio_expander_test_component/gpio_expander_test_component.cpp b/tests/integration/fixtures/external_components/gpio_expander_test_component/gpio_expander_test_component.cpp index 7e88950592..6e128687c4 100644 --- a/tests/integration/fixtures/external_components/gpio_expander_test_component/gpio_expander_test_component.cpp +++ b/tests/integration/fixtures/external_components/gpio_expander_test_component/gpio_expander_test_component.cpp @@ -27,11 +27,13 @@ void GPIOExpanderTestComponent::setup() { bool GPIOExpanderTestComponent::digital_read_hw(uint8_t pin) { ESP_LOGD(TAG, "digital_read_hw pin=%d", pin); + // Return true to indicate successful read operation return true; } bool GPIOExpanderTestComponent::digital_read_cache(uint8_t pin) { ESP_LOGD(TAG, "digital_read_cache pin=%d", pin); + // Return the pin state (always HIGH for testing) return true; } diff --git a/tests/integration/fixtures/external_components/gpio_expander_test_component_uint16/__init__.py b/tests/integration/fixtures/external_components/gpio_expander_test_component_uint16/__init__.py new file mode 100644 index 0000000000..76f20b942c --- /dev/null +++ b/tests/integration/fixtures/external_components/gpio_expander_test_component_uint16/__init__.py @@ -0,0 +1,24 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_ID + +AUTO_LOAD = ["gpio_expander"] + +gpio_expander_test_component_uint16_ns = cg.esphome_ns.namespace( + "gpio_expander_test_component_uint16" +) + +GPIOExpanderTestUint16Component = gpio_expander_test_component_uint16_ns.class_( + "GPIOExpanderTestUint16Component", cg.Component +) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(GPIOExpanderTestUint16Component), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) diff --git a/tests/integration/fixtures/external_components/gpio_expander_test_component_uint16/gpio_expander_test_component_uint16.cpp b/tests/integration/fixtures/external_components/gpio_expander_test_component_uint16/gpio_expander_test_component_uint16.cpp new file mode 100644 index 0000000000..09537c81bb --- /dev/null +++ b/tests/integration/fixtures/external_components/gpio_expander_test_component_uint16/gpio_expander_test_component_uint16.cpp @@ -0,0 +1,43 @@ +#include "gpio_expander_test_component_uint16.h" +#include "esphome/core/log.h" + +namespace esphome::gpio_expander_test_component_uint16 { + +static const char *const TAG = "gpio_expander_test_uint16"; + +void GPIOExpanderTestUint16Component::setup() { + ESP_LOGD(TAG, "Testing uint16_t bank (single 16-pin bank)"); + + // Test reading all 16 pins - first should trigger hw read, rest use cache + for (uint8_t pin = 0; pin < 16; pin++) { + this->digital_read(pin); + } + + // Reset cache and test specific reads + ESP_LOGD(TAG, "Resetting cache for uint16_t test"); + this->reset_pin_cache_(); + + // First read triggers hw for entire bank + this->digital_read(5); + // These should all use cache since they're in the same bank + this->digital_read(10); + this->digital_read(15); + this->digital_read(0); + + ESP_LOGD(TAG, "DONE_UINT16"); +} + +bool GPIOExpanderTestUint16Component::digital_read_hw(uint8_t pin) { + ESP_LOGD(TAG, "uint16_digital_read_hw pin=%d", pin); + // In a real component, this would read from I2C/SPI into internal state + // For testing, we just return true to indicate successful read + return true; // Return true to indicate successful read +} + +bool GPIOExpanderTestUint16Component::digital_read_cache(uint8_t pin) { + ESP_LOGD(TAG, "uint16_digital_read_cache pin=%d", pin); + // Return the actual pin state from our test pattern + return (this->test_state_ >> pin) & 1; +} + +} // namespace esphome::gpio_expander_test_component_uint16 diff --git a/tests/integration/fixtures/external_components/gpio_expander_test_component_uint16/gpio_expander_test_component_uint16.h b/tests/integration/fixtures/external_components/gpio_expander_test_component_uint16/gpio_expander_test_component_uint16.h new file mode 100644 index 0000000000..be102f9b57 --- /dev/null +++ b/tests/integration/fixtures/external_components/gpio_expander_test_component_uint16/gpio_expander_test_component_uint16.h @@ -0,0 +1,23 @@ +#pragma once + +#include "esphome/components/gpio_expander/cached_gpio.h" +#include "esphome/core/component.h" + +namespace esphome::gpio_expander_test_component_uint16 { + +// Test component using uint16_t bank type (single 16-pin bank) +class GPIOExpanderTestUint16Component : public Component, + public esphome::gpio_expander::CachedGpioExpander { + public: + void setup() override; + + protected: + bool digital_read_hw(uint8_t pin) override; + bool digital_read_cache(uint8_t pin) override; + void digital_write_hw(uint8_t pin, bool value) override{}; + + private: + uint16_t test_state_{0xAAAA}; // Test pattern: alternating bits +}; + +} // namespace esphome::gpio_expander_test_component_uint16 diff --git a/tests/integration/fixtures/gpio_expander_cache.yaml b/tests/integration/fixtures/gpio_expander_cache.yaml index 7d7ca1a876..8b5375af4c 100644 --- a/tests/integration/fixtures/gpio_expander_cache.yaml +++ b/tests/integration/fixtures/gpio_expander_cache.yaml @@ -12,6 +12,10 @@ external_components: - source: type: local path: EXTERNAL_COMPONENT_PATH - components: [gpio_expander_test_component] + components: [gpio_expander_test_component, gpio_expander_test_component_uint16] +# Test with uint8_t (multiple banks) gpio_expander_test_component: + +# Test with uint16_t (single bank) +gpio_expander_test_component_uint16: diff --git a/tests/integration/test_gpio_expander_cache.py b/tests/integration/test_gpio_expander_cache.py index 9353bb1dd6..e5f0f2818f 100644 --- a/tests/integration/test_gpio_expander_cache.py +++ b/tests/integration/test_gpio_expander_cache.py @@ -30,9 +30,15 @@ async def test_gpio_expander_cache( logs_done = asyncio.Event() - # Patterns to match in logs - digital_read_hw_pattern = re.compile(r"digital_read_hw pin=(\d+)") - digital_read_cache_pattern = re.compile(r"digital_read_cache pin=(\d+)") + # Patterns to match in logs - match any variation of digital_read + read_hw_pattern = re.compile(r"(?:uint16_)?digital_read_hw pin=(\d+)") + read_cache_pattern = re.compile(r"(?:uint16_)?digital_read_cache pin=(\d+)") + + # Keep specific patterns for building the expected order + digital_read_hw_pattern = re.compile(r"^digital_read_hw pin=(\d+)") + digital_read_cache_pattern = re.compile(r"^digital_read_cache pin=(\d+)") + uint16_read_hw_pattern = re.compile(r"^uint16_digital_read_hw pin=(\d+)") + uint16_read_cache_pattern = re.compile(r"^uint16_digital_read_cache pin=(\d+)") # ensure logs are in the expected order log_order = [ @@ -59,6 +65,17 @@ async def test_gpio_expander_cache( (digital_read_cache_pattern, 14), (digital_read_hw_pattern, 14), (digital_read_cache_pattern, 14), + # uint16_t component tests (single bank of 16 pins) + (uint16_read_hw_pattern, 0), # First pin triggers hw read + [ + (uint16_read_cache_pattern, i) for i in range(0, 16) + ], # All 16 pins return via cache + # After cache reset + (uint16_read_hw_pattern, 5), # First read after reset triggers hw + (uint16_read_cache_pattern, 5), + (uint16_read_cache_pattern, 10), # These use cache (same bank) + (uint16_read_cache_pattern, 15), + (uint16_read_cache_pattern, 0), ] # Flatten the log order for easier processing log_order: list[tuple[re.Pattern, int]] = [ @@ -77,17 +94,22 @@ async def test_gpio_expander_cache( clean_line = re.sub(r"\x1b\[[0-9;]*m", "", line) - if "digital_read" in clean_line: + # Extract just the log message part (after the log level) + msg = clean_line.split(": ", 1)[-1] if ": " in clean_line else clean_line + + # Check if this line contains a read operation we're tracking + if read_hw_pattern.search(msg) or read_cache_pattern.search(msg): if index >= len(log_order): - print(f"Received unexpected log line: {clean_line}") + print(f"Received unexpected log line: {msg}") logs_done.set() return pattern, expected_pin = log_order[index] - match = pattern.search(clean_line) + match = pattern.search(msg) if not match: - print(f"Log line did not match next expected pattern: {clean_line}") + print(f"Log line did not match next expected pattern: {msg}") + print(f"Expected pattern: {pattern.pattern}") logs_done.set() return @@ -99,9 +121,10 @@ async def test_gpio_expander_cache( index += 1 - elif "DONE" in clean_line: - # Check if we reached the end of the expected log entries - logs_done.set() + elif "DONE_UINT16" in clean_line: + # uint16 component is done, check if we've seen all expected logs + if index == len(log_order): + logs_done.set() # Run with log monitoring async with ( From 629f1e94f1934a6c57424460dd0417b451103b22 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 8 Sep 2025 10:58:41 +1200 Subject: [PATCH 176/208] [ota] Fix duplicate include and sort (#10643) --- esphome/components/esphome/ota/ota_esphome.h | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/esphome/components/esphome/ota/ota_esphome.h b/esphome/components/esphome/ota/ota_esphome.h index 55adec351d..f5a3e43ae3 100644 --- a/esphome/components/esphome/ota/ota_esphome.h +++ b/esphome/components/esphome/ota/ota_esphome.h @@ -2,12 +2,11 @@ #include "esphome/core/defines.h" #ifdef USE_OTA +#include "esphome/components/ota/ota_backend.h" +#include "esphome/components/socket/socket.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" #include "esphome/core/preferences.h" -#include "esphome/core/log.h" -#include "esphome/components/ota/ota_backend.h" -#include "esphome/components/socket/socket.h" namespace esphome { From 0cc0979674566d3a1ca2db86ec364ccbfdbb9823 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 7 Sep 2025 17:59:23 -0500 Subject: [PATCH 177/208] [pca6416a] Migrate to CachedGpioExpander to reduce I2C bus usage (#10587) --- esphome/components/pca6416a/__init__.py | 1 + esphome/components/pca6416a/pca6416a.cpp | 25 +++++++++++++++++++----- esphome/components/pca6416a/pca6416a.h | 17 +++++++++++----- 3 files changed, 33 insertions(+), 10 deletions(-) diff --git a/esphome/components/pca6416a/__init__.py b/esphome/components/pca6416a/__init__.py index da6c4623c9..e540edb91f 100644 --- a/esphome/components/pca6416a/__init__.py +++ b/esphome/components/pca6416a/__init__.py @@ -14,6 +14,7 @@ from esphome.const import ( CODEOWNERS = ["@Mat931"] DEPENDENCIES = ["i2c"] +AUTO_LOAD = ["gpio_expander"] MULTI_CONF = True pca6416a_ns = cg.esphome_ns.namespace("pca6416a") diff --git a/esphome/components/pca6416a/pca6416a.cpp b/esphome/components/pca6416a/pca6416a.cpp index 730c494e34..c0056e780b 100644 --- a/esphome/components/pca6416a/pca6416a.cpp +++ b/esphome/components/pca6416a/pca6416a.cpp @@ -51,6 +51,11 @@ void PCA6416AComponent::setup() { this->status_has_error()); } +void PCA6416AComponent::loop() { + // Invalidate cache at the start of each loop + this->reset_pin_cache_(); +} + void PCA6416AComponent::dump_config() { if (this->has_pullup_) { ESP_LOGCONFIG(TAG, "PCAL6416A:"); @@ -63,15 +68,25 @@ void PCA6416AComponent::dump_config() { } } -bool PCA6416AComponent::digital_read(uint8_t pin) { - uint8_t bit = pin % 8; +bool PCA6416AComponent::digital_read_hw(uint8_t pin) { uint8_t reg_addr = pin < 8 ? PCA6416A_INPUT0 : PCA6416A_INPUT1; uint8_t value = 0; - this->read_register_(reg_addr, &value); - return value & (1 << bit); + if (!this->read_register_(reg_addr, &value)) { + return false; + } + + // Update the appropriate part of input_mask_ + if (pin < 8) { + this->input_mask_ = (this->input_mask_ & 0xFF00) | value; + } else { + this->input_mask_ = (this->input_mask_ & 0x00FF) | (uint16_t(value) << 8); + } + return true; } -void PCA6416AComponent::digital_write(uint8_t pin, bool value) { +bool PCA6416AComponent::digital_read_cache(uint8_t pin) { return this->input_mask_ & (1 << pin); } + +void PCA6416AComponent::digital_write_hw(uint8_t pin, bool value) { uint8_t reg_addr = pin < 8 ? PCA6416A_OUTPUT0 : PCA6416A_OUTPUT1; this->update_register_(pin, value, reg_addr); } diff --git a/esphome/components/pca6416a/pca6416a.h b/esphome/components/pca6416a/pca6416a.h index 1e8015c40a..10a4a64e9b 100644 --- a/esphome/components/pca6416a/pca6416a.h +++ b/esphome/components/pca6416a/pca6416a.h @@ -3,20 +3,20 @@ #include "esphome/core/component.h" #include "esphome/core/hal.h" #include "esphome/components/i2c/i2c.h" +#include "esphome/components/gpio_expander/cached_gpio.h" namespace esphome { namespace pca6416a { -class PCA6416AComponent : public Component, public i2c::I2CDevice { +class PCA6416AComponent : public Component, + public i2c::I2CDevice, + public gpio_expander::CachedGpioExpander { public: PCA6416AComponent() = default; /// Check i2c availability and setup masks void setup() override; - /// Helper function to read the value of a pin. - bool digital_read(uint8_t pin); - /// Helper function to write the value of a pin. - void digital_write(uint8_t pin, bool value); + void loop() override; /// Helper function to set the pin mode of a pin. void pin_mode(uint8_t pin, gpio::Flags flags); @@ -25,6 +25,11 @@ class PCA6416AComponent : public Component, public i2c::I2CDevice { void dump_config() override; protected: + // Virtual methods from CachedGpioExpander + bool digital_read_hw(uint8_t pin) override; + bool digital_read_cache(uint8_t pin) override; + void digital_write_hw(uint8_t pin, bool value) override; + bool read_register_(uint8_t reg, uint8_t *value); bool write_register_(uint8_t reg, uint8_t value); void update_register_(uint8_t pin, bool pin_value, uint8_t reg_addr); @@ -32,6 +37,8 @@ class PCA6416AComponent : public Component, public i2c::I2CDevice { /// The mask to write as output state - 1 means HIGH, 0 means LOW uint8_t output_0_{0x00}; uint8_t output_1_{0x00}; + /// Cache for input values (16-bit combined for both banks) + uint16_t input_mask_{0x00}; /// Storage for last I2C error seen esphome::i2c::ErrorCode last_error_; /// Only the PCAL6416A has pull-up resistors From 93da52c4d294561fed5b82bf7d36340b62be098d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 7 Sep 2025 18:26:11 -0500 Subject: [PATCH 178/208] [pca9554] Migrate to CachedGpioExpander to reduce I2C bus usage (#10571) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 2 +- esphome/components/pca9554/__init__.py | 3 ++- esphome/components/pca9554/pca9554.cpp | 30 +++++++++++--------------- esphome/components/pca9554/pca9554.h | 19 ++++++++-------- 4 files changed, 25 insertions(+), 29 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 116f35f3b6..c34774fce6 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -342,7 +342,7 @@ esphome/components/ota/* @esphome/core esphome/components/output/* @esphome/core esphome/components/packet_transport/* @clydebarrow esphome/components/pca6416a/* @Mat931 -esphome/components/pca9554/* @clydebarrow @hwstar +esphome/components/pca9554/* @bdraco @clydebarrow @hwstar esphome/components/pcf85063/* @brogon esphome/components/pcf8563/* @KoenBreeman esphome/components/pi4ioe5v6408/* @jesserockz diff --git a/esphome/components/pca9554/__init__.py b/esphome/components/pca9554/__init__.py index 05713cccda..626b08a378 100644 --- a/esphome/components/pca9554/__init__.py +++ b/esphome/components/pca9554/__init__.py @@ -11,7 +11,8 @@ from esphome.const import ( CONF_OUTPUT, ) -CODEOWNERS = ["@hwstar", "@clydebarrow"] +CODEOWNERS = ["@hwstar", "@clydebarrow", "@bdraco"] +AUTO_LOAD = ["gpio_expander"] DEPENDENCIES = ["i2c"] MULTI_CONF = True CONF_PIN_COUNT = "pin_count" diff --git a/esphome/components/pca9554/pca9554.cpp b/esphome/components/pca9554/pca9554.cpp index 1166cc1a09..e8d49f66e2 100644 --- a/esphome/components/pca9554/pca9554.cpp +++ b/esphome/components/pca9554/pca9554.cpp @@ -37,10 +37,9 @@ void PCA9554Component::setup() { } void PCA9554Component::loop() { - // The read_inputs_() method will cache the input values from the chip. - this->read_inputs_(); - // Clear all the previously read flags. - this->was_previously_read_ = 0x00; + // Invalidate the cache at the start of each loop. + // The actual read will happen on demand when digital_read() is called + this->reset_pin_cache_(); } void PCA9554Component::dump_config() { @@ -54,21 +53,17 @@ void PCA9554Component::dump_config() { } } -bool PCA9554Component::digital_read(uint8_t pin) { - // Note: We want to try and avoid doing any I2C bus read transactions here - // to conserve I2C bus bandwidth. So what we do is check to see if we - // have seen a read during the time esphome is running this loop. If we have, - // we do an I2C bus transaction to get the latest value. If we haven't - // we return a cached value which was read at the time loop() was called. - if (this->was_previously_read_ & (1 << pin)) - this->read_inputs_(); // Force a read of a new value - // Indicate we saw a read request for this pin in case a - // read happens later in the same loop. - this->was_previously_read_ |= (1 << pin); +bool PCA9554Component::digital_read_hw(uint8_t pin) { + // Read all pins from hardware into input_mask_ + return this->read_inputs_(); // Return true if I2C read succeeded, false on error +} + +bool PCA9554Component::digital_read_cache(uint8_t pin) { + // Return the cached pin state from input_mask_ return this->input_mask_ & (1 << pin); } -void PCA9554Component::digital_write(uint8_t pin, bool value) { +void PCA9554Component::digital_write_hw(uint8_t pin, bool value) { if (value) { this->output_mask_ |= (1 << pin); } else { @@ -127,8 +122,7 @@ bool PCA9554Component::write_register_(uint8_t reg, uint16_t value) { float PCA9554Component::get_setup_priority() const { return setup_priority::IO; } -// Run our loop() method very early in the loop, so that we cache read values before -// before other components call our digital_read() method. +// Run our loop() method early to invalidate cache before any other components access the pins float PCA9554Component::get_loop_priority() const { return 9.0f; } // Just after WIFI void PCA9554GPIOPin::setup() { pin_mode(flags_); } diff --git a/esphome/components/pca9554/pca9554.h b/esphome/components/pca9554/pca9554.h index efeec4d306..7b356b4068 100644 --- a/esphome/components/pca9554/pca9554.h +++ b/esphome/components/pca9554/pca9554.h @@ -3,22 +3,21 @@ #include "esphome/core/component.h" #include "esphome/core/hal.h" #include "esphome/components/i2c/i2c.h" +#include "esphome/components/gpio_expander/cached_gpio.h" namespace esphome { namespace pca9554 { -class PCA9554Component : public Component, public i2c::I2CDevice { +class PCA9554Component : public Component, + public i2c::I2CDevice, + public gpio_expander::CachedGpioExpander { public: PCA9554Component() = default; /// Check i2c availability and setup masks void setup() override; - /// Poll for input changes periodically + /// Invalidate cache at start of each loop void loop() override; - /// Helper function to read the value of a pin. - bool digital_read(uint8_t pin); - /// Helper function to write the value of a pin. - void digital_write(uint8_t pin, bool value); /// Helper function to set the pin mode of a pin. void pin_mode(uint8_t pin, gpio::Flags flags); @@ -32,9 +31,13 @@ class PCA9554Component : public Component, public i2c::I2CDevice { protected: bool read_inputs_(); - bool write_register_(uint8_t reg, uint16_t value); + // Virtual methods from CachedGpioExpander + bool digital_read_hw(uint8_t pin) override; + bool digital_read_cache(uint8_t pin) override; + void digital_write_hw(uint8_t pin, bool value) override; + /// number of bits the expander has size_t pin_count_{8}; /// width of registers @@ -45,8 +48,6 @@ class PCA9554Component : public Component, public i2c::I2CDevice { uint16_t output_mask_{0x00}; /// The state of the actual input pin states - 1 means HIGH, 0 means LOW uint16_t input_mask_{0x00}; - /// Flags to check if read previously during this loop - uint16_t was_previously_read_ = {0x00}; /// Storage for last I2C error seen esphome::i2c::ErrorCode last_error_; }; From afa191ae41365918a1a085139d5ea6bb144c5652 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 7 Sep 2025 18:26:22 -0500 Subject: [PATCH 179/208] [pcf8574] Migrate to CachedGpioExpander to reduce I2C bus usage (#10573) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/pcf8574/__init__.py | 1 + esphome/components/pcf8574/pcf8574.cpp | 19 ++++++++++++++----- esphome/components/pcf8574/pcf8574.h | 19 +++++++++++++------ 3 files changed, 28 insertions(+), 11 deletions(-) diff --git a/esphome/components/pcf8574/__init__.py b/esphome/components/pcf8574/__init__.py index ff7c314bcd..f387d0a610 100644 --- a/esphome/components/pcf8574/__init__.py +++ b/esphome/components/pcf8574/__init__.py @@ -11,6 +11,7 @@ from esphome.const import ( CONF_OUTPUT, ) +AUTO_LOAD = ["gpio_expander"] DEPENDENCIES = ["i2c"] MULTI_CONF = True diff --git a/esphome/components/pcf8574/pcf8574.cpp b/esphome/components/pcf8574/pcf8574.cpp index 848fbed484..72d8865d7f 100644 --- a/esphome/components/pcf8574/pcf8574.cpp +++ b/esphome/components/pcf8574/pcf8574.cpp @@ -16,6 +16,10 @@ void PCF8574Component::setup() { this->write_gpio_(); this->read_gpio_(); } +void PCF8574Component::loop() { + // Invalidate the cache at the start of each loop + this->reset_pin_cache_(); +} void PCF8574Component::dump_config() { ESP_LOGCONFIG(TAG, "PCF8574:"); LOG_I2C_DEVICE(this) @@ -24,17 +28,19 @@ void PCF8574Component::dump_config() { ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); } } -bool PCF8574Component::digital_read(uint8_t pin) { - this->read_gpio_(); - return this->input_mask_ & (1 << pin); +bool PCF8574Component::digital_read_hw(uint8_t pin) { + // Read all pins from hardware into input_mask_ + return this->read_gpio_(); // Return true if I2C read succeeded, false on error } -void PCF8574Component::digital_write(uint8_t pin, bool value) { + +bool PCF8574Component::digital_read_cache(uint8_t pin) { return this->input_mask_ & (1 << pin); } + +void PCF8574Component::digital_write_hw(uint8_t pin, bool value) { if (value) { this->output_mask_ |= (1 << pin); } else { this->output_mask_ &= ~(1 << pin); } - this->write_gpio_(); } void PCF8574Component::pin_mode(uint8_t pin, gpio::Flags flags) { @@ -91,6 +97,9 @@ bool PCF8574Component::write_gpio_() { } float PCF8574Component::get_setup_priority() const { return setup_priority::IO; } +// Run our loop() method early to invalidate cache before any other components access the pins +float PCF8574Component::get_loop_priority() const { return 9.0f; } // Just after WIFI + void PCF8574GPIOPin::setup() { pin_mode(flags_); } void PCF8574GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } bool PCF8574GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } diff --git a/esphome/components/pcf8574/pcf8574.h b/esphome/components/pcf8574/pcf8574.h index 6edc67fc96..fd1ea8af63 100644 --- a/esphome/components/pcf8574/pcf8574.h +++ b/esphome/components/pcf8574/pcf8574.h @@ -3,11 +3,16 @@ #include "esphome/core/component.h" #include "esphome/core/hal.h" #include "esphome/components/i2c/i2c.h" +#include "esphome/components/gpio_expander/cached_gpio.h" namespace esphome { namespace pcf8574 { -class PCF8574Component : public Component, public i2c::I2CDevice { +// PCF8574(8 pins)/PCF8575(16 pins) always read/write all pins in a single I2C transaction +// so we use uint16_t as bank type to ensure all pins are in one bank and cached together +class PCF8574Component : public Component, + public i2c::I2CDevice, + public gpio_expander::CachedGpioExpander { public: PCF8574Component() = default; @@ -15,20 +20,22 @@ class PCF8574Component : public Component, public i2c::I2CDevice { /// Check i2c availability and setup masks void setup() override; - /// Helper function to read the value of a pin. - bool digital_read(uint8_t pin); - /// Helper function to write the value of a pin. - void digital_write(uint8_t pin, bool value); + /// Invalidate cache at start of each loop + void loop() override; /// Helper function to set the pin mode of a pin. void pin_mode(uint8_t pin, gpio::Flags flags); float get_setup_priority() const override; + float get_loop_priority() const override; void dump_config() override; protected: - bool read_gpio_(); + bool digital_read_hw(uint8_t pin) override; + bool digital_read_cache(uint8_t pin) override; + void digital_write_hw(uint8_t pin, bool value) override; + bool read_gpio_(); bool write_gpio_(); /// Mask for the pin mode - 1 means output, 0 means input From 6e2bcabbc9ae8d654e47d587c5ae47cfe41a4e6f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 7 Sep 2025 18:26:33 -0500 Subject: [PATCH 180/208] [sx1509] Migrate to CachedGpioExpander to reduce I2C bus usage (#10588) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/sx1509/__init__.py | 2 +- esphome/components/sx1509/sx1509.cpp | 19 ++++++++++++------- esphome/components/sx1509/sx1509.h | 15 +++++++++++---- 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/esphome/components/sx1509/__init__.py b/esphome/components/sx1509/__init__.py index 67dc924903..b61b92fd1e 100644 --- a/esphome/components/sx1509/__init__.py +++ b/esphome/components/sx1509/__init__.py @@ -25,7 +25,7 @@ CONF_SCAN_TIME = "scan_time" CONF_DEBOUNCE_TIME = "debounce_time" CONF_SX1509_ID = "sx1509_id" -AUTO_LOAD = ["key_provider"] +AUTO_LOAD = ["key_provider", "gpio_expander"] DEPENDENCIES = ["i2c"] MULTI_CONF = True diff --git a/esphome/components/sx1509/sx1509.cpp b/esphome/components/sx1509/sx1509.cpp index 2bf6701dd2..746ec9cda3 100644 --- a/esphome/components/sx1509/sx1509.cpp +++ b/esphome/components/sx1509/sx1509.cpp @@ -39,6 +39,9 @@ void SX1509Component::dump_config() { } void SX1509Component::loop() { + // Reset cache at the start of each loop + this->reset_pin_cache_(); + if (this->has_keypad_) { if (millis() - this->last_loop_timestamp_ < min_loop_period_) return; @@ -73,18 +76,20 @@ void SX1509Component::loop() { } } -bool SX1509Component::digital_read(uint8_t pin) { +bool SX1509Component::digital_read_hw(uint8_t pin) { + // Always read all pins when any input pin is accessed + return this->read_byte_16(REG_DATA_B, &this->input_mask_); +} + +bool SX1509Component::digital_read_cache(uint8_t pin) { + // Return cached value for input pins, false for output pins if (this->ddr_mask_ & (1 << pin)) { - uint16_t temp_reg_data; - if (!this->read_byte_16(REG_DATA_B, &temp_reg_data)) - return false; - if (temp_reg_data & (1 << pin)) - return true; + return (this->input_mask_ & (1 << pin)) != 0; } return false; } -void SX1509Component::digital_write(uint8_t pin, bool bit_value) { +void SX1509Component::digital_write_hw(uint8_t pin, bool bit_value) { if ((~this->ddr_mask_) & (1 << pin)) { // If the pin is an output, write high/low uint16_t temp_reg_data = 0; diff --git a/esphome/components/sx1509/sx1509.h b/esphome/components/sx1509/sx1509.h index c0e86aa8a1..2afd0d0e4e 100644 --- a/esphome/components/sx1509/sx1509.h +++ b/esphome/components/sx1509/sx1509.h @@ -2,6 +2,7 @@ #include "esphome/components/i2c/i2c.h" #include "esphome/components/key_provider/key_provider.h" +#include "esphome/components/gpio_expander/cached_gpio.h" #include "esphome/core/component.h" #include "esphome/core/hal.h" #include "sx1509_gpio_pin.h" @@ -30,7 +31,10 @@ class SX1509Processor { class SX1509KeyTrigger : public Trigger {}; -class SX1509Component : public Component, public i2c::I2CDevice, public key_provider::KeyProvider { +class SX1509Component : public Component, + public i2c::I2CDevice, + public gpio_expander::CachedGpioExpander, + public key_provider::KeyProvider { public: SX1509Component() = default; @@ -39,11 +43,9 @@ class SX1509Component : public Component, public i2c::I2CDevice, public key_prov float get_setup_priority() const override { return setup_priority::HARDWARE; } void loop() override; - bool digital_read(uint8_t pin); uint16_t read_key_data(); void set_pin_value(uint8_t pin, uint8_t i_on) { this->write_byte(REG_I_ON[pin], i_on); }; void pin_mode(uint8_t pin, gpio::Flags flags); - void digital_write(uint8_t pin, bool bit_value); uint32_t get_clock() { return this->clk_x_; }; void set_rows_cols(uint8_t rows, uint8_t cols) { this->rows_ = rows; @@ -61,10 +63,15 @@ class SX1509Component : public Component, public i2c::I2CDevice, public key_prov void setup_led_driver(uint8_t pin); protected: + // Virtual methods from CachedGpioExpander + bool digital_read_hw(uint8_t pin) override; + bool digital_read_cache(uint8_t pin) override; + void digital_write_hw(uint8_t pin, bool value) override; + uint32_t clk_x_ = 2000000; uint8_t frequency_ = 0; uint16_t ddr_mask_ = 0x00; - uint16_t input_mask_ = 0x00; + uint16_t input_mask_ = 0x00; // Cache for input values (16-bit for all pins) uint16_t port_mask_ = 0x00; uint16_t output_state_ = 0x00; bool has_keypad_ = false; From 0ff08bbc090bd02cab8d3658d9cd0d8b843c656f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 7 Sep 2025 18:26:49 -0500 Subject: [PATCH 181/208] [mcp23016] Migrate to CachedGpioExpander to reduce I2C bus usage (#10581) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/mcp23016/__init__.py | 1 + esphome/components/mcp23016/mcp23016.cpp | 25 +++++++++++++++++++----- esphome/components/mcp23016/mcp23016.h | 14 +++++++++---- 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/esphome/components/mcp23016/__init__.py b/esphome/components/mcp23016/__init__.py index 3333e46c97..5a1f011617 100644 --- a/esphome/components/mcp23016/__init__.py +++ b/esphome/components/mcp23016/__init__.py @@ -11,6 +11,7 @@ from esphome.const import ( CONF_OUTPUT, ) +AUTO_LOAD = ["gpio_expander"] DEPENDENCIES = ["i2c"] MULTI_CONF = True diff --git a/esphome/components/mcp23016/mcp23016.cpp b/esphome/components/mcp23016/mcp23016.cpp index 9d8d6e4dae..be86cb2256 100644 --- a/esphome/components/mcp23016/mcp23016.cpp +++ b/esphome/components/mcp23016/mcp23016.cpp @@ -22,14 +22,29 @@ void MCP23016::setup() { this->write_reg_(MCP23016_IODIR0, 0xFF); this->write_reg_(MCP23016_IODIR1, 0xFF); } -bool MCP23016::digital_read(uint8_t pin) { - uint8_t bit = pin % 8; + +void MCP23016::loop() { + // Invalidate cache at the start of each loop + this->reset_pin_cache_(); +} +bool MCP23016::digital_read_hw(uint8_t pin) { uint8_t reg_addr = pin < 8 ? MCP23016_GP0 : MCP23016_GP1; uint8_t value = 0; - this->read_reg_(reg_addr, &value); - return value & (1 << bit); + if (!this->read_reg_(reg_addr, &value)) { + return false; + } + + // Update the appropriate part of input_mask_ + if (pin < 8) { + this->input_mask_ = (this->input_mask_ & 0xFF00) | value; + } else { + this->input_mask_ = (this->input_mask_ & 0x00FF) | (uint16_t(value) << 8); + } + return true; } -void MCP23016::digital_write(uint8_t pin, bool value) { + +bool MCP23016::digital_read_cache(uint8_t pin) { return this->input_mask_ & (1 << pin); } +void MCP23016::digital_write_hw(uint8_t pin, bool value) { uint8_t reg_addr = pin < 8 ? MCP23016_OLAT0 : MCP23016_OLAT1; this->update_reg_(pin, value, reg_addr); } diff --git a/esphome/components/mcp23016/mcp23016.h b/esphome/components/mcp23016/mcp23016.h index e4ed47a3b2..781c207de0 100644 --- a/esphome/components/mcp23016/mcp23016.h +++ b/esphome/components/mcp23016/mcp23016.h @@ -3,6 +3,7 @@ #include "esphome/core/component.h" #include "esphome/core/hal.h" #include "esphome/components/i2c/i2c.h" +#include "esphome/components/gpio_expander/cached_gpio.h" namespace esphome { namespace mcp23016 { @@ -24,19 +25,22 @@ enum MCP23016GPIORegisters { MCP23016_IOCON1 = 0x0B, }; -class MCP23016 : public Component, public i2c::I2CDevice { +class MCP23016 : public Component, public i2c::I2CDevice, public gpio_expander::CachedGpioExpander { public: MCP23016() = default; void setup() override; - - bool digital_read(uint8_t pin); - void digital_write(uint8_t pin, bool value); + void loop() override; void pin_mode(uint8_t pin, gpio::Flags flags); float get_setup_priority() const override; protected: + // Virtual methods from CachedGpioExpander + bool digital_read_hw(uint8_t pin) override; + bool digital_read_cache(uint8_t pin) override; + void digital_write_hw(uint8_t pin, bool value) override; + // read a given register bool read_reg_(uint8_t reg, uint8_t *value); // write a value to a given register @@ -46,6 +50,8 @@ class MCP23016 : public Component, public i2c::I2CDevice { uint8_t olat_0_{0x00}; uint8_t olat_1_{0x00}; + // Cache for input values (16-bit combined for both banks) + uint16_t input_mask_{0x00}; }; class MCP23016GPIOPin : public GPIOPin { From 166ad942ef74bbf74123c1126064113022eff23c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 7 Sep 2025 18:51:07 -0500 Subject: [PATCH 182/208] [scheduler] Reduce SchedulerItem memory usage by 7.4% on 32-bit platforms (#10553) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/core/scheduler.cpp | 26 +++++++++++++++----------- esphome/core/scheduler.h | 36 +++++++++++++++++++++++++++--------- 2 files changed, 42 insertions(+), 20 deletions(-) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 9a9e61d579..90d19e1ead 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -27,7 +27,6 @@ static constexpr size_t MAX_POOL_SIZE = 5; // Set to 5 to match the pool size - when we have as many cancelled items as our // pool can hold, it's time to clean up and recycle them. static constexpr uint32_t MAX_LOGICALLY_DELETED_ITEMS = 5; - // Half the 32-bit range - used to detect rollovers vs normal time progression static constexpr uint32_t HALF_MAX_UINT32 = std::numeric_limits::max() / 2; // max delay to start an interval sequence @@ -146,12 +145,12 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type // first execution happens immediately after a random smallish offset // Calculate random offset (0 to min(interval/2, 5s)) uint32_t offset = (uint32_t) (std::min(delay / 2, MAX_INTERVAL_DELAY) * random_float()); - item->next_execution_ = now + offset; + item->set_next_execution(now + offset); ESP_LOGV(TAG, "Scheduler interval for %s is %" PRIu32 "ms, offset %" PRIu32 "ms", name_cstr ? name_cstr : "", delay, offset); } else { item->interval = 0; - item->next_execution_ = now + delay; + item->set_next_execution(now + delay); } #ifdef ESPHOME_DEBUG_SCHEDULER @@ -167,7 +166,8 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type name_cstr ? name_cstr : "(null)", type_str, delay); } else { ESP_LOGD(TAG, "set_%s(name='%s/%s', %s=%" PRIu32 ", offset=%" PRIu32 ")", type_str, item->get_source(), - name_cstr ? name_cstr : "(null)", type_str, delay, static_cast(item->next_execution_ - now)); + name_cstr ? name_cstr : "(null)", type_str, delay, + static_cast(item->get_next_execution() - now)); } #endif /* ESPHOME_DEBUG_SCHEDULER */ @@ -312,9 +312,10 @@ optional HOT Scheduler::next_schedule_in(uint32_t now) { auto &item = this->items_[0]; // Convert the fresh timestamp from caller (usually Application::loop()) to 64-bit const auto now_64 = this->millis_64_(now); // 'now' from parameter - fresh from caller - if (item->next_execution_ < now_64) + const uint64_t next_exec = item->get_next_execution(); + if (next_exec < now_64) return 0; - return item->next_execution_ - now_64; + return next_exec - now_64; } void HOT Scheduler::call(uint32_t now) { #ifndef ESPHOME_THREAD_SINGLE @@ -387,7 +388,7 @@ void HOT Scheduler::call(uint32_t now) { bool is_cancelled = is_item_removed_(item.get()); ESP_LOGD(TAG, " %s '%s/%s' interval=%" PRIu32 " next_execution in %" PRIu64 "ms at %" PRIu64 "%s", item->get_type_str(), item->get_source(), name ? name : "(null)", item->interval, - item->next_execution_ - now_64, item->next_execution_, is_cancelled ? " [CANCELLED]" : ""); + item->get_next_execution() - now_64, item->get_next_execution(), is_cancelled ? " [CANCELLED]" : ""); old_items.push_back(std::move(item)); } @@ -439,7 +440,7 @@ void HOT Scheduler::call(uint32_t now) { { // Don't copy-by value yet auto &item = this->items_[0]; - if (item->next_execution_ > now_64) { + if (item->get_next_execution() > now_64) { // Not reached timeout yet, done for this call break; } @@ -478,7 +479,7 @@ void HOT Scheduler::call(uint32_t now) { const char *item_name = item->get_name(); ESP_LOGV(TAG, "Running %s '%s/%s' with interval=%" PRIu32 " next_execution=%" PRIu64 " (now=%" PRIu64 ")", item->get_type_str(), item->get_source(), item_name ? item_name : "(null)", item->interval, - item->next_execution_, now_64); + item->get_next_execution(), now_64); #endif /* ESPHOME_DEBUG_SCHEDULER */ // Warning: During callback(), a lot of stuff can happen, including: @@ -503,7 +504,7 @@ void HOT Scheduler::call(uint32_t now) { } if (item->type == SchedulerItem::INTERVAL) { - item->next_execution_ = now_64 + item->interval; + item->set_next_execution(now_64 + item->interval); // Add new item directly to to_add_ // since we have the lock held this->to_add_.push_back(std::move(item)); @@ -806,7 +807,10 @@ uint64_t Scheduler::millis_64_(uint32_t now) { bool HOT Scheduler::SchedulerItem::cmp(const std::unique_ptr &a, const std::unique_ptr &b) { - return a->next_execution_ > b->next_execution_; + // High bits are almost always equal (change only on 32-bit rollover ~49 days) + // Optimize for common case: check low bits first when high bits are equal + return (a->next_execution_high_ == b->next_execution_high_) ? (a->next_execution_low_ > b->next_execution_low_) + : (a->next_execution_high_ > b->next_execution_high_); } void Scheduler::recycle_item_(std::unique_ptr item) { diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 85cfaab2e0..68ad64b9b1 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -88,19 +88,22 @@ class Scheduler { struct SchedulerItem { // Ordered by size to minimize padding Component *component; - uint32_t interval; - // 64-bit time to handle millis() rollover. The scheduler combines the 32-bit millis() - // with a 16-bit rollover counter to create a 64-bit time that won't roll over for - // billions of years. This ensures correct scheduling even when devices run for months. - uint64_t next_execution_; - // Optimized name storage using tagged union union { const char *static_name; // For string literals (no allocation) char *dynamic_name; // For allocated strings } name_; - + uint32_t interval; + // Split time to handle millis() rollover. The scheduler combines the 32-bit millis() + // with a 16-bit rollover counter to create a 48-bit time space (using 32+16 bits). + // This is intentionally limited to 48 bits, not stored as a full 64-bit value. + // With 49.7 days per 32-bit rollover, the 16-bit counter supports + // 49.7 days × 65536 = ~8900 years. This ensures correct scheduling + // even when devices run for months. Split into two fields for better memory + // alignment on 32-bit systems. + uint32_t next_execution_low_; // Lower 32 bits of execution time (millis value) std::function callback; + uint16_t next_execution_high_; // Upper 16 bits (millis_major counter) #ifdef ESPHOME_THREAD_MULTI_ATOMICS // Multi-threaded with atomics: use atomic for lock-free access @@ -126,7 +129,8 @@ class Scheduler { SchedulerItem() : component(nullptr), interval(0), - next_execution_(0), + next_execution_low_(0), + next_execution_high_(0), #ifdef ESPHOME_THREAD_MULTI_ATOMICS // remove is initialized in the member declaration as std::atomic{false} type(TIMEOUT), @@ -185,7 +189,21 @@ class Scheduler { } static bool cmp(const std::unique_ptr &a, const std::unique_ptr &b); - const char *get_type_str() const { return (type == TIMEOUT) ? "timeout" : "interval"; } + + // Note: We use 48 bits total (32 + 16), stored in a 64-bit value for API compatibility. + // The upper 16 bits of the 64-bit value are always zero, which is fine since + // millis_major_ is also 16 bits and they must match. + constexpr uint64_t get_next_execution() const { + return (static_cast(next_execution_high_) << 32) | next_execution_low_; + } + + constexpr void set_next_execution(uint64_t value) { + next_execution_low_ = static_cast(value); + // Cast to uint16_t intentionally truncates to lower 16 bits of the upper 32 bits. + // This is correct because millis_major_ that creates these values is also 16 bits. + next_execution_high_ = static_cast(value >> 32); + } + constexpr const char *get_type_str() const { return (type == TIMEOUT) ? "timeout" : "interval"; } const char *get_source() const { return component ? component->get_component_source() : "unknown"; } }; From 7eaaa4e4266a22b15a1456b0ac59e8c0411ab3bb Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 8 Sep 2025 10:56:34 +1000 Subject: [PATCH 183/208] [mipi_rgb] Unified driver for MIPI RGB displays (#9892) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/mipi/__init__.py | 106 +++-- esphome/components/mipi_rgb/__init__.py | 2 + esphome/components/mipi_rgb/display.py | 321 +++++++++++++++ esphome/components/mipi_rgb/mipi_rgb.cpp | 388 ++++++++++++++++++ esphome/components/mipi_rgb/mipi_rgb.h | 127 ++++++ esphome/components/mipi_rgb/models/guition.py | 24 ++ esphome/components/mipi_rgb/models/lilygo.py | 228 ++++++++++ esphome/components/mipi_rgb/models/rpi.py | 9 + esphome/components/mipi_rgb/models/st7701s.py | 214 ++++++++++ .../components/mipi_rgb/models/waveshare.py | 64 +++ .../mipi_rgb/test.esp32-s3-idf.yaml | 67 +++ 12 files changed, 1520 insertions(+), 31 deletions(-) create mode 100644 esphome/components/mipi_rgb/__init__.py create mode 100644 esphome/components/mipi_rgb/display.py create mode 100644 esphome/components/mipi_rgb/mipi_rgb.cpp create mode 100644 esphome/components/mipi_rgb/mipi_rgb.h create mode 100644 esphome/components/mipi_rgb/models/guition.py create mode 100644 esphome/components/mipi_rgb/models/rpi.py create mode 100644 esphome/components/mipi_rgb/models/st7701s.py create mode 100644 esphome/components/mipi_rgb/models/waveshare.py create mode 100644 tests/components/mipi_rgb/test.esp32-s3-idf.yaml diff --git a/CODEOWNERS b/CODEOWNERS index c34774fce6..3f963bf960 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -299,6 +299,7 @@ esphome/components/mics_4514/* @jesserockz esphome/components/midea/* @dudanov esphome/components/midea_ir/* @dudanov esphome/components/mipi_dsi/* @clydebarrow +esphome/components/mipi_rgb/* @clydebarrow esphome/components/mipi_spi/* @clydebarrow esphome/components/mitsubishi/* @RubyBailey esphome/components/mixer/speaker/* @kahrendt diff --git a/esphome/components/mipi/__init__.py b/esphome/components/mipi/__init__.py index a9ecb9d79a..8b1ca899df 100644 --- a/esphome/components/mipi/__init__.py +++ b/esphome/components/mipi/__init__.py @@ -222,6 +222,12 @@ def delay(ms): class DriverChip: + """ + A class representing a MIPI DBI driver chip model. + The parameters supplied as defaults will be used to provide default values for the display configuration. + Setting swap_xy to cv.UNDEFINED will indicate that the model does not support swapping X and Y axes. + """ + models: dict[str, Self] = {} def __init__( @@ -232,7 +238,7 @@ class DriverChip: ): name = name.upper() self.name = name - self.initsequence = initsequence or defaults.get("init_sequence") + self.initsequence = initsequence self.defaults = defaults DriverChip.models[name] = self @@ -246,6 +252,17 @@ class DriverChip: return models def extend(self, name, **kwargs) -> "DriverChip": + """ + Extend the current model with additional parameters or a modified init sequence. + Parameters supplied here will override the defaults of the current model. + if the initsequence is not provided, the current model's initsequence will be used. + If add_init_sequence is provided, it will be appended to the current initsequence. + :param name: + :param kwargs: + :return: + """ + initsequence = list(kwargs.pop("initsequence", self.initsequence)) + initsequence.extend(kwargs.pop("add_init_sequence", ())) defaults = self.defaults.copy() if ( CONF_WIDTH in defaults @@ -260,23 +277,39 @@ class DriverChip: ): defaults[CONF_NATIVE_HEIGHT] = defaults[CONF_HEIGHT] defaults.update(kwargs) - return DriverChip(name, initsequence=self.initsequence, **defaults) + return self.__class__(name, initsequence=tuple(initsequence), **defaults) def get_default(self, key, fallback: Any = False) -> Any: return self.defaults.get(key, fallback) + @property + def transforms(self) -> set[str]: + """ + Return the available transforms for this model. + """ + if self.get_default("no_transform", False): + return set() + if self.get_default(CONF_SWAP_XY) != cv.UNDEFINED: + return {CONF_MIRROR_X, CONF_MIRROR_Y, CONF_SWAP_XY} + return {CONF_MIRROR_X, CONF_MIRROR_Y} + def option(self, name, fallback=False) -> cv.Optional: return cv.Optional(name, default=self.get_default(name, fallback)) def rotation_as_transform(self, config) -> bool: """ Check if a rotation can be implemented in hardware using the MADCTL register. - A rotation of 180 is always possible, 90 and 270 are possible if the model supports swapping X and Y. + A rotation of 180 is always possible if x and y mirroring are supported, 90 and 270 are possible if the model supports swapping X and Y. """ + transforms = self.transforms rotation = config.get(CONF_ROTATION, 0) - return rotation and ( - self.get_default(CONF_SWAP_XY) != cv.UNDEFINED or rotation == 180 - ) + if rotation == 0 or not transforms: + return False + if rotation == 180: + return CONF_MIRROR_X in transforms and CONF_MIRROR_Y in transforms + if rotation == 90: + return CONF_SWAP_XY in transforms and CONF_MIRROR_X in transforms + return CONF_SWAP_XY in transforms and CONF_MIRROR_Y in transforms def get_dimensions(self, config) -> tuple[int, int, int, int]: if CONF_DIMENSIONS in config: @@ -301,10 +334,10 @@ class DriverChip: # if mirroring axes and there are offsets, also mirror the offsets to cater for situations where # the offset is asymmetric - if transform[CONF_MIRROR_X]: + if transform.get(CONF_MIRROR_X): native_width = self.get_default(CONF_NATIVE_WIDTH, width + offset_width * 2) offset_width = native_width - width - offset_width - if transform[CONF_MIRROR_Y]: + if transform.get(CONF_MIRROR_Y): native_height = self.get_default( CONF_NATIVE_HEIGHT, height + offset_height * 2 ) @@ -314,7 +347,7 @@ class DriverChip: 90, 270, ) - if transform[CONF_SWAP_XY] is True or rotated: + if transform.get(CONF_SWAP_XY) is True or rotated: width, height = height, width offset_height, offset_width = offset_width, offset_height return width, height, offset_width, offset_height @@ -324,27 +357,50 @@ class DriverChip: transform = config.get( CONF_TRANSFORM, { - CONF_MIRROR_X: self.get_default(CONF_MIRROR_X, False), - CONF_MIRROR_Y: self.get_default(CONF_MIRROR_Y, False), - CONF_SWAP_XY: self.get_default(CONF_SWAP_XY, False), + CONF_MIRROR_X: self.get_default(CONF_MIRROR_X), + CONF_MIRROR_Y: self.get_default(CONF_MIRROR_Y), + CONF_SWAP_XY: self.get_default(CONF_SWAP_XY), }, ) + # fill in defaults if not provided + mirror_x = transform.get(CONF_MIRROR_X, self.get_default(CONF_MIRROR_X)) + mirror_y = transform.get(CONF_MIRROR_Y, self.get_default(CONF_MIRROR_Y)) + swap_xy = transform.get(CONF_SWAP_XY, self.get_default(CONF_SWAP_XY)) + transform[CONF_MIRROR_X] = mirror_x + transform[CONF_MIRROR_Y] = mirror_y + transform[CONF_SWAP_XY] = swap_xy # Can we use the MADCTL register to set the rotation? if can_transform and CONF_TRANSFORM not in config: rotation = config[CONF_ROTATION] if rotation == 180: - transform[CONF_MIRROR_X] = not transform[CONF_MIRROR_X] - transform[CONF_MIRROR_Y] = not transform[CONF_MIRROR_Y] + transform[CONF_MIRROR_X] = not mirror_x + transform[CONF_MIRROR_Y] = not mirror_y elif rotation == 90: - transform[CONF_SWAP_XY] = not transform[CONF_SWAP_XY] - transform[CONF_MIRROR_X] = not transform[CONF_MIRROR_X] + transform[CONF_SWAP_XY] = not swap_xy + transform[CONF_MIRROR_X] = not mirror_x else: - transform[CONF_SWAP_XY] = not transform[CONF_SWAP_XY] - transform[CONF_MIRROR_Y] = not transform[CONF_MIRROR_Y] + transform[CONF_SWAP_XY] = not swap_xy + transform[CONF_MIRROR_Y] = not mirror_y transform[CONF_TRANSFORM] = True return transform + def add_madctl(self, sequence: list, config: dict): + # Add the MADCTL command to the sequence based on the configuration. + use_flip = config.get(CONF_USE_AXIS_FLIPS) + madctl = 0 + transform = self.get_transform(config) + if transform[CONF_MIRROR_X]: + madctl |= MADCTL_XFLIP if use_flip else MADCTL_MX + if transform[CONF_MIRROR_Y]: + madctl |= MADCTL_YFLIP if use_flip else MADCTL_MY + if transform.get(CONF_SWAP_XY) is True: # Exclude Undefined + madctl |= MADCTL_MV + if config[CONF_COLOR_ORDER] == MODE_BGR: + madctl |= MADCTL_BGR + sequence.append((MADCTL, madctl)) + return madctl + def get_sequence(self, config) -> tuple[tuple[int, ...], int]: """ Create the init sequence for the display. @@ -367,21 +423,9 @@ class DriverChip: pixel_mode = PIXEL_MODES[pixel_mode] sequence.append((PIXFMT, pixel_mode)) - # Does the chip use the flipping bits for mirroring rather than the reverse order bits? - use_flip = config.get(CONF_USE_AXIS_FLIPS) - madctl = 0 - transform = self.get_transform(config) if self.rotation_as_transform(config): LOGGER.info("Using hardware transform to implement rotation") - if transform.get(CONF_MIRROR_X): - madctl |= MADCTL_XFLIP if use_flip else MADCTL_MX - if transform.get(CONF_MIRROR_Y): - madctl |= MADCTL_YFLIP if use_flip else MADCTL_MY - if transform.get(CONF_SWAP_XY) is True: # Exclude Undefined - madctl |= MADCTL_MV - if config[CONF_COLOR_ORDER] == MODE_BGR: - madctl |= MADCTL_BGR - sequence.append((MADCTL, madctl)) + madctl = self.add_madctl(sequence, config) if config[CONF_INVERT_COLORS]: sequence.append((INVON,)) else: diff --git a/esphome/components/mipi_rgb/__init__.py b/esphome/components/mipi_rgb/__init__.py new file mode 100644 index 0000000000..4f9972c6e0 --- /dev/null +++ b/esphome/components/mipi_rgb/__init__.py @@ -0,0 +1,2 @@ +CODEOWNERS = ["@clydebarrow"] +DOMAIN = "mipi_rgb" diff --git a/esphome/components/mipi_rgb/display.py b/esphome/components/mipi_rgb/display.py new file mode 100644 index 0000000000..3001d33980 --- /dev/null +++ b/esphome/components/mipi_rgb/display.py @@ -0,0 +1,321 @@ +import importlib +import pkgutil + +from esphome import pins +import esphome.codegen as cg +from esphome.components import display, spi +from esphome.components.const import ( + BYTE_ORDER_BIG, + BYTE_ORDER_LITTLE, + CONF_BYTE_ORDER, + CONF_DRAW_ROUNDING, +) +from esphome.components.display import CONF_SHOW_TEST_CARD +from esphome.components.esp32 import const, only_on_variant +from esphome.components.mipi import ( + COLOR_ORDERS, + CONF_DE_PIN, + CONF_HSYNC_BACK_PORCH, + CONF_HSYNC_FRONT_PORCH, + CONF_HSYNC_PULSE_WIDTH, + CONF_PCLK_PIN, + CONF_PIXEL_MODE, + CONF_USE_AXIS_FLIPS, + CONF_VSYNC_BACK_PORCH, + CONF_VSYNC_FRONT_PORCH, + CONF_VSYNC_PULSE_WIDTH, + MODE_BGR, + PIXEL_MODE_16BIT, + PIXEL_MODE_18BIT, + DriverChip, + dimension_schema, + map_sequence, + power_of_two, + requires_buffer, +) +from esphome.components.rpi_dpi_rgb.display import ( + CONF_PCLK_FREQUENCY, + CONF_PCLK_INVERTED, +) +import esphome.config_validation as cv +from esphome.const import ( + CONF_BLUE, + CONF_COLOR_ORDER, + CONF_CS_PIN, + CONF_DATA_PINS, + CONF_DATA_RATE, + CONF_DC_PIN, + CONF_DIMENSIONS, + CONF_ENABLE_PIN, + CONF_GREEN, + CONF_HSYNC_PIN, + CONF_ID, + CONF_IGNORE_STRAPPING_WARNING, + CONF_INIT_SEQUENCE, + CONF_INVERT_COLORS, + CONF_LAMBDA, + CONF_MIRROR_X, + CONF_MIRROR_Y, + CONF_MODEL, + CONF_NUMBER, + CONF_RED, + CONF_RESET_PIN, + CONF_ROTATION, + CONF_SPI_ID, + CONF_SWAP_XY, + CONF_TRANSFORM, + CONF_VSYNC_PIN, + CONF_WIDTH, +) +from esphome.final_validate import full_config + +from ..spi import CONF_SPI_MODE, SPI_DATA_RATE_SCHEMA, SPI_MODE_OPTIONS, SPIComponent +from . import models + +DEPENDENCIES = ["esp32", "psram"] + +mipi_rgb_ns = cg.esphome_ns.namespace("mipi_rgb") +mipi_rgb = mipi_rgb_ns.class_("MipiRgb", display.Display, cg.Component) +mipi_rgb_spi = mipi_rgb_ns.class_( + "MipiRgbSpi", mipi_rgb, display.Display, cg.Component, spi.SPIDevice +) +ColorOrder = display.display_ns.enum("ColorMode") + +DATA_PIN_SCHEMA = pins.internal_gpio_output_pin_schema + +DriverChip("CUSTOM") + +# Import all models dynamically from the models package + +for module_info in pkgutil.iter_modules(models.__path__): + importlib.import_module(f".models.{module_info.name}", package=__package__) + +MODELS = DriverChip.get_models() + + +def data_pin_validate(value): + """ + It is safe to use strapping pins as RGB output data bits, as they are outputs only, + and not initialised until after boot. + """ + if not isinstance(value, dict): + try: + return DATA_PIN_SCHEMA( + {CONF_NUMBER: value, CONF_IGNORE_STRAPPING_WARNING: True} + ) + except cv.Invalid: + pass + return DATA_PIN_SCHEMA(value) + + +def data_pin_set(length): + return cv.All( + [data_pin_validate], + cv.Length(min=length, max=length, msg=f"Exactly {length} data pins required"), + ) + + +def model_schema(config): + model = MODELS[config[CONF_MODEL].upper()] + if transforms := model.transforms: + transform = cv.Schema({cv.Required(x): cv.boolean for x in transforms}) + for x in (CONF_SWAP_XY, CONF_MIRROR_X, CONF_MIRROR_Y): + if x not in transforms: + transform = transform.extend( + {cv.Optional(x): cv.invalid(f"{x} not supported by this model")} + ) + else: + transform = cv.invalid("This model does not support transforms") + + # RPI model does not use an init sequence, indicates with empty list + if model.initsequence is None: + # Custom model requires an init sequence + iseqconf = cv.Required(CONF_INIT_SEQUENCE) + uses_spi = True + else: + iseqconf = cv.Optional(CONF_INIT_SEQUENCE) + uses_spi = CONF_INIT_SEQUENCE in config or len(model.initsequence) != 0 + swap_xy = config.get(CONF_TRANSFORM, {}).get(CONF_SWAP_XY, False) + + # Dimensions are optional if the model has a default width and the swap_xy transform is not overridden + cv_dimensions = ( + cv.Optional if model.get_default(CONF_WIDTH) and not swap_xy else cv.Required + ) + pixel_modes = (PIXEL_MODE_16BIT, PIXEL_MODE_18BIT, "16", "18") + schema = display.FULL_DISPLAY_SCHEMA.extend( + { + model.option(CONF_RESET_PIN, cv.UNDEFINED): pins.gpio_output_pin_schema, + cv.GenerateID(): cv.declare_id(mipi_rgb_spi if uses_spi else mipi_rgb), + cv_dimensions(CONF_DIMENSIONS): dimension_schema( + model.get_default(CONF_DRAW_ROUNDING, 1) + ), + model.option(CONF_ENABLE_PIN, cv.UNDEFINED): cv.ensure_list( + pins.gpio_output_pin_schema + ), + model.option(CONF_COLOR_ORDER, MODE_BGR): cv.enum(COLOR_ORDERS, upper=True), + model.option(CONF_DRAW_ROUNDING, 2): power_of_two, + model.option(CONF_PIXEL_MODE, PIXEL_MODE_16BIT): cv.one_of( + *pixel_modes, lower=True + ), + model.option(CONF_TRANSFORM, cv.UNDEFINED): transform, + cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True), + model.option(CONF_INVERT_COLORS, False): cv.boolean, + model.option(CONF_USE_AXIS_FLIPS, True): cv.boolean, + model.option(CONF_PCLK_FREQUENCY, "40MHz"): cv.All( + cv.frequency, cv.Range(min=4e6, max=100e6) + ), + model.option(CONF_PCLK_INVERTED, True): cv.boolean, + iseqconf: cv.ensure_list(map_sequence), + model.option(CONF_BYTE_ORDER, BYTE_ORDER_BIG): cv.one_of( + BYTE_ORDER_LITTLE, BYTE_ORDER_BIG, lower=True + ), + model.option(CONF_HSYNC_PULSE_WIDTH): cv.int_, + model.option(CONF_HSYNC_BACK_PORCH): cv.int_, + model.option(CONF_HSYNC_FRONT_PORCH): cv.int_, + model.option(CONF_VSYNC_PULSE_WIDTH): cv.int_, + model.option(CONF_VSYNC_BACK_PORCH): cv.int_, + model.option(CONF_VSYNC_FRONT_PORCH): cv.int_, + model.option(CONF_DATA_PINS): cv.Any( + data_pin_set(16), + cv.Schema( + { + cv.Required(CONF_RED): data_pin_set(5), + cv.Required(CONF_GREEN): data_pin_set(6), + cv.Required(CONF_BLUE): data_pin_set(5), + } + ), + ), + model.option( + CONF_DE_PIN, cv.UNDEFINED + ): pins.internal_gpio_output_pin_schema, + model.option(CONF_PCLK_PIN): pins.internal_gpio_output_pin_schema, + model.option(CONF_HSYNC_PIN): pins.internal_gpio_output_pin_schema, + model.option(CONF_VSYNC_PIN): pins.internal_gpio_output_pin_schema, + model.option(CONF_RESET_PIN, cv.UNDEFINED): pins.gpio_output_pin_schema, + } + ) + if uses_spi: + schema = schema.extend( + { + cv.GenerateID(CONF_SPI_ID): cv.use_id(SPIComponent), + model.option(CONF_DC_PIN, cv.UNDEFINED): pins.gpio_output_pin_schema, + model.option(CONF_DATA_RATE, "1MHz"): SPI_DATA_RATE_SCHEMA, + model.option(CONF_SPI_MODE, "MODE0"): cv.enum( + SPI_MODE_OPTIONS, upper=True + ), + model.option(CONF_CS_PIN, cv.UNDEFINED): pins.gpio_output_pin_schema, + } + ) + return schema + + +def _config_schema(config): + config = cv.Schema( + { + cv.Required(CONF_MODEL): cv.one_of(*MODELS, upper=True), + }, + extra=cv.ALLOW_EXTRA, + )(config) + schema = model_schema(config) + return cv.All( + schema, + only_on_variant(supported=[const.VARIANT_ESP32S3]), + cv.only_with_esp_idf, + )(config) + + +CONFIG_SCHEMA = _config_schema + + +def _final_validate(config): + global_config = full_config.get() + + from esphome.components.lvgl import DOMAIN as LVGL_DOMAIN + + if not requires_buffer(config) and LVGL_DOMAIN not in global_config: + # If no drawing methods are configured, and LVGL is not enabled, show a test card + config[CONF_SHOW_TEST_CARD] = True + if CONF_SPI_ID in config: + config = spi.final_validate_device_schema( + "mipi_rgb", require_miso=False, require_mosi=True + )(config) + return config + + +FINAL_VALIDATE_SCHEMA = _final_validate + + +async def to_code(config): + model = MODELS[config[CONF_MODEL].upper()] + width, height, _offset_width, _offset_height = model.get_dimensions(config) + var = cg.new_Pvariable(config[CONF_ID], width, height) + cg.add(var.set_model(model.name)) + if enable_pin := config.get(CONF_ENABLE_PIN): + enable = [await cg.gpio_pin_expression(pin) for pin in enable_pin] + cg.add(var.set_enable_pins(enable)) + + if CONF_SPI_ID in config: + await spi.register_spi_device(var, config) + sequence, madctl = model.get_sequence(config) + cg.add(var.set_init_sequence(sequence)) + cg.add(var.set_madctl(madctl)) + + cg.add(var.set_color_mode(COLOR_ORDERS[config[CONF_COLOR_ORDER]])) + cg.add(var.set_invert_colors(config[CONF_INVERT_COLORS])) + cg.add(var.set_hsync_pulse_width(config[CONF_HSYNC_PULSE_WIDTH])) + cg.add(var.set_hsync_back_porch(config[CONF_HSYNC_BACK_PORCH])) + cg.add(var.set_hsync_front_porch(config[CONF_HSYNC_FRONT_PORCH])) + cg.add(var.set_vsync_pulse_width(config[CONF_VSYNC_PULSE_WIDTH])) + cg.add(var.set_vsync_back_porch(config[CONF_VSYNC_BACK_PORCH])) + cg.add(var.set_vsync_front_porch(config[CONF_VSYNC_FRONT_PORCH])) + cg.add(var.set_pclk_inverted(config[CONF_PCLK_INVERTED])) + cg.add(var.set_pclk_frequency(config[CONF_PCLK_FREQUENCY])) + index = 0 + dpins = [] + if CONF_RED in config[CONF_DATA_PINS]: + red_pins = config[CONF_DATA_PINS][CONF_RED] + green_pins = config[CONF_DATA_PINS][CONF_GREEN] + blue_pins = config[CONF_DATA_PINS][CONF_BLUE] + if config[CONF_COLOR_ORDER] == "BGR": + dpins.extend(red_pins) + dpins.extend(green_pins) + dpins.extend(blue_pins) + else: + dpins.extend(blue_pins) + dpins.extend(green_pins) + dpins.extend(red_pins) + # swap bytes to match big-endian format + dpins = dpins[8:16] + dpins[0:8] + else: + dpins = config[CONF_DATA_PINS] + for index, pin in enumerate(dpins): + data_pin = await cg.gpio_pin_expression(pin) + cg.add(var.add_data_pin(data_pin, index)) + + if dc_pin := config.get(CONF_DC_PIN): + dc = await cg.gpio_pin_expression(dc_pin) + cg.add(var.set_dc_pin(dc)) + + if reset_pin := config.get(CONF_RESET_PIN): + reset = await cg.gpio_pin_expression(reset_pin) + cg.add(var.set_reset_pin(reset)) + + if model.rotation_as_transform(config): + config[CONF_ROTATION] = 0 + + if de_pin := config.get(CONF_DE_PIN): + pin = await cg.gpio_pin_expression(de_pin) + cg.add(var.set_de_pin(pin)) + pin = await cg.gpio_pin_expression(config[CONF_PCLK_PIN]) + cg.add(var.set_pclk_pin(pin)) + pin = await cg.gpio_pin_expression(config[CONF_HSYNC_PIN]) + cg.add(var.set_hsync_pin(pin)) + pin = await cg.gpio_pin_expression(config[CONF_VSYNC_PIN]) + cg.add(var.set_vsync_pin(pin)) + + await display.register_display(var, config) + if lamb := config.get(CONF_LAMBDA): + lambda_ = await cg.process_lambda( + lamb, [(display.DisplayRef, "it")], return_type=cg.void + ) + cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/mipi_rgb/mipi_rgb.cpp b/esphome/components/mipi_rgb/mipi_rgb.cpp new file mode 100644 index 0000000000..00c9c8cbff --- /dev/null +++ b/esphome/components/mipi_rgb/mipi_rgb.cpp @@ -0,0 +1,388 @@ +#ifdef USE_ESP32_VARIANT_ESP32S3 +#include "mipi_rgb.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" +#include "esp_lcd_panel_rgb.h" + +namespace esphome { +namespace mipi_rgb { + +static const uint8_t DELAY_FLAG = 0xFF; +static constexpr uint8_t MADCTL_MY = 0x80; // Bit 7 Bottom to top +static constexpr uint8_t MADCTL_MX = 0x40; // Bit 6 Right to left +static constexpr uint8_t MADCTL_MV = 0x20; // Bit 5 Swap axes +static constexpr uint8_t MADCTL_ML = 0x10; // Bit 4 Refresh bottom to top +static constexpr uint8_t MADCTL_BGR = 0x08; // Bit 3 Blue-Green-Red pixel order +static constexpr uint8_t MADCTL_XFLIP = 0x02; // Mirror the display horizontally +static constexpr uint8_t MADCTL_YFLIP = 0x01; // Mirror the display vertically + +void MipiRgb::setup_enables_() { + if (!this->enable_pins_.empty()) { + for (auto *pin : this->enable_pins_) { + pin->setup(); + pin->digital_write(true); + } + delay(10); + } + if (this->reset_pin_ != nullptr) { + this->reset_pin_->setup(); + this->reset_pin_->digital_write(true); + delay(5); + this->reset_pin_->digital_write(false); + delay(5); + this->reset_pin_->digital_write(true); + } +} + +#ifdef USE_SPI +void MipiRgbSpi::setup() { + this->setup_enables_(); + this->spi_setup(); + this->write_init_sequence_(); + this->common_setup_(); +} +void MipiRgbSpi::write_command_(uint8_t value) { + this->enable(); + if (this->dc_pin_ == nullptr) { + this->write(value, 9); + } else { + this->dc_pin_->digital_write(false); + this->write_byte(value); + this->dc_pin_->digital_write(true); + } + this->disable(); +} + +void MipiRgbSpi::write_data_(uint8_t value) { + this->enable(); + if (this->dc_pin_ == nullptr) { + this->write(value | 0x100, 9); + } else { + this->dc_pin_->digital_write(true); + this->write_byte(value); + } + this->disable(); +} + +/** + * this relies upon the init sequence being well-formed, which is guaranteed by the Python init code. + */ + +void MipiRgbSpi::write_init_sequence_() { + size_t index = 0; + auto &vec = this->init_sequence_; + while (index != vec.size()) { + if (vec.size() - index < 2) { + this->mark_failed("Malformed init sequence"); + return; + } + uint8_t cmd = vec[index++]; + uint8_t x = vec[index++]; + if (x == DELAY_FLAG) { + ESP_LOGD(TAG, "Delay %dms", cmd); + delay(cmd); + } else { + uint8_t num_args = x & 0x7F; + if (vec.size() - index < num_args) { + this->mark_failed("Malformed init sequence"); + return; + } + if (cmd == SLEEP_OUT) { + delay(120); // NOLINT + } + const auto *ptr = vec.data() + index; + ESP_LOGD(TAG, "Write command %02X, length %d, byte(s) %s", cmd, num_args, + format_hex_pretty(ptr, num_args, '.', false).c_str()); + index += num_args; + this->write_command_(cmd); + while (num_args-- != 0) + this->write_data_(*ptr++); + if (cmd == SLEEP_OUT) + delay(10); + } + } + // this->spi_teardown(); // SPI not needed after this + this->init_sequence_.clear(); + delay(10); +} + +void MipiRgbSpi::dump_config() { + MipiRgb::dump_config(); + LOG_PIN(" CS Pin: ", this->cs_); + LOG_PIN(" DC Pin: ", this->dc_pin_); + ESP_LOGCONFIG(TAG, + " SPI Data rate: %uMHz" + "\n Mirror X: %s" + "\n Mirror Y: %s" + "\n Swap X/Y: %s" + "\n Color Order: %s", + (unsigned) (this->data_rate_ / 1000000), YESNO(this->madctl_ & (MADCTL_XFLIP | MADCTL_MX)), + YESNO(this->madctl_ & (MADCTL_YFLIP | MADCTL_MY | MADCTL_ML)), YESNO(this->madctl_ & MADCTL_MV), + this->madctl_ & MADCTL_BGR ? "BGR" : "RGB"); +} + +#endif // USE_SPI + +void MipiRgb::setup() { + this->setup_enables_(); + this->common_setup_(); +} + +void MipiRgb::common_setup_() { + esp_lcd_rgb_panel_config_t config{}; + config.flags.fb_in_psram = 1; + config.bounce_buffer_size_px = this->width_ * 10; + config.num_fbs = 1; + config.timings.h_res = this->width_; + config.timings.v_res = this->height_; + config.timings.hsync_pulse_width = this->hsync_pulse_width_; + config.timings.hsync_back_porch = this->hsync_back_porch_; + config.timings.hsync_front_porch = this->hsync_front_porch_; + config.timings.vsync_pulse_width = this->vsync_pulse_width_; + config.timings.vsync_back_porch = this->vsync_back_porch_; + config.timings.vsync_front_porch = this->vsync_front_porch_; + config.timings.flags.pclk_active_neg = this->pclk_inverted_; + config.timings.pclk_hz = this->pclk_frequency_; + config.clk_src = LCD_CLK_SRC_PLL160M; + size_t data_pin_count = sizeof(this->data_pins_) / sizeof(this->data_pins_[0]); + for (size_t i = 0; i != data_pin_count; i++) { + config.data_gpio_nums[i] = this->data_pins_[i]->get_pin(); + } + config.data_width = data_pin_count; + config.disp_gpio_num = -1; + config.hsync_gpio_num = this->hsync_pin_->get_pin(); + config.vsync_gpio_num = this->vsync_pin_->get_pin(); + if (this->de_pin_) { + config.de_gpio_num = this->de_pin_->get_pin(); + } else { + config.de_gpio_num = -1; + } + config.pclk_gpio_num = this->pclk_pin_->get_pin(); + esp_err_t err = esp_lcd_new_rgb_panel(&config, &this->handle_); + if (err == ESP_OK) + err = esp_lcd_panel_reset(this->handle_); + if (err == ESP_OK) + err = esp_lcd_panel_init(this->handle_); + if (err != ESP_OK) { + auto msg = str_sprintf("lcd setup failed: %s", esp_err_to_name(err)); + this->mark_failed(msg.c_str()); + } + ESP_LOGCONFIG(TAG, "MipiRgb setup complete"); +} + +void MipiRgb::loop() { + if (this->handle_ != nullptr) + esp_lcd_rgb_panel_restart(this->handle_); +} + +void MipiRgb::update() { + if (this->is_failed()) + return; + if (this->auto_clear_enabled_) { + this->clear(); + } + if (this->show_test_card_) { + this->test_card(); + } else if (this->page_ != nullptr) { + this->page_->get_writer()(*this); + } else if (this->writer_.has_value()) { + (*this->writer_)(*this); + } else { + this->stop_poller(); + } + if (this->buffer_ == nullptr || this->x_low_ > this->x_high_ || this->y_low_ > this->y_high_) + return; + ESP_LOGV(TAG, "x_low %d, y_low %d, x_high %d, y_high %d", this->x_low_, this->y_low_, this->x_high_, this->y_high_); + int w = this->x_high_ - this->x_low_ + 1; + int h = this->y_high_ - this->y_low_ + 1; + this->write_to_display_(this->x_low_, this->y_low_, w, h, reinterpret_cast(this->buffer_), + this->x_low_, this->y_low_, this->width_ - w - this->x_low_); + // invalidate watermarks + this->x_low_ = this->width_; + this->y_low_ = this->height_; + this->x_high_ = 0; + this->y_high_ = 0; +} + +void MipiRgb::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order, + display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) { + if (w <= 0 || h <= 0 || this->is_failed()) + return; + // if color mapping is required, pass the buck. + // note that endianness is not considered here - it is assumed to match! + if (bitness != display::COLOR_BITNESS_565) { + Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset, x_pad); + this->write_to_display_(x_start, y_start, w, h, reinterpret_cast(this->buffer_), x_start, y_start, + this->width_ - w - x_start); + } else { + this->write_to_display_(x_start, y_start, w, h, ptr, x_offset, y_offset, x_pad); + } +} + +void MipiRgb::write_to_display_(int x_start, int y_start, int w, int h, const uint8_t *ptr, int x_offset, int y_offset, + int x_pad) { + esp_err_t err = ESP_OK; + auto stride = (x_offset + w + x_pad) * 2; + ptr += y_offset * stride + x_offset * 2; // skip to the first pixel + // x_ and y_offset are offsets into the source buffer, unrelated to our own offsets into the display. + if (x_offset == 0 && x_pad == 0) { + err = esp_lcd_panel_draw_bitmap(this->handle_, x_start, y_start, x_start + w, y_start + h, ptr); + } else { + // draw line by line + for (int y = 0; y != h; y++) { + err = esp_lcd_panel_draw_bitmap(this->handle_, x_start, y + y_start, x_start + w, y + y_start + 1, ptr); + if (err != ESP_OK) + break; + ptr += stride; // next line + } + } + if (err != ESP_OK) + ESP_LOGE(TAG, "lcd_lcd_panel_draw_bitmap failed: %s", esp_err_to_name(err)); +} + +bool MipiRgb::check_buffer_() { + if (this->is_failed()) + return false; + if (this->buffer_ != nullptr) + return true; + // this is dependent on the enum values. + RAMAllocator allocator; + this->buffer_ = allocator.allocate(this->height_ * this->width_); + if (this->buffer_ == nullptr) { + this->mark_failed("Could not allocate buffer for display!"); + return false; + } + return true; +} + +void MipiRgb::draw_pixel_at(int x, int y, Color color) { + if (!this->get_clipping().inside(x, y) || this->is_failed()) + return; + + switch (this->rotation_) { + case display::DISPLAY_ROTATION_0_DEGREES: + break; + case display::DISPLAY_ROTATION_90_DEGREES: + std::swap(x, y); + x = this->width_ - x - 1; + break; + case display::DISPLAY_ROTATION_180_DEGREES: + x = this->width_ - x - 1; + y = this->height_ - y - 1; + break; + case display::DISPLAY_ROTATION_270_DEGREES: + std::swap(x, y); + y = this->height_ - y - 1; + break; + } + if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) { + return; + } + if (!this->check_buffer_()) + return; + size_t pos = (y * this->width_) + x; + uint8_t hi_byte = static_cast(color.r & 0xF8) | (color.g >> 5); + uint8_t lo_byte = static_cast((color.g & 0x1C) << 3) | (color.b >> 3); + uint16_t new_color = hi_byte | (lo_byte << 8); // big endian + if (this->buffer_[pos] == new_color) + return; + this->buffer_[pos] = new_color; + // low and high watermark may speed up drawing from buffer + if (x < this->x_low_) + this->x_low_ = x; + if (y < this->y_low_) + this->y_low_ = y; + if (x > this->x_high_) + this->x_high_ = x; + if (y > this->y_high_) + this->y_high_ = y; +} +void MipiRgb::fill(Color color) { + if (!this->check_buffer_()) + return; + auto *ptr_16 = reinterpret_cast(this->buffer_); + uint8_t hi_byte = static_cast(color.r & 0xF8) | (color.g >> 5); + uint8_t lo_byte = static_cast((color.g & 0x1C) << 3) | (color.b >> 3); + uint16_t new_color = lo_byte | (hi_byte << 8); // little endian + std::fill_n(ptr_16, this->width_ * this->height_, new_color); +} + +int MipiRgb::get_width() { + switch (this->rotation_) { + case display::DISPLAY_ROTATION_90_DEGREES: + case display::DISPLAY_ROTATION_270_DEGREES: + return this->get_height_internal(); + case display::DISPLAY_ROTATION_0_DEGREES: + case display::DISPLAY_ROTATION_180_DEGREES: + default: + return this->get_width_internal(); + } +} + +int MipiRgb::get_height() { + switch (this->rotation_) { + case display::DISPLAY_ROTATION_0_DEGREES: + case display::DISPLAY_ROTATION_180_DEGREES: + return this->get_height_internal(); + case display::DISPLAY_ROTATION_90_DEGREES: + case display::DISPLAY_ROTATION_270_DEGREES: + default: + return this->get_width_internal(); + } +} + +static std::string get_pin_name(GPIOPin *pin) { + if (pin == nullptr) + return "None"; + return pin->dump_summary(); +} + +void MipiRgb::dump_pins_(uint8_t start, uint8_t end, const char *name, uint8_t offset) { + for (uint8_t i = start; i != end; i++) { + ESP_LOGCONFIG(TAG, " %s pin %d: %s", name, offset++, this->data_pins_[i]->dump_summary().c_str()); + } +} + +void MipiRgb::dump_config() { + ESP_LOGCONFIG(TAG, + "MIPI_RGB LCD" + "\n Model: %s" + "\n Width: %u" + "\n Height: %u" + "\n Rotation: %d degrees" + "\n HSync Pulse Width: %u" + "\n HSync Back Porch: %u" + "\n HSync Front Porch: %u" + "\n VSync Pulse Width: %u" + "\n VSync Back Porch: %u" + "\n VSync Front Porch: %u" + "\n Invert Colors: %s" + "\n Pixel Clock: %dMHz" + "\n Reset Pin: %s" + "\n DE Pin: %s" + "\n PCLK Pin: %s" + "\n HSYNC Pin: %s" + "\n VSYNC Pin: %s", + this->model_, this->width_, this->height_, this->rotation_, this->hsync_pulse_width_, + this->hsync_back_porch_, this->hsync_front_porch_, this->vsync_pulse_width_, this->vsync_back_porch_, + this->vsync_front_porch_, YESNO(this->invert_colors_), this->pclk_frequency_ / 1000000, + get_pin_name(this->reset_pin_).c_str(), get_pin_name(this->de_pin_).c_str(), + get_pin_name(this->pclk_pin_).c_str(), get_pin_name(this->hsync_pin_).c_str(), + get_pin_name(this->vsync_pin_).c_str()); + + if (this->madctl_ & MADCTL_BGR) { + this->dump_pins_(8, 13, "Blue", 0); + this->dump_pins_(13, 16, "Green", 0); + this->dump_pins_(0, 3, "Green", 3); + this->dump_pins_(3, 8, "Red", 0); + } else { + this->dump_pins_(8, 13, "Red", 0); + this->dump_pins_(13, 16, "Green", 0); + this->dump_pins_(0, 3, "Green", 3); + this->dump_pins_(3, 8, "Blue", 0); + } +} + +} // namespace mipi_rgb +} // namespace esphome +#endif // USE_ESP32_VARIANT_ESP32S3 diff --git a/esphome/components/mipi_rgb/mipi_rgb.h b/esphome/components/mipi_rgb/mipi_rgb.h new file mode 100644 index 0000000000..173e23752d --- /dev/null +++ b/esphome/components/mipi_rgb/mipi_rgb.h @@ -0,0 +1,127 @@ +#pragma once + +#ifdef USE_ESP32_VARIANT_ESP32S3 +#include "esphome/core/gpio.h" +#include "esphome/components/display/display.h" +#include "esp_lcd_panel_ops.h" +#ifdef USE_SPI +#include "esphome/components/spi/spi.h" +#endif + +namespace esphome { +namespace mipi_rgb { + +constexpr static const char *const TAG = "display.mipi_rgb"; +const uint8_t SW_RESET_CMD = 0x01; +const uint8_t SLEEP_OUT = 0x11; +const uint8_t SDIR_CMD = 0xC7; +const uint8_t MADCTL_CMD = 0x36; +const uint8_t INVERT_OFF = 0x20; +const uint8_t INVERT_ON = 0x21; +const uint8_t DISPLAY_ON = 0x29; +const uint8_t CMD2_BKSEL = 0xFF; +const uint8_t CMD2_BK0[5] = {0x77, 0x01, 0x00, 0x00, 0x10}; + +class MipiRgb : public display::Display { + public: + MipiRgb(int width, int height) : width_(width), height_(height) {} + void setup() override; + void loop() override; + void update() override; + void fill(Color color); + void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order, + display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override; + void write_to_display_(int x_start, int y_start, int w, int h, const uint8_t *ptr, int x_offset, int y_offset, + int x_pad); + bool check_buffer_(); + + display::ColorOrder get_color_mode() { return this->color_mode_; } + void set_color_mode(display::ColorOrder color_mode) { this->color_mode_ = color_mode; } + void set_invert_colors(bool invert_colors) { this->invert_colors_ = invert_colors; } + void set_madctl(uint8_t madctl) { this->madctl_ = madctl; } + + void add_data_pin(InternalGPIOPin *data_pin, size_t index) { this->data_pins_[index] = data_pin; }; + void set_de_pin(InternalGPIOPin *de_pin) { this->de_pin_ = de_pin; } + void set_pclk_pin(InternalGPIOPin *pclk_pin) { this->pclk_pin_ = pclk_pin; } + void set_vsync_pin(InternalGPIOPin *vsync_pin) { this->vsync_pin_ = vsync_pin; } + void set_hsync_pin(InternalGPIOPin *hsync_pin) { this->hsync_pin_ = hsync_pin; } + void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; } + void set_width(uint16_t width) { this->width_ = width; } + void set_pclk_frequency(uint32_t pclk_frequency) { this->pclk_frequency_ = pclk_frequency; } + void set_pclk_inverted(bool inverted) { this->pclk_inverted_ = inverted; } + void set_model(const char *model) { this->model_ = model; } + int get_width() override; + int get_height() override; + void set_hsync_back_porch(uint16_t hsync_back_porch) { this->hsync_back_porch_ = hsync_back_porch; } + void set_hsync_front_porch(uint16_t hsync_front_porch) { this->hsync_front_porch_ = hsync_front_porch; } + void set_hsync_pulse_width(uint16_t hsync_pulse_width) { this->hsync_pulse_width_ = hsync_pulse_width; } + void set_vsync_pulse_width(uint16_t vsync_pulse_width) { this->vsync_pulse_width_ = vsync_pulse_width; } + void set_vsync_back_porch(uint16_t vsync_back_porch) { this->vsync_back_porch_ = vsync_back_porch; } + void set_vsync_front_porch(uint16_t vsync_front_porch) { this->vsync_front_porch_ = vsync_front_porch; } + void set_enable_pins(std::vector enable_pins) { this->enable_pins_ = std::move(enable_pins); } + display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; } + int get_width_internal() override { return this->width_; } + int get_height_internal() override { return this->height_; } + void dump_pins_(uint8_t start, uint8_t end, const char *name, uint8_t offset); + void dump_config() override; + void draw_pixel_at(int x, int y, Color color) override; + + // this will be horribly slow. + protected: + void setup_enables_(); + void common_setup_(); + InternalGPIOPin *de_pin_{nullptr}; + InternalGPIOPin *pclk_pin_{nullptr}; + InternalGPIOPin *hsync_pin_{nullptr}; + InternalGPIOPin *vsync_pin_{nullptr}; + GPIOPin *reset_pin_{nullptr}; + InternalGPIOPin *data_pins_[16] = {}; + uint16_t hsync_pulse_width_ = 10; + uint16_t hsync_back_porch_ = 10; + uint16_t hsync_front_porch_ = 20; + uint16_t vsync_pulse_width_ = 10; + uint16_t vsync_back_porch_ = 10; + uint16_t vsync_front_porch_ = 10; + uint32_t pclk_frequency_ = 16 * 1000 * 1000; + bool pclk_inverted_{true}; + uint8_t madctl_{}; + const char *model_{"Unknown"}; + bool invert_colors_{}; + display::ColorOrder color_mode_{display::COLOR_ORDER_BGR}; + size_t width_; + size_t height_; + uint16_t *buffer_{nullptr}; + std::vector enable_pins_{}; + uint16_t x_low_{1}; + uint16_t y_low_{1}; + uint16_t x_high_{0}; + uint16_t y_high_{0}; + + esp_lcd_panel_handle_t handle_{}; +}; + +#ifdef USE_SPI +class MipiRgbSpi : public MipiRgb, + public spi::SPIDevice { + public: + MipiRgbSpi(int width, int height) : MipiRgb(width, height) {} + + void set_init_sequence(const std::vector &init_sequence) { this->init_sequence_ = init_sequence; } + void set_dc_pin(GPIOPin *dc_pin) { this->dc_pin_ = dc_pin; } + void setup() override; + + protected: + void write_command_(uint8_t value); + void write_data_(uint8_t value); + void write_init_sequence_(); + void dump_config(); + + GPIOPin *dc_pin_{nullptr}; + std::vector init_sequence_; +}; +#endif + +} // namespace mipi_rgb +} // namespace esphome +#endif diff --git a/esphome/components/mipi_rgb/models/guition.py b/esphome/components/mipi_rgb/models/guition.py new file mode 100644 index 0000000000..da433e686e --- /dev/null +++ b/esphome/components/mipi_rgb/models/guition.py @@ -0,0 +1,24 @@ +from .st7701s import st7701s + +st7701s.extend( + "GUITION-4848S040", + width=480, + height=480, + data_rate="2MHz", + cs_pin=39, + de_pin=18, + hsync_pin=16, + vsync_pin=17, + pclk_pin=21, + pclk_frequency="12MHz", + pixel_mode="18bit", + mirror_x=True, + mirror_y=True, + data_pins={ + "red": [11, 12, 13, 14, 0], + "green": [8, 20, 3, 46, 9, 10], + "blue": [4, 5, 6, 7, 15], + }, + # Additional configuration for Guition 4848S040, 16 bit bus config + add_init_sequence=((0xCD, 0x00),), +) diff --git a/esphome/components/mipi_rgb/models/lilygo.py b/esphome/components/mipi_rgb/models/lilygo.py index e69de29bb2..109dc42af6 100644 --- a/esphome/components/mipi_rgb/models/lilygo.py +++ b/esphome/components/mipi_rgb/models/lilygo.py @@ -0,0 +1,228 @@ +from esphome.config_validation import UNDEFINED + +from .st7701s import ST7701S + +# fmt: off +ST7701S( + "T-PANEL-S3", + width=480, + height=480, + color_order="BGR", + invert_colors=False, + swap_xy=UNDEFINED, + spi_mode="MODE3", + cs_pin={"xl9535": None, "number": 17}, + reset_pin={"xl9535": None, "number": 5}, + hsync_pin=39, + vsync_pin=40, + pclk_pin=41, + data_pins={ + "red": [12, 13, 42, 46, 45], + "green": [6, 7, 8, 9, 10, 11], + "blue": [1, 2, 3, 4, 5], + }, + hsync_front_porch=20, + hsync_back_porch=0, + hsync_pulse_width=2, + vsync_front_porch=30, + vsync_back_porch=1, + vsync_pulse_width=8, + pclk_frequency="6MHz", + pclk_inverted=False, + initsequence=( + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x13), (0xEF, 0x08), (0xFF, 0x77, 0x01, 0x00, 0x00, 0x10), + (0xC0, 0x3B, 0x00), (0xC1, 0x0B, 0x02), (0xC2, 0x30, 0x02, 0x37), (0xCC, 0x10), + (0xB0, 0x00, 0x0F, 0x16, 0x0E, 0x11, 0x07, 0x09, 0x09, 0x08, 0x23, 0x05, 0x11, 0x0F, 0x28, 0x2D, 0x18), + (0xB1, 0x00, 0x0F, 0x16, 0x0E, 0x11, 0x07, 0x09, 0x08, 0x09, 0x23, 0x05, 0x11, 0x0F, 0x28, 0x2D, 0x18), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x11), + (0xB0, 0x4D), (0xB1, 0x33), (0xB2, 0x87), (0xB5, 0x4B), (0xB7, 0x8C), (0xB8, 0x20), (0xC1, 0x78), + (0xC2, 0x78), (0xD0, 0x88), (0xE0, 0x00, 0x00, 0x02), + (0xE1, 0x02, 0xF0, 0x00, 0x00, 0x03, 0xF0, 0x00, 0x00, 0x00, 0x44, 0x44), + (0xE2, 0x10, 0x10, 0x40, 0x40, 0xF2, 0xF0, 0x00, 0x00, 0xF2, 0xF0, 0x00, 0x00), + (0xE3, 0x00, 0x00, 0x11, 0x11), (0xE4, 0x44, 0x44), + (0xE5, 0x07, 0xEF, 0xF0, 0xF0, 0x09, 0xF1, 0xF0, 0xF0, 0x03, 0xF3, 0xF0, 0xF0, 0x05, 0xED, 0xF0, 0xF0), + (0xE6, 0x00, 0x00, 0x11, 0x11), (0xE7, 0x44, 0x44), + (0xE8, 0x08, 0xF0, 0xF0, 0xF0, 0x0A, 0xF2, 0xF0, 0xF0, 0x04, 0xF4, 0xF0, 0xF0, 0x06, 0xEE, 0xF0, 0xF0), + (0xEB, 0x00, 0x00, 0xE4, 0xE4, 0x44, 0x88, 0x40), + (0xEC, 0x78, 0x00), + (0xED, 0x20, 0xF9, 0x87, 0x76, 0x65, 0x54, 0x4F, 0xFF, 0xFF, 0xF4, 0x45, 0x56, 0x67, 0x78, 0x9F, 0x02), + (0xEF, 0x10, 0x0D, 0x04, 0x08, 0x3F, 0x1F), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x10), + ), +) + + +t_rgb = ST7701S( + "T-RGB-2.1", + width=480, + height=480, + color_order="BGR", + pixel_mode="18bit", + invert_colors=False, + swap_xy=UNDEFINED, + spi_mode="MODE3", + cs_pin={"xl9535": None, "number": 3}, + de_pin=45, + hsync_pin=47, + vsync_pin=41, + pclk_pin=42, + data_pins={ + "red": [7, 6, 5, 3, 2], + "green": [14, 13, 12, 11, 10, 9], + "blue": [21, 18, 17, 16, 15], + }, + hsync_front_porch=50, + hsync_pulse_width=1, + hsync_back_porch=30, + vsync_front_porch=20, + vsync_pulse_width=1, + vsync_back_porch=30, + pclk_frequency="12MHz", + pclk_inverted=False, + initsequence=( + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x10), + + (0xC0, 0x3B, 0x00), + (0xC1, 0x0B, 0x02), + (0xC2, 0x07, 0x02), + (0xCC, 0x10), + (0xCD, 0x08), + + (0xB0, + 0x00, 0x11, 0x16, 0x0e, + 0x11, 0x06, 0x05, 0x09, + 0x08, 0x21, 0x06, 0x13, + 0x10, 0x29, 0x31, 0x18), + + (0xB1, + 0x00, 0x11, 0x16, 0x0e, + 0x11, 0x07, 0x05, 0x09, + 0x09, 0x21, 0x05, 0x13, + 0x11, 0x2a, 0x31, 0x18), + + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x11), + + (0xB0, 0x6D), + (0xB1, 0x37), + (0xB2, 0x81), + (0xB3, 0x80), + (0xB5, 0x43), + (0xB7, 0x85), + (0xB8, 0x20), + + (0xC1, 0x78), + (0xC2, 0x78), + (0xC3, 0x8C), + + (0xD0, 0x88), + + (0xE0, 0x00, 0x00, 0x02), + (0xE1, + 0x03, 0xA0, 0x00, 0x00, + 0x04, 0xA0, 0x00, 0x00, + 0x00, 0x20, 0x20), + + (0xE2, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00), + + (0xE3, 0x00, 0x00, 0x11, 0x00), + (0xE4, 0x22, 0x00), + + (0xE5, + 0x05, 0xEC, 0xA0, 0xA0, + 0x07, 0xEE, 0xA0, 0xA0, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00), + + (0xE6, 0x00, 0x00, 0x11, 0x00), + (0xE7, 0x22, 0x00), + + (0xE8, + 0x06, 0xED, 0xA0, 0xA0, + 0x08, 0xEF, 0xA0, 0xA0, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00), + + (0xEB, 0x00, 0x00, 0x40, 0x40, 0x00, 0x00, 0x10), + + (0xED, + 0xFF, 0xFF, 0xFF, 0xBA, + 0x0A, 0xBF, 0x45, 0xFF, + 0xFF, 0x54, 0xFB, 0xA0, + 0xAB, 0xFF, 0xFF, 0xFF), + + (0xEF, 0x10, 0x0D, 0x04, 0x08, 0x3F, 0x1F), + + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x13), + (0xEF, 0x08), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x10) + ) + +) +t_rgb.extend( + "T-RGB-2.8", + initsequence=( + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x13), + (0xEF, 0x08), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x10), + (0xC0, 0x3B, 0x00), + (0xC1, 0x10, 0x0C), + (0xC2, 0x07, 0x0A), + (0xC7, 0x00), + (0xC7, 0x10), + (0xCD, 0x08), + (0xB0, + 0x05, 0x12, 0x98, 0x0e, 0x0F, + 0x07, 0x07, 0x09, 0x09, 0x23, + 0x05, 0x52, 0x0F, 0x67, 0x2C, 0x11), + (0xB1, + 0x0B, 0x11, 0x97, 0x0C, 0x12, + 0x06, 0x06, 0x08, 0x08, 0x22, + 0x03, 0x51, 0x11, 0x66, 0x2B, 0x0F), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x11), + (0xB0, 0x5D), + (0xB1, 0x2D), + (0xB2, 0x81), + (0xB3, 0x80), + (0xB5, 0x4E), + (0xB7, 0x85), + (0xB8, 0x20), + (0xC1, 0x78), + (0xC2, 0x78), + (0xD0, 0x88), + (0xE0, 0x00, 0x00, 0x02), + (0xE1, + 0x06, 0x30, 0x08, 0x30, 0x05, + 0x30, 0x07, 0x30, 0x00, 0x33, + 0x33), + (0xE2, + 0x11, 0x11, 0x33, 0x33, 0xf4, + 0x00, 0x00, 0x00, 0xf4, 0x00, + 0x00, 0x00), + (0xE3, 0x00, 0x00, 0x11, 0x11), + (0xE4, 0x44, 0x44), + (0xE5, + 0x0d, 0xf5, 0x30, 0xf0, 0x0f, + 0xf7, 0x30, 0xf0, 0x09, 0xf1, + 0x30, 0xf0, 0x0b, 0xf3, 0x30, 0xf0), + (0xE6, 0x00, 0x00, 0x11, 0x11), + (0xE7, 0x44, 0x44), + (0xE8, + 0x0c, 0xf4, 0x30, 0xf0, + 0x0e, 0xf6, 0x30, 0xf0, + 0x08, 0xf0, 0x30, 0xf0, + 0x0a, 0xf2, 0x30, 0xf0), + (0xe9, 0x36), + (0xEB, 0x00, 0x01, 0xe4, 0xe4, 0x44, 0x88, 0x40), + (0xED, + 0xff, 0x10, 0xaf, 0x76, + 0x54, 0x2b, 0xcf, 0xff, + 0xff, 0xfc, 0xb2, 0x45, + 0x67, 0xfa, 0x01, 0xff), + (0xEF, 0x08, 0x08, 0x08, 0x45, 0x3f, 0x54), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x10), + ) +) diff --git a/esphome/components/mipi_rgb/models/rpi.py b/esphome/components/mipi_rgb/models/rpi.py new file mode 100644 index 0000000000..076d96b658 --- /dev/null +++ b/esphome/components/mipi_rgb/models/rpi.py @@ -0,0 +1,9 @@ +from esphome.components.mipi import DriverChip +from esphome.config_validation import UNDEFINED + +# A driver chip for Raspberry Pi MIPI RGB displays. These require no init sequence +DriverChip( + "RPI", + swap_xy=UNDEFINED, + initsequence=(), +) diff --git a/esphome/components/mipi_rgb/models/st7701s.py b/esphome/components/mipi_rgb/models/st7701s.py new file mode 100644 index 0000000000..bfd1c9aa3f --- /dev/null +++ b/esphome/components/mipi_rgb/models/st7701s.py @@ -0,0 +1,214 @@ +from esphome.components.mipi import ( + MADCTL, + MADCTL_ML, + MADCTL_XFLIP, + MODE_BGR, + DriverChip, +) +from esphome.config_validation import UNDEFINED +from esphome.const import CONF_COLOR_ORDER, CONF_HEIGHT, CONF_MIRROR_X, CONF_MIRROR_Y + +SDIR_CMD = 0xC7 + + +class ST7701S(DriverChip): + # The ST7701s does not use the standard MADCTL bits for x/y mirroring + def add_madctl(self, sequence: list, config: dict): + transform = self.get_transform(config) + madctl = 0x00 + if config[CONF_COLOR_ORDER] == MODE_BGR: + madctl |= 0x08 + if transform.get(CONF_MIRROR_Y): + madctl |= MADCTL_ML + sequence.append((MADCTL, madctl)) + sdir = 0 + if transform.get(CONF_MIRROR_X): + sdir |= 0x04 + madctl |= MADCTL_XFLIP + sequence.append((SDIR_CMD, sdir)) + return madctl + + @property + def transforms(self) -> set[str]: + """ + The ST7701 never supports axis swapping, and mirroring the y-axis only works for full height. + """ + if self.get_default(CONF_HEIGHT) != 864: + return {CONF_MIRROR_X} + return {CONF_MIRROR_X, CONF_MIRROR_Y} + + +# fmt: off +st7701s = ST7701S( + "ST7701S", + width=480, + height=864, + swap_xy=UNDEFINED, + hsync_front_porch=20, + hsync_back_porch=10, + hsync_pulse_width=10, + vsync_front_porch=10, + vsync_back_porch=10, + vsync_pulse_width=10, + pclk_frequency="16MHz", + pclk_inverted=True, + initsequence=( + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x10), # Page 0 + (0xC0, 0x3B, 0x00), (0xC1, 0x0D, 0x02), (0xC2, 0x31, 0x05), + (0xB0, 0x00, 0x11, 0x18, 0x0E, 0x11, 0x06, 0x07, 0x08, 0x07, 0x22, 0x04, 0x12, 0x0F, 0xAA, 0x31, 0x18,), + (0xB1, 0x00, 0x11, 0x19, 0x0E, 0x12, 0x07, 0x08, 0x08, 0x08, 0x22, 0x04, 0x11, 0x11, 0xA9, 0x32, 0x18,), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x11), # page 1 + (0xB0, 0x60), (0xB1, 0x32), (0xB2, 0x07), (0xB3, 0x80), (0xB5, 0x49), (0xB7, 0x85), (0xB8, 0x21), (0xC1, 0x78), + (0xC2, 0x78), (0xE0, 0x00, 0x1B, 0x02), + (0xE1, 0x08, 0xA0, 0x00, 0x00, 0x07, 0xA0, 0x00, 0x00, 0x00, 0x44, 0x44), + (0xE2, 0x11, 0x11, 0x44, 0x44, 0xED, 0xA0, 0x00, 0x00, 0xEC, 0xA0, 0x00, 0x00), + (0xE3, 0x00, 0x00, 0x11, 0x11), + (0xE4, 0x44, 0x44), + (0xE5, 0x0A, 0xE9, 0xD8, 0xA0, 0x0C, 0xEB, 0xD8, 0xA0, 0x0E, 0xED, 0xD8, 0xA0, 0x10, 0xEF, 0xD8, 0xA0,), + (0xE6, 0x00, 0x00, 0x11, 0x11), (0xE7, 0x44, 0x44), + (0xE8, 0x09, 0xE8, 0xD8, 0xA0, 0x0B, 0xEA, 0xD8, 0xA0, 0x0D, 0xEC, 0xD8, 0xA0, 0x0F, 0xEE, 0xD8, 0xA0,), + (0xEB, 0x02, 0x00, 0xE4, 0xE4, 0x88, 0x00, 0x40), (0xEC, 0x3C, 0x00), + (0xED, 0xAB, 0x89, 0x76, 0x54, 0x02, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x20, 0x45, 0x67, 0x98, 0xBA,), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x13), # Page 3 + (0xE5, 0xE4), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x10), # Page 0 + (0xCD, 0x08), + ) +) + +st7701s.extend( + "MAKERFABS-4", + width=480, + height=480, + color_order="RGB", + invert_colors=True, + pixel_mode="18bit", + cs_pin=1, + de_pin={ + "number": 45, + "ignore_strapping_warning": True + }, + hsync_pin=5, + vsync_pin=4, + pclk_pin=21, + data_pins={ + "red": [39, 40, 41, 42, 2], + "green": [0, 9, 14, 47, 48, 3], + "blue": [6, 7, 15, 16, 8] + } +) + +st7701s.extend( + "SEEED-INDICATOR-D1", + width=480, + height=480, + mirror_x=True, + mirror_y=True, + invert_colors=True, + pixel_mode="18bit", + spi_mode="MODE3", + data_rate="2MHz", + hsync_front_porch=10, + hsync_pulse_width=8, + hsync_back_porch=50, + vsync_front_porch=10, + vsync_pulse_width=8, + vsync_back_porch=20, + cs_pin={"pca9554": None, "number": 4}, + de_pin=18, + hsync_pin=16, + vsync_pin=17, + pclk_pin=21, + pclk_inverted=False, + data_pins={ + "red": [4, 3, 2, 1, 0], + "green": [10, 9, 8, 7, 6, 5], + "blue": [15, 14, 13, 12, 11] + }, +) + +st7701s.extend( + "UEDX48480021-MD80ET", + width=480, + height=480, + pixel_mode="18bit", + cs_pin=18, + reset_pin=8, + de_pin=17, + vsync_pin={"number": 3, "ignore_strapping_warning": True}, + hsync_pin={"number": 46, "ignore_strapping_warning": True}, + pclk_pin=9, + data_pins={ + "red": [40, 41, 42, 2, 1], + "green": [21, 47, 48, 45, 38, 39], + "blue": [10, 11, {"number": 12, "allow_other_uses": True}, {"number": 13, "allow_other_uses": True}, 14] + }, + initsequence=( + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x13), (0xEF, 0x08), (0xFF, 0x77, 0x01, 0x00, 0x00, 0x10), + (0xC0, 0x3B, 0x00), (0xC1, 0x0B, 0x02), (0xC2, 0x07, 0x02), (0xC7, 0x00), (0xCC, 0x10), (0xCD, 0x08), + (0xB0, 0x00, 0x11, 0x16, 0x0E, 0x11, 0x06, 0x05, 0x09, 0x08, 0x21, 0x06, 0x13, 0x10, 0x29, 0x31, 0x18), + (0xB1, 0x00, 0x11, 0x16, 0x0E, 0x11, 0x07, 0x05, 0x09, 0x09, 0x21, 0x05, 0x13, 0x11, 0x2A, 0x31, 0x18), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x11), + (0xB0, 0x6D), (0xB1, 0x37), (0xB2, 0x8B), (0xB3, 0x80), (0xB5, 0x43), (0xB7, 0x85), + (0xB8, 0x20), (0xC0, 0x09), (0xC1, 0x78), (0xC2, 0x78), (0xD0, 0x88), + (0xE0, 0x00, 0x00, 0x02), + (0xE1, 0x03, 0xA0, 0x00, 0x00, 0x04, 0xA0, 0x00, 0x00, 0x00, 0x20, 0x20), + (0xE2, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00), + (0xE3, 0x00, 0x00, 0x11, 0x00), + (0xE4, 0x22, 0x00), + (0xE5, 0x05, 0xEC, 0xF6, 0xCA, 0x07, 0xEE, 0xF6, 0xCA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00), + (0xE6, 0x00, 0x00, 0x11, 0x00), + (0xE7, 0x22, 0x00), + (0xE8, 0x06, 0xED, 0xF6, 0xCA, 0x08, 0xEF, 0xF6, 0xCA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00), + (0xE9, 0x36, 0x00), + (0xEB, 0x00, 0x00, 0x40, 0x40, 0x00, 0x00, 0x00), + (0xED, 0xFF, 0xFF, 0xFF, 0xBA, 0x0A, 0xFF, 0x45, 0xFF, 0xFF, 0x54, 0xFF, 0xA0, 0xAB, 0xFF, 0xFF, 0xFF), + (0xEF, 0x08, 0x08, 0x08, 0x45, 0x3F, 0x54), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x13), (0xE8, 0x00, 0x0E), (0xFF, 0x77, 0x01, 0x00, 0x00, 0x00), + (0x11, 0x00), (0xFF, 0x77, 0x01, 0x00, 0x00, 0x13), (0xE8, 0x00, 0x0C), + (0xE8, 0x00, 0x00), (0xFF, 0x77, 0x01, 0x00, 0x00, 0x00) + ) +) + +st7701s.extend( + "ZX2D10GE01R-V4848", + width=480, + height=480, + pixel_mode="18bit", + cs_pin=21, + de_pin=39, + vsync_pin=48, + hsync_pin=40, + pclk_pin={"number": 45, "ignore_strapping_warning": True}, + pclk_frequency="15MHz", + pclk_inverted=True, + hsync_pulse_width=10, + hsync_back_porch=10, + hsync_front_porch=10, + vsync_pulse_width=2, + vsync_back_porch=12, + vsync_front_porch=14, + data_pins={ + "red": [10, 16, 9, 15, 46], + "green": [8, 13, 18, 12, 11, 17], + "blue": [{"number": 47, "allow_other_uses": True}, {"number": 41, "allow_other_uses": True}, 0, 42, 14] + }, + initsequence=( + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x13), (0xEF, 0x08), (0xFF, 0x77, 0x01, 0x00, 0x00, 0x10), (0xC0, 0x3B, 0x00), + (0xC1, 0x0B, 0x02), (0xC2, 0x07, 0x02), (0xCC, 0x10), (0xCD, 0x08), + (0xB0, 0x00, 0x11, 0x16, 0x0e, 0x11, 0x06, 0x05, 0x09, 0x08, 0x21, 0x06, 0x13, 0x10, 0x29, 0x31, 0x18), + (0xB1, 0x00, 0x11, 0x16, 0x0e, 0x11, 0x07, 0x05, 0x09, 0x09, 0x21, 0x05, 0x13, 0x11, 0x2a, 0x31, 0x18), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x11), (0xB0, 0x6d), (0xB1, 0x37), (0xB2, 0x81), (0xB3, 0x80), (0xB5, 0x43), + (0xB7, 0x85), (0xB8, 0x20), (0xC1, 0x78), (0xC2, 0x78), (0xD0, 0x88), (0xE0, 0x00, 0x00, 0x02), + (0xE1, 0x03, 0xA0, 0x00, 0x00, 0x04, 0xA0, 0x00, 0x00, 0x00, 0x20, 0x20), + (0xE2, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00), + (0xE3, 0x00, 0x00, 0x11, 0x00), (0xE4, 0x22, 0x00), + (0xE5, 0x05, 0xEC, 0xA0, 0xA0, 0x07, 0xEE, 0xA0, 0xA0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00), + (0xE6, 0x00, 0x00, 0x11, 0x00), (0xE7, 0x22, 0x00), + (0xE8, 0x06, 0xED, 0xA0, 0xA0, 0x08, 0xEF, 0xA0, 0xA0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00), + (0xEB, 0x00, 0x00, 0x40, 0x40, 0x00, 0x00, 0x00), + (0xED, 0xFF, 0xFF, 0xFF, 0xBA, 0x0A, 0xBF, 0x45, 0xFF, 0xFF, 0x54, 0xFB, 0xA0, 0xAB, 0xFF, 0xFF, 0xFF), + (0xEF, 0x10, 0x0D, 0x04, 0x08, 0x3F, 0x1F), + (0xFF, 0x77, 0x01, 0x00, 0x00, 0x13), (0xEF, 0x08), (0xFF, 0x77, 0x01, 0x00, 0x00, 0x00) + ) +) diff --git a/esphome/components/mipi_rgb/models/waveshare.py b/esphome/components/mipi_rgb/models/waveshare.py new file mode 100644 index 0000000000..49a75da232 --- /dev/null +++ b/esphome/components/mipi_rgb/models/waveshare.py @@ -0,0 +1,64 @@ +from esphome.components.mipi import DriverChip +from esphome.config_validation import UNDEFINED + +from .st7701s import st7701s + +wave_4_3 = DriverChip( + "ESP32-S3-TOUCH-LCD-4.3", + swap_xy=UNDEFINED, + initsequence=(), + width=800, + height=480, + pclk_frequency="16MHz", + reset_pin={"ch422g": None, "number": 3}, + enable_pin={"ch422g": None, "number": 2}, + de_pin=5, + hsync_pin={"number": 46, "ignore_strapping_warning": True}, + vsync_pin={"number": 3, "ignore_strapping_warning": True}, + pclk_pin=7, + pclk_inverted=True, + hsync_front_porch=210, + hsync_pulse_width=30, + hsync_back_porch=30, + vsync_front_porch=4, + vsync_pulse_width=4, + vsync_back_porch=4, + data_pins={ + "red": [1, 2, 42, 41, 40], + "green": [39, 0, 45, 48, 47, 21], + "blue": [14, 38, 18, 17, 10], + }, +) +wave_4_3.extend( + "ESP32-S3-TOUCH-LCD-7-800X480", + enable_pin=[{"ch422g": None, "number": 2}, {"ch422g": None, "number": 6}], + hsync_back_porch=8, + hsync_front_porch=8, + hsync_pulse_width=4, + vsync_back_porch=16, + vsync_front_porch=16, + vsync_pulse_width=4, +) + +st7701s.extend( + "WAVESHARE-4-480x480", + data_rate="2MHz", + spi_mode="MODE3", + color_order="BGR", + pixel_mode="18bit", + width=480, + height=480, + invert_colors=True, + cs_pin=42, + de_pin=40, + hsync_pin=38, + vsync_pin=39, + pclk_pin=41, + pclk_frequency="12MHz", + pclk_inverted=False, + data_pins={ + "red": [46, 3, 8, 18, 17], + "green": [14, 13, 12, 11, 10, 9], + "blue": [5, 45, 48, 47, 21], + }, +) diff --git a/tests/components/mipi_rgb/test.esp32-s3-idf.yaml b/tests/components/mipi_rgb/test.esp32-s3-idf.yaml new file mode 100644 index 0000000000..8d0e20d6f5 --- /dev/null +++ b/tests/components/mipi_rgb/test.esp32-s3-idf.yaml @@ -0,0 +1,67 @@ +psram: + mode: octal + +spi: + - clk_pin: + number: 47 + allow_other_uses: true + mosi_pin: + number: 41 + allow_other_uses: true + +display: + - platform: mipi_rgb + model: ZX2D10GE01R-V4848 + update_interval: 1s + color_order: BGR + draw_rounding: 2 + pixel_mode: 18bit + invert_colors: false + use_axis_flips: true + pclk_frequency: 15000000.0 + pclk_inverted: true + byte_order: big_endian + hsync_pulse_width: 10 + hsync_back_porch: 10 + hsync_front_porch: 10 + vsync_pulse_width: 2 + vsync_back_porch: 12 + vsync_front_porch: 14 + data_pins: + red: + - number: 10 + - number: 16 + - number: 9 + - number: 15 + - number: 46 + ignore_strapping_warning: true + green: + - number: 8 + - number: 13 + - number: 18 + - number: 12 + - number: 11 + - number: 17 + blue: + - number: 47 + allow_other_uses: true + - number: 41 + allow_other_uses: true + - number: 0 + ignore_strapping_warning: true + - number: 42 + - number: 14 + de_pin: + number: 39 + pclk_pin: + number: 45 + ignore_strapping_warning: true + hsync_pin: + number: 40 + vsync_pin: + number: 48 + data_rate: 1000000.0 + spi_mode: MODE0 + cs_pin: + number: 21 + show_test_card: true From 666e33e70ba7a59b70cac7127be9c1ac949dcc59 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 7 Sep 2025 20:09:47 -0500 Subject: [PATCH 184/208] [api] Store plaintext error message in PROGMEM on ESP8266 (#10634) --- .../api/api_frame_helper_plaintext.cpp | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/esphome/components/api/api_frame_helper_plaintext.cpp b/esphome/components/api/api_frame_helper_plaintext.cpp index fdaacbd94e..859bb26630 100644 --- a/esphome/components/api/api_frame_helper_plaintext.cpp +++ b/esphome/components/api/api_frame_helper_plaintext.cpp @@ -10,6 +10,10 @@ #include #include +#ifdef USE_ESP8266 +#include +#endif + namespace esphome::api { static const char *const TAG = "api.plaintext"; @@ -197,11 +201,20 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) { // We must send at least 3 bytes to be read, so we add // a message after the indicator byte to ensures its long // enough and can aid in debugging. - const char msg[] = "\x00" - "Bad indicator byte"; + static constexpr uint8_t INDICATOR_MSG_SIZE = 19; +#ifdef USE_ESP8266 + static const char MSG_PROGMEM[] PROGMEM = "\x00" + "Bad indicator byte"; + char msg[INDICATOR_MSG_SIZE]; + memcpy_P(msg, MSG_PROGMEM, INDICATOR_MSG_SIZE); iov[0].iov_base = (void *) msg; - iov[0].iov_len = 19; - this->write_raw_(iov, 1, 19); +#else + static const char MSG[] = "\x00" + "Bad indicator byte"; + iov[0].iov_base = (void *) MSG; +#endif + iov[0].iov_len = INDICATOR_MSG_SIZE; + this->write_raw_(iov, 1, INDICATOR_MSG_SIZE); } return aerr; } From 8d90f13e9723747052136dd4782210be777a6a03 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 7 Sep 2025 20:10:00 -0500 Subject: [PATCH 185/208] [core] Store component source strings in flash on ESP8266 (breaking change) (#10621) --- esphome/components/debug/debug_esp32.cpp | 2 +- .../runtime_stats/runtime_stats.cpp | 33 ++++++----------- .../components/runtime_stats/runtime_stats.h | 14 +++----- esphome/core/application.cpp | 8 ++--- esphome/core/component.cpp | 35 +++++++++---------- esphome/core/component.h | 11 +++--- esphome/core/scheduler.cpp | 8 ++--- esphome/core/scheduler.h | 2 +- esphome/cpp_generator.py | 13 +++++++ esphome/cpp_helpers.py | 4 +-- 10 files changed, 62 insertions(+), 68 deletions(-) diff --git a/esphome/components/debug/debug_esp32.cpp b/esphome/components/debug/debug_esp32.cpp index 37990aeec5..b1dfe1bc9a 100644 --- a/esphome/components/debug/debug_esp32.cpp +++ b/esphome/components/debug/debug_esp32.cpp @@ -52,7 +52,7 @@ void DebugComponent::on_shutdown() { char buffer[REBOOT_MAX_LEN]{}; auto pref = global_preferences->make_preference(REBOOT_MAX_LEN, fnv1_hash(REBOOT_KEY + App.get_name())); if (component != nullptr) { - strncpy(buffer, component->get_component_source(), REBOOT_MAX_LEN - 1); + strncpy(buffer, LOG_STR_ARG(component->get_component_log_str()), REBOOT_MAX_LEN - 1); buffer[REBOOT_MAX_LEN - 1] = '\0'; } ESP_LOGD(TAG, "Storing reboot source: %s", buffer); diff --git a/esphome/components/runtime_stats/runtime_stats.cpp b/esphome/components/runtime_stats/runtime_stats.cpp index 8f5d5daf01..f95be5291f 100644 --- a/esphome/components/runtime_stats/runtime_stats.cpp +++ b/esphome/components/runtime_stats/runtime_stats.cpp @@ -17,16 +17,8 @@ void RuntimeStatsCollector::record_component_time(Component *component, uint32_t if (component == nullptr) return; - // Check if we have cached the name for this component - auto name_it = this->component_names_cache_.find(component); - if (name_it == this->component_names_cache_.end()) { - // First time seeing this component, cache its name - const char *source = component->get_component_source(); - this->component_names_cache_[component] = source; - this->component_stats_[source].record_time(duration_ms); - } else { - this->component_stats_[name_it->second].record_time(duration_ms); - } + // Record stats using component pointer as key + this->component_stats_[component].record_time(duration_ms); if (this->next_log_time_ == 0) { this->next_log_time_ = current_time + this->log_interval_; @@ -42,9 +34,10 @@ void RuntimeStatsCollector::log_stats_() { std::vector stats_to_display; for (const auto &it : this->component_stats_) { + Component *component = it.first; const ComponentRuntimeStats &stats = it.second; if (stats.get_period_count() > 0) { - ComponentStatPair pair = {it.first, &stats}; + ComponentStatPair pair = {component, &stats}; stats_to_display.push_back(pair); } } @@ -54,12 +47,9 @@ void RuntimeStatsCollector::log_stats_() { // Log top components by period runtime for (const auto &it : stats_to_display) { - const char *source = it.name; - const ComponentRuntimeStats *stats = it.stats; - - ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.2fms, max=%" PRIu32 "ms, total=%" PRIu32 "ms", source, - stats->get_period_count(), stats->get_period_avg_time_ms(), stats->get_period_max_time_ms(), - stats->get_period_time_ms()); + ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.2fms, max=%" PRIu32 "ms, total=%" PRIu32 "ms", + LOG_STR_ARG(it.component->get_component_log_str()), it.stats->get_period_count(), + it.stats->get_period_avg_time_ms(), it.stats->get_period_max_time_ms(), it.stats->get_period_time_ms()); } // Log total stats since boot @@ -72,12 +62,9 @@ void RuntimeStatsCollector::log_stats_() { }); for (const auto &it : stats_to_display) { - const char *source = it.name; - const ComponentRuntimeStats *stats = it.stats; - - ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.2fms, max=%" PRIu32 "ms, total=%" PRIu32 "ms", source, - stats->get_total_count(), stats->get_total_avg_time_ms(), stats->get_total_max_time_ms(), - stats->get_total_time_ms()); + ESP_LOGI(TAG, " %s: count=%" PRIu32 ", avg=%.2fms, max=%" PRIu32 "ms, total=%" PRIu32 "ms", + LOG_STR_ARG(it.component->get_component_log_str()), it.stats->get_total_count(), + it.stats->get_total_avg_time_ms(), it.stats->get_total_max_time_ms(), it.stats->get_total_time_ms()); } } diff --git a/esphome/components/runtime_stats/runtime_stats.h b/esphome/components/runtime_stats/runtime_stats.h index e2f8bee563..56122364c2 100644 --- a/esphome/components/runtime_stats/runtime_stats.h +++ b/esphome/components/runtime_stats/runtime_stats.h @@ -79,7 +79,7 @@ class ComponentRuntimeStats { // For sorting components by run time struct ComponentStatPair { - const char *name; + Component *component; const ComponentRuntimeStats *stats; bool operator>(const ComponentStatPair &other) const { @@ -109,15 +109,9 @@ class RuntimeStatsCollector { } } - // Use const char* keys for efficiency - // Custom comparator for const char* keys in map - // Without this, std::map would compare pointer addresses instead of string contents, - // causing identical component names at different addresses to be treated as different keys - struct CStrCompare { - bool operator()(const char *a, const char *b) const { return std::strcmp(a, b) < 0; } - }; - std::map component_stats_; - std::map component_names_cache_; + // Map from component to its stats + // We use Component* as the key since each component is unique + std::map component_stats_; uint32_t log_interval_; uint32_t next_log_time_; }; diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index dc745a2a46..b78f6fb903 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -80,7 +80,7 @@ void Application::register_component_(Component *comp) { for (auto *c : this->components_) { if (comp == c) { - ESP_LOGW(TAG, "Component %s already registered! (%p)", c->get_component_source(), c); + ESP_LOGW(TAG, "Component %s already registered! (%p)", LOG_STR_ARG(c->get_component_log_str()), c); return; } } @@ -340,8 +340,8 @@ void Application::teardown_components(uint32_t timeout_ms) { // Note: At this point, connections are either disconnected or in a bad state, // so this warning will only appear via serial rather than being transmitted to clients for (size_t i = 0; i < pending_count; ++i) { - ESP_LOGW(TAG, "%s did not complete teardown within %" PRIu32 " ms", pending_components[i]->get_component_source(), - timeout_ms); + ESP_LOGW(TAG, "%s did not complete teardown within %" PRIu32 " ms", + LOG_STR_ARG(pending_components[i]->get_component_log_str()), timeout_ms); } } } @@ -473,7 +473,7 @@ void Application::enable_pending_loops_() { // Clear the pending flag and enable the loop component->pending_enable_loop_ = false; - ESP_LOGVV(TAG, "%s loop enabled from ISR", component->get_component_source()); + ESP_LOGVV(TAG, "%s loop enabled from ISR", LOG_STR_ARG(component->get_component_log_str())); component->component_state_ &= ~COMPONENT_STATE_MASK; component->component_state_ |= COMPONENT_STATE_LOOP; diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 44a86a4aa5..ce4e2bf788 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -141,7 +141,7 @@ void Component::call_dump_config() { } } } - ESP_LOGE(TAG, " %s is marked FAILED: %s", this->get_component_source(), + ESP_LOGE(TAG, " %s is marked FAILED: %s", LOG_STR_ARG(this->get_component_log_str()), error_msg ? error_msg : LOG_STR_LITERAL("unspecified")); } } @@ -153,14 +153,14 @@ void Component::call() { case COMPONENT_STATE_CONSTRUCTION: { // State Construction: Call setup and set state to setup this->set_component_state_(COMPONENT_STATE_SETUP); - ESP_LOGV(TAG, "Setup %s", this->get_component_source()); + ESP_LOGV(TAG, "Setup %s", LOG_STR_ARG(this->get_component_log_str())); #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG uint32_t start_time = millis(); #endif this->call_setup(); #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG uint32_t setup_time = millis() - start_time; - ESP_LOGCONFIG(TAG, "Setup %s took %ums", this->get_component_source(), (unsigned) setup_time); + ESP_LOGCONFIG(TAG, "Setup %s took %ums", LOG_STR_ARG(this->get_component_log_str()), (unsigned) setup_time); #endif break; } @@ -181,10 +181,8 @@ void Component::call() { break; } } -const char *Component::get_component_source() const { - if (this->component_source_ == nullptr) - return ""; - return this->component_source_; +const LogString *Component::get_component_log_str() const { + return this->component_source_ == nullptr ? LOG_STR("") : this->component_source_; } bool Component::should_warn_of_blocking(uint32_t blocking_time) { if (blocking_time > this->warn_if_blocking_over_) { @@ -200,7 +198,7 @@ bool Component::should_warn_of_blocking(uint32_t blocking_time) { return false; } void Component::mark_failed() { - ESP_LOGE(TAG, "%s was marked as failed", this->get_component_source()); + ESP_LOGE(TAG, "%s was marked as failed", LOG_STR_ARG(this->get_component_log_str())); this->set_component_state_(COMPONENT_STATE_FAILED); this->status_set_error(); // Also remove from loop since failed components shouldn't loop @@ -212,14 +210,14 @@ void Component::set_component_state_(uint8_t state) { } void Component::disable_loop() { if ((this->component_state_ & COMPONENT_STATE_MASK) != COMPONENT_STATE_LOOP_DONE) { - ESP_LOGVV(TAG, "%s loop disabled", this->get_component_source()); + ESP_LOGVV(TAG, "%s loop disabled", LOG_STR_ARG(this->get_component_log_str())); this->set_component_state_(COMPONENT_STATE_LOOP_DONE); App.disable_component_loop_(this); } } void Component::enable_loop() { if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE) { - ESP_LOGVV(TAG, "%s loop enabled", this->get_component_source()); + ESP_LOGVV(TAG, "%s loop enabled", LOG_STR_ARG(this->get_component_log_str())); this->set_component_state_(COMPONENT_STATE_LOOP); App.enable_component_loop_(this); } @@ -239,7 +237,7 @@ void IRAM_ATTR HOT Component::enable_loop_soon_any_context() { } void Component::reset_to_construction_state() { if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED) { - ESP_LOGI(TAG, "%s is being reset to construction state", this->get_component_source()); + ESP_LOGI(TAG, "%s is being reset to construction state", LOG_STR_ARG(this->get_component_log_str())); this->set_component_state_(COMPONENT_STATE_CONSTRUCTION); // Clear error status when resetting this->status_clear_error(); @@ -286,7 +284,7 @@ void Component::status_set_warning(const char *message) { return; this->component_state_ |= STATUS_LED_WARNING; App.app_state_ |= STATUS_LED_WARNING; - ESP_LOGW(TAG, "%s set Warning flag: %s", this->get_component_source(), + ESP_LOGW(TAG, "%s set Warning flag: %s", LOG_STR_ARG(this->get_component_log_str()), message ? message : LOG_STR_LITERAL("unspecified")); } void Component::status_set_warning(const LogString *message) { @@ -295,7 +293,7 @@ void Component::status_set_warning(const LogString *message) { return; this->component_state_ |= STATUS_LED_WARNING; App.app_state_ |= STATUS_LED_WARNING; - ESP_LOGW(TAG, "%s set Warning flag: %s", this->get_component_source(), + ESP_LOGW(TAG, "%s set Warning flag: %s", LOG_STR_ARG(this->get_component_log_str()), message ? LOG_STR_ARG(message) : LOG_STR_LITERAL("unspecified")); } void Component::status_set_error(const char *message) { @@ -303,7 +301,7 @@ void Component::status_set_error(const char *message) { return; this->component_state_ |= STATUS_LED_ERROR; App.app_state_ |= STATUS_LED_ERROR; - ESP_LOGE(TAG, "%s set Error flag: %s", this->get_component_source(), + ESP_LOGE(TAG, "%s set Error flag: %s", LOG_STR_ARG(this->get_component_log_str()), message ? message : LOG_STR_LITERAL("unspecified")); if (message != nullptr) { // Lazy allocate the error messages vector if needed @@ -325,13 +323,13 @@ void Component::status_clear_warning() { if ((this->component_state_ & STATUS_LED_WARNING) == 0) return; this->component_state_ &= ~STATUS_LED_WARNING; - ESP_LOGW(TAG, "%s cleared Warning flag", this->get_component_source()); + ESP_LOGW(TAG, "%s cleared Warning flag", LOG_STR_ARG(this->get_component_log_str())); } void Component::status_clear_error() { if ((this->component_state_ & STATUS_LED_ERROR) == 0) return; this->component_state_ &= ~STATUS_LED_ERROR; - ESP_LOGE(TAG, "%s cleared Error flag", this->get_component_source()); + ESP_LOGE(TAG, "%s cleared Error flag", LOG_STR_ARG(this->get_component_log_str())); } void Component::status_momentary_warning(const std::string &name, uint32_t length) { this->status_set_warning(); @@ -442,8 +440,9 @@ uint32_t WarnIfComponentBlockingGuard::finish() { should_warn = blocking_time > WARN_IF_BLOCKING_OVER_MS; } if (should_warn) { - const char *src = component_ == nullptr ? "" : component_->get_component_source(); - ESP_LOGW(TAG, "%s took a long time for an operation (%" PRIu32 " ms)", src, blocking_time); + ESP_LOGW(TAG, "%s took a long time for an operation (%" PRIu32 " ms)", + component_ == nullptr ? LOG_STR_LITERAL("") : LOG_STR_ARG(component_->get_component_log_str()), + blocking_time); ESP_LOGW(TAG, "Components should block for at most 30 ms"); } diff --git a/esphome/core/component.h b/esphome/core/component.h index 9dfcbb92fa..e97941374d 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -5,6 +5,7 @@ #include #include +#include "esphome/core/log.h" #include "esphome/core/optional.h" namespace esphome { @@ -223,12 +224,12 @@ class Component { * * This is set by the ESPHome core, and should not be called manually. */ - void set_component_source(const char *source) { component_source_ = source; } - /** Get the integration where this component was declared as a string. + void set_component_source(const LogString *source) { component_source_ = source; } + /** Get the integration where this component was declared as a LogString for logging. * - * Returns "" if source not set + * Returns LOG_STR("") if source not set */ - const char *get_component_source() const; + const LogString *get_component_log_str() const; bool should_warn_of_blocking(uint32_t blocking_time); @@ -408,7 +409,7 @@ class Component { bool cancel_defer(const std::string &name); // NOLINT // Ordered for optimal packing on 32-bit systems - const char *component_source_{nullptr}; + const LogString *component_source_{nullptr}; uint16_t warn_if_blocking_over_{WARN_IF_BLOCKING_OVER_MS}; ///< Warn if blocked for this many ms (max 65.5s) /// State of this component - each bit has a purpose: /// Bits 0-2: Component state (0x00=CONSTRUCTION, 0x01=SETUP, 0x02=LOOP, 0x03=FAILED, 0x04=LOOP_DONE) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 90d19e1ead..262349b6f9 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -162,10 +162,10 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type // Debug logging const char *type_str = (type == SchedulerItem::TIMEOUT) ? "timeout" : "interval"; if (type == SchedulerItem::TIMEOUT) { - ESP_LOGD(TAG, "set_%s(name='%s/%s', %s=%" PRIu32 ")", type_str, item->get_source(), + ESP_LOGD(TAG, "set_%s(name='%s/%s', %s=%" PRIu32 ")", type_str, LOG_STR_ARG(item->get_source()), name_cstr ? name_cstr : "(null)", type_str, delay); } else { - ESP_LOGD(TAG, "set_%s(name='%s/%s', %s=%" PRIu32 ", offset=%" PRIu32 ")", type_str, item->get_source(), + ESP_LOGD(TAG, "set_%s(name='%s/%s', %s=%" PRIu32 ", offset=%" PRIu32 ")", type_str, LOG_STR_ARG(item->get_source()), name_cstr ? name_cstr : "(null)", type_str, delay, static_cast(item->get_next_execution() - now)); } @@ -387,7 +387,7 @@ void HOT Scheduler::call(uint32_t now) { const char *name = item->get_name(); bool is_cancelled = is_item_removed_(item.get()); ESP_LOGD(TAG, " %s '%s/%s' interval=%" PRIu32 " next_execution in %" PRIu64 "ms at %" PRIu64 "%s", - item->get_type_str(), item->get_source(), name ? name : "(null)", item->interval, + item->get_type_str(), LOG_STR_ARG(item->get_source()), name ? name : "(null)", item->interval, item->get_next_execution() - now_64, item->get_next_execution(), is_cancelled ? " [CANCELLED]" : ""); old_items.push_back(std::move(item)); @@ -478,7 +478,7 @@ void HOT Scheduler::call(uint32_t now) { #ifdef ESPHOME_DEBUG_SCHEDULER const char *item_name = item->get_name(); ESP_LOGV(TAG, "Running %s '%s/%s' with interval=%" PRIu32 " next_execution=%" PRIu64 " (now=%" PRIu64 ")", - item->get_type_str(), item->get_source(), item_name ? item_name : "(null)", item->interval, + item->get_type_str(), LOG_STR_ARG(item->get_source()), item_name ? item_name : "(null)", item->interval, item->get_next_execution(), now_64); #endif /* ESPHOME_DEBUG_SCHEDULER */ diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 68ad64b9b1..301342e8c2 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -204,7 +204,7 @@ class Scheduler { next_execution_high_ = static_cast(value >> 32); } constexpr const char *get_type_str() const { return (type == TIMEOUT) ? "timeout" : "interval"; } - const char *get_source() const { return component ? component->get_component_source() : "unknown"; } + const LogString *get_source() const { return component ? component->get_component_log_str() : LOG_STR("unknown"); } }; // Common implementation for both timeout and interval diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index 34e4eec1ee..291592dd2b 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -253,6 +253,19 @@ class StringLiteral(Literal): return cpp_string_escape(self.string) +class LogStringLiteral(Literal): + """A string literal that uses LOG_STR() macro for flash storage on ESP8266.""" + + __slots__ = ("string",) + + def __init__(self, string: str) -> None: + super().__init__() + self.string = string + + def __str__(self) -> str: + return f"LOG_STR({cpp_string_escape(self.string)})" + + class IntLiteral(Literal): __slots__ = ("i",) diff --git a/esphome/cpp_helpers.py b/esphome/cpp_helpers.py index b61b215bdc..2698b9b3d5 100644 --- a/esphome/cpp_helpers.py +++ b/esphome/cpp_helpers.py @@ -9,7 +9,7 @@ from esphome.const import ( ) from esphome.core import CORE, ID, coroutine from esphome.coroutine import FakeAwaitable -from esphome.cpp_generator import add, get_variable +from esphome.cpp_generator import LogStringLiteral, add, get_variable from esphome.cpp_types import App from esphome.types import ConfigFragmentType, ConfigType from esphome.util import Registry, RegistryEntry @@ -76,7 +76,7 @@ async def register_component(var, config): "Error while finding name of component, please report this", exc_info=e ) if name is not None: - add(var.set_component_source(name)) + add(var.set_component_source(LogStringLiteral(name))) add(App.register_component(var)) return var From e5bba00deb724808260d67387d4afd4a9ada5e92 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 8 Sep 2025 08:46:30 -0500 Subject: [PATCH 186/208] [esp32] Reduce GPIO memory usage by 50% through bit-packing (#10556) --- esphome/components/esp32/gpio.cpp | 50 +++++++++++++++++-------------- esphome/components/esp32/gpio.h | 37 ++++++++++++++++------- 2 files changed, 53 insertions(+), 34 deletions(-) diff --git a/esphome/components/esp32/gpio.cpp b/esphome/components/esp32/gpio.cpp index 27572063ca..a98245b889 100644 --- a/esphome/components/esp32/gpio.cpp +++ b/esphome/components/esp32/gpio.cpp @@ -54,13 +54,13 @@ struct ISRPinArg { ISRInternalGPIOPin ESP32InternalGPIOPin::to_isr() const { auto *arg = new ISRPinArg{}; // NOLINT(cppcoreguidelines-owning-memory) - arg->pin = this->pin_; + arg->pin = this->get_pin_num(); arg->flags = gpio::FLAG_NONE; - arg->inverted = inverted_; + arg->inverted = this->pin_flags_.inverted; #if defined(USE_ESP32_VARIANT_ESP32) - arg->use_rtc = rtc_gpio_is_valid_gpio(this->pin_); + arg->use_rtc = rtc_gpio_is_valid_gpio(this->get_pin_num()); if (arg->use_rtc) - arg->rtc_pin = rtc_io_number_get(this->pin_); + arg->rtc_pin = rtc_io_number_get(this->get_pin_num()); #endif return ISRInternalGPIOPin((void *) arg); } @@ -69,23 +69,23 @@ void ESP32InternalGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpi gpio_int_type_t idf_type = GPIO_INTR_ANYEDGE; switch (type) { case gpio::INTERRUPT_RISING_EDGE: - idf_type = inverted_ ? GPIO_INTR_NEGEDGE : GPIO_INTR_POSEDGE; + idf_type = this->pin_flags_.inverted ? GPIO_INTR_NEGEDGE : GPIO_INTR_POSEDGE; break; case gpio::INTERRUPT_FALLING_EDGE: - idf_type = inverted_ ? GPIO_INTR_POSEDGE : GPIO_INTR_NEGEDGE; + idf_type = this->pin_flags_.inverted ? GPIO_INTR_POSEDGE : GPIO_INTR_NEGEDGE; break; case gpio::INTERRUPT_ANY_EDGE: idf_type = GPIO_INTR_ANYEDGE; break; case gpio::INTERRUPT_LOW_LEVEL: - idf_type = inverted_ ? GPIO_INTR_HIGH_LEVEL : GPIO_INTR_LOW_LEVEL; + idf_type = this->pin_flags_.inverted ? GPIO_INTR_HIGH_LEVEL : GPIO_INTR_LOW_LEVEL; break; case gpio::INTERRUPT_HIGH_LEVEL: - idf_type = inverted_ ? GPIO_INTR_LOW_LEVEL : GPIO_INTR_HIGH_LEVEL; + idf_type = this->pin_flags_.inverted ? GPIO_INTR_LOW_LEVEL : GPIO_INTR_HIGH_LEVEL; break; } - gpio_set_intr_type(pin_, idf_type); - gpio_intr_enable(pin_); + gpio_set_intr_type(this->get_pin_num(), idf_type); + gpio_intr_enable(this->get_pin_num()); if (!isr_service_installed) { auto res = gpio_install_isr_service(ESP_INTR_FLAG_LEVEL3); if (res != ESP_OK) { @@ -94,31 +94,31 @@ void ESP32InternalGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpi } isr_service_installed = true; } - gpio_isr_handler_add(pin_, func, arg); + gpio_isr_handler_add(this->get_pin_num(), func, arg); } std::string ESP32InternalGPIOPin::dump_summary() const { char buffer[32]; - snprintf(buffer, sizeof(buffer), "GPIO%" PRIu32, static_cast(pin_)); + snprintf(buffer, sizeof(buffer), "GPIO%" PRIu32, static_cast(this->pin_)); return buffer; } void ESP32InternalGPIOPin::setup() { gpio_config_t conf{}; - conf.pin_bit_mask = 1ULL << static_cast(pin_); - conf.mode = flags_to_mode(flags_); - conf.pull_up_en = flags_ & gpio::FLAG_PULLUP ? GPIO_PULLUP_ENABLE : GPIO_PULLUP_DISABLE; - conf.pull_down_en = flags_ & gpio::FLAG_PULLDOWN ? GPIO_PULLDOWN_ENABLE : GPIO_PULLDOWN_DISABLE; + conf.pin_bit_mask = 1ULL << static_cast(this->pin_); + conf.mode = flags_to_mode(this->flags_); + conf.pull_up_en = this->flags_ & gpio::FLAG_PULLUP ? GPIO_PULLUP_ENABLE : GPIO_PULLUP_DISABLE; + conf.pull_down_en = this->flags_ & gpio::FLAG_PULLDOWN ? GPIO_PULLDOWN_ENABLE : GPIO_PULLDOWN_DISABLE; conf.intr_type = GPIO_INTR_DISABLE; gpio_config(&conf); - if (flags_ & gpio::FLAG_OUTPUT) { - gpio_set_drive_capability(pin_, drive_strength_); + if (this->flags_ & gpio::FLAG_OUTPUT) { + gpio_set_drive_capability(this->get_pin_num(), this->get_drive_strength()); } } void ESP32InternalGPIOPin::pin_mode(gpio::Flags flags) { // can't call gpio_config here because that logs in esp-idf which may cause issues - gpio_set_direction(pin_, flags_to_mode(flags)); + gpio_set_direction(this->get_pin_num(), flags_to_mode(flags)); gpio_pull_mode_t pull_mode = GPIO_FLOATING; if ((flags & gpio::FLAG_PULLUP) && (flags & gpio::FLAG_PULLDOWN)) { pull_mode = GPIO_PULLUP_PULLDOWN; @@ -127,12 +127,16 @@ void ESP32InternalGPIOPin::pin_mode(gpio::Flags flags) { } else if (flags & gpio::FLAG_PULLDOWN) { pull_mode = GPIO_PULLDOWN_ONLY; } - gpio_set_pull_mode(pin_, pull_mode); + gpio_set_pull_mode(this->get_pin_num(), pull_mode); } -bool ESP32InternalGPIOPin::digital_read() { return bool(gpio_get_level(pin_)) != inverted_; } -void ESP32InternalGPIOPin::digital_write(bool value) { gpio_set_level(pin_, value != inverted_ ? 1 : 0); } -void ESP32InternalGPIOPin::detach_interrupt() const { gpio_intr_disable(pin_); } +bool ESP32InternalGPIOPin::digital_read() { + return bool(gpio_get_level(this->get_pin_num())) != this->pin_flags_.inverted; +} +void ESP32InternalGPIOPin::digital_write(bool value) { + gpio_set_level(this->get_pin_num(), value != this->pin_flags_.inverted ? 1 : 0); +} +void ESP32InternalGPIOPin::detach_interrupt() const { gpio_intr_disable(this->get_pin_num()); } } // namespace esp32 diff --git a/esphome/components/esp32/gpio.h b/esphome/components/esp32/gpio.h index 0fefc1c058..565e276ea8 100644 --- a/esphome/components/esp32/gpio.h +++ b/esphome/components/esp32/gpio.h @@ -7,12 +7,18 @@ namespace esphome { namespace esp32 { +// Static assertions to ensure our bit-packed fields can hold the enum values +static_assert(GPIO_NUM_MAX <= 256, "gpio_num_t has too many values for uint8_t"); +static_assert(GPIO_DRIVE_CAP_MAX <= 4, "gpio_drive_cap_t has too many values for 2-bit field"); + class ESP32InternalGPIOPin : public InternalGPIOPin { public: - void set_pin(gpio_num_t pin) { pin_ = pin; } - void set_inverted(bool inverted) { inverted_ = inverted; } - void set_drive_strength(gpio_drive_cap_t drive_strength) { drive_strength_ = drive_strength; } - void set_flags(gpio::Flags flags) { flags_ = flags; } + void set_pin(gpio_num_t pin) { this->pin_ = static_cast(pin); } + void set_inverted(bool inverted) { this->pin_flags_.inverted = inverted; } + void set_drive_strength(gpio_drive_cap_t drive_strength) { + this->pin_flags_.drive_strength = static_cast(drive_strength); + } + void set_flags(gpio::Flags flags) { this->flags_ = flags; } void setup() override; void pin_mode(gpio::Flags flags) override; @@ -21,17 +27,26 @@ class ESP32InternalGPIOPin : public InternalGPIOPin { std::string dump_summary() const override; void detach_interrupt() const override; ISRInternalGPIOPin to_isr() const override; - uint8_t get_pin() const override { return (uint8_t) pin_; } - gpio::Flags get_flags() const override { return flags_; } - bool is_inverted() const override { return inverted_; } + uint8_t get_pin() const override { return this->pin_; } + gpio::Flags get_flags() const override { return this->flags_; } + bool is_inverted() const override { return this->pin_flags_.inverted; } + gpio_num_t get_pin_num() const { return static_cast(this->pin_); } + gpio_drive_cap_t get_drive_strength() const { return static_cast(this->pin_flags_.drive_strength); } protected: void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; - gpio_num_t pin_; - gpio_drive_cap_t drive_strength_; - gpio::Flags flags_; - bool inverted_; + // Memory layout: 8 bytes total on 32-bit systems + // - 3 bytes for members below + // - 1 byte padding for alignment + // - 4 bytes for vtable pointer + uint8_t pin_; // GPIO pin number (0-255, actual max ~54 on ESP32) + gpio::Flags flags_; // GPIO flags (1 byte) + struct PinFlags { + uint8_t inverted : 1; // Invert pin logic (1 bit) + uint8_t drive_strength : 2; // Drive strength 0-3 (2 bits) + uint8_t reserved : 5; // Reserved for future use (5 bits) + } pin_flags_; // Total: 1 byte // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) static bool isr_service_installed; }; From 75c9430d91ffde5a1febfe6520686b14c6b6e68a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 8 Sep 2025 10:41:03 -0500 Subject: [PATCH 187/208] [core] Fix serial upload regression from DNS resolution PR #10595 (#10648) --- esphome/__main__.py | 39 +++++++++++++++------------------------ esphome/espota2.py | 13 +++++++------ 2 files changed, 22 insertions(+), 30 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index 70d5cacd72..e1f683397f 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -398,28 +398,27 @@ def check_permissions(port: str): def upload_program( config: ConfigType, args: ArgsProtocol, devices: list[str] -) -> int | str: +) -> tuple[int, str | None]: host = devices[0] try: module = importlib.import_module("esphome.components." + CORE.target_platform) if getattr(module, "upload_program")(config, args, host): - return 0 + return 0, host except AttributeError: pass if get_port_type(host) == "SERIAL": check_permissions(host) + + exit_code = 1 if CORE.target_platform in (PLATFORM_ESP32, PLATFORM_ESP8266): file = getattr(args, "file", None) - return upload_using_esptool(config, host, file, args.upload_speed) + exit_code = upload_using_esptool(config, host, file, args.upload_speed) + elif CORE.target_platform == PLATFORM_RP2040 or CORE.is_libretiny: + exit_code = upload_using_platformio(config, host) + # else: Unknown target platform, exit_code remains 1 - if CORE.target_platform in (PLATFORM_RP2040): - return upload_using_platformio(config, host) - - if CORE.is_libretiny: - return upload_using_platformio(config, host) - - return 1 # Unknown target platform + return exit_code, host if exit_code == 0 else None ota_conf = {} for ota_item in config.get(CONF_OTA, []): @@ -553,7 +552,7 @@ def command_upload(args: ArgsProtocol, config: ConfigType) -> int | None: purpose="uploading", ) - exit_code = upload_program(config, args, devices) + exit_code, _ = upload_program(config, args, devices) if exit_code == 0: _LOGGER.info("Successfully uploaded program.") else: @@ -610,19 +609,11 @@ def command_run(args: ArgsProtocol, config: ConfigType) -> int | None: purpose="uploading", ) - # Try each device for upload until one succeeds - successful_device: str | None = None - for device in devices: - _LOGGER.info("Uploading to %s", device) - exit_code = upload_program(config, args, device) - if exit_code == 0: - _LOGGER.info("Successfully uploaded program.") - successful_device = device - break - if len(devices) > 1: - _LOGGER.warning("Failed to upload to %s", device) - - if successful_device is None: + exit_code, successful_device = upload_program(config, args, devices) + if exit_code == 0: + _LOGGER.info("Successfully uploaded program.") + else: + _LOGGER.warning("Failed to upload to %s", devices) return exit_code if args.no_logs: diff --git a/esphome/espota2.py b/esphome/espota2.py index d83f25a303..3d25af985b 100644 --- a/esphome/espota2.py +++ b/esphome/espota2.py @@ -310,7 +310,7 @@ def perform_ota( def run_ota_impl_( remote_host: str | list[str], remote_port: int, password: str, filename: str -) -> int: +) -> tuple[int, str | None]: # Handle both single host and list of hosts try: # Resolve all hosts at once for parallel DNS resolution @@ -344,21 +344,22 @@ def run_ota_impl_( perform_ota(sock, password, file_handle, filename) except OTAError as err: _LOGGER.error(str(err)) - return 1 + return 1, None finally: sock.close() - return 0 + # Successfully uploaded to sa[0] + return 0, sa[0] _LOGGER.error("Connection failed.") - return 1 + return 1, None def run_ota( remote_host: str | list[str], remote_port: int, password: str, filename: str -) -> int: +) -> tuple[int, str | None]: try: return run_ota_impl_(remote_host, remote_port, password, filename) except OTAError as err: _LOGGER.error(err) - return 1 + return 1, None From 703b5927938d5cb6859f8b5c1a16dca2e5e093a6 Mon Sep 17 00:00:00 2001 From: Thomas Rupprecht Date: Mon, 8 Sep 2025 20:03:41 +0200 Subject: [PATCH 188/208] Add I2S Audio Port for ESP32-C5/C6/H2 (#10414) --- esphome/components/i2s_audio/__init__.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/esphome/components/i2s_audio/__init__.py b/esphome/components/i2s_audio/__init__.py index bfa1b726f1..cff91a546f 100644 --- a/esphome/components/i2s_audio/__init__.py +++ b/esphome/components/i2s_audio/__init__.py @@ -4,6 +4,9 @@ from esphome.components.esp32 import add_idf_sdkconfig_option, get_esp32_variant from esphome.components.esp32.const import ( VARIANT_ESP32, VARIANT_ESP32C3, + VARIANT_ESP32C5, + VARIANT_ESP32C6, + VARIANT_ESP32H2, VARIANT_ESP32P4, VARIANT_ESP32S2, VARIANT_ESP32S3, @@ -62,12 +65,15 @@ I2S_ROLE_OPTIONS = { CONF_SECONDARY: i2s_role_t.I2S_ROLE_SLAVE, # NOLINT } -# https://github.com/espressif/esp-idf/blob/master/components/soc/{variant}/include/soc/soc_caps.h +# https://github.com/espressif/esp-idf/blob/master/components/soc/{variant}/include/soc/soc_caps.h (SOC_I2S_NUM) I2S_PORTS = { VARIANT_ESP32: 2, VARIANT_ESP32S2: 1, VARIANT_ESP32S3: 2, VARIANT_ESP32C3: 1, + VARIANT_ESP32C5: 1, + VARIANT_ESP32C6: 1, + VARIANT_ESP32H2: 1, VARIANT_ESP32P4: 3, } From 5cc0e21bc7d4114ead57cfed0a4fdf894efb3c70 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 8 Sep 2025 16:04:07 -0500 Subject: [PATCH 189/208] [core] Reduce unnecessary nesting in scheduler loop (#10644) --- esphome/core/scheduler.cpp | 122 ++++++++++++++++++------------------- 1 file changed, 58 insertions(+), 64 deletions(-) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 262349b6f9..68da0a56ca 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -436,85 +436,79 @@ void HOT Scheduler::call(uint32_t now) { this->to_remove_ = 0; } while (!this->items_.empty()) { - // use scoping to indicate visibility of `item` variable - { - // Don't copy-by value yet - auto &item = this->items_[0]; - if (item->get_next_execution() > now_64) { - // Not reached timeout yet, done for this call - break; - } - // Don't run on failed components - if (item->component != nullptr && item->component->is_failed()) { - LockGuard guard{this->lock_}; - this->pop_raw_(); - continue; - } + // Don't copy-by value yet + auto &item = this->items_[0]; + if (item->get_next_execution() > now_64) { + // Not reached timeout yet, done for this call + break; + } + // Don't run on failed components + if (item->component != nullptr && item->component->is_failed()) { + LockGuard guard{this->lock_}; + this->pop_raw_(); + continue; + } - // Check if item is marked for removal - // This handles two cases: - // 1. Item was marked for removal after cleanup_() but before we got here - // 2. Item is marked for removal but wasn't at the front of the heap during cleanup_() + // Check if item is marked for removal + // This handles two cases: + // 1. Item was marked for removal after cleanup_() but before we got here + // 2. Item is marked for removal but wasn't at the front of the heap during cleanup_() #ifdef ESPHOME_THREAD_MULTI_NO_ATOMICS - // Multi-threaded platforms without atomics: must take lock to safely read remove flag - { - LockGuard guard{this->lock_}; - if (is_item_removed_(item.get())) { - this->pop_raw_(); - this->to_remove_--; - continue; - } - } -#else - // Single-threaded or multi-threaded with atomics: can check without lock + // Multi-threaded platforms without atomics: must take lock to safely read remove flag + { + LockGuard guard{this->lock_}; if (is_item_removed_(item.get())) { - LockGuard guard{this->lock_}; this->pop_raw_(); this->to_remove_--; continue; } + } +#else + // Single-threaded or multi-threaded with atomics: can check without lock + if (is_item_removed_(item.get())) { + LockGuard guard{this->lock_}; + this->pop_raw_(); + this->to_remove_--; + continue; + } #endif #ifdef ESPHOME_DEBUG_SCHEDULER - const char *item_name = item->get_name(); - ESP_LOGV(TAG, "Running %s '%s/%s' with interval=%" PRIu32 " next_execution=%" PRIu64 " (now=%" PRIu64 ")", - item->get_type_str(), LOG_STR_ARG(item->get_source()), item_name ? item_name : "(null)", item->interval, - item->get_next_execution(), now_64); + const char *item_name = item->get_name(); + ESP_LOGV(TAG, "Running %s '%s/%s' with interval=%" PRIu32 " next_execution=%" PRIu64 " (now=%" PRIu64 ")", + item->get_type_str(), LOG_STR_ARG(item->get_source()), item_name ? item_name : "(null)", item->interval, + item->get_next_execution(), now_64); #endif /* ESPHOME_DEBUG_SCHEDULER */ - // Warning: During callback(), a lot of stuff can happen, including: - // - timeouts/intervals get added, potentially invalidating vector pointers - // - timeouts/intervals get cancelled - this->execute_item_(item.get(), now); + // Warning: During callback(), a lot of stuff can happen, including: + // - timeouts/intervals get added, potentially invalidating vector pointers + // - timeouts/intervals get cancelled + this->execute_item_(item.get(), now); + + LockGuard guard{this->lock_}; + + auto executed_item = std::move(this->items_[0]); + // Only pop after function call, this ensures we were reachable + // during the function call and know if we were cancelled. + this->pop_raw_(); + + if (executed_item->remove) { + // We were removed/cancelled in the function call, stop + this->to_remove_--; + continue; } - { - LockGuard guard{this->lock_}; - - // new scope, item from before might have been moved in the vector - auto item = std::move(this->items_[0]); - // Only pop after function call, this ensures we were reachable - // during the function call and know if we were cancelled. - this->pop_raw_(); - - if (item->remove) { - // We were removed/cancelled in the function call, stop - this->to_remove_--; - continue; - } - - if (item->type == SchedulerItem::INTERVAL) { - item->set_next_execution(now_64 + item->interval); - // Add new item directly to to_add_ - // since we have the lock held - this->to_add_.push_back(std::move(item)); - } else { - // Timeout completed - recycle it - this->recycle_item_(std::move(item)); - } - - has_added_items |= !this->to_add_.empty(); + if (executed_item->type == SchedulerItem::INTERVAL) { + executed_item->set_next_execution(now_64 + executed_item->interval); + // Add new item directly to to_add_ + // since we have the lock held + this->to_add_.push_back(std::move(executed_item)); + } else { + // Timeout completed - recycle it + this->recycle_item_(std::move(executed_item)); } + + has_added_items |= !this->to_add_.empty(); } if (has_added_items) { From f6d69231e891d84b0c56a0dae14037edd66aec98 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Tue, 9 Sep 2025 01:10:29 +0200 Subject: [PATCH 190/208] [light] add missing header (#10590) --- esphome/components/light/light_state.h | 1 + .../components/light/test.nrf52-adafruit.yaml | 19 +++++++++++++++++++ tests/components/light/test.nrf52-mcumgr.yaml | 19 +++++++++++++++++++ 3 files changed, 39 insertions(+) create mode 100644 tests/components/light/test.nrf52-adafruit.yaml create mode 100644 tests/components/light/test.nrf52-mcumgr.yaml diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h index 48323dd3c3..1427c02c35 100644 --- a/esphome/components/light/light_state.h +++ b/esphome/components/light/light_state.h @@ -12,6 +12,7 @@ #include "light_transformer.h" #include +#include namespace esphome { namespace light { diff --git a/tests/components/light/test.nrf52-adafruit.yaml b/tests/components/light/test.nrf52-adafruit.yaml new file mode 100644 index 0000000000..cb421ed4bb --- /dev/null +++ b/tests/components/light/test.nrf52-adafruit.yaml @@ -0,0 +1,19 @@ +esphome: + on_boot: + then: + - light.toggle: test_binary_light + +output: + - platform: gpio + id: test_binary + pin: 0 + +light: + - platform: binary + id: test_binary_light + name: Binary Light + output: test_binary + effects: + - strobe: + on_state: + - logger.log: Binary light state changed diff --git a/tests/components/light/test.nrf52-mcumgr.yaml b/tests/components/light/test.nrf52-mcumgr.yaml new file mode 100644 index 0000000000..cb421ed4bb --- /dev/null +++ b/tests/components/light/test.nrf52-mcumgr.yaml @@ -0,0 +1,19 @@ +esphome: + on_boot: + then: + - light.toggle: test_binary_light + +output: + - platform: gpio + id: test_binary + pin: 0 + +light: + - platform: binary + id: test_binary_light + name: Binary Light + output: test_binary + effects: + - strobe: + on_state: + - logger.log: Binary light state changed From 90c2fdd5652acfdfe9fc5c8909211c86362b8439 Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Tue, 9 Sep 2025 02:56:18 +0200 Subject: [PATCH 191/208] [adc] Fix autorange negative coefficient bug causing incorrect voltage readings (#10549) --- esphome/components/adc/adc_sensor_esp32.cpp | 35 +++++++++++++++++---- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/esphome/components/adc/adc_sensor_esp32.cpp b/esphome/components/adc/adc_sensor_esp32.cpp index 87d4ddd35f..ab6a89fce0 100644 --- a/esphome/components/adc/adc_sensor_esp32.cpp +++ b/esphome/components/adc/adc_sensor_esp32.cpp @@ -241,6 +241,8 @@ float ADCSensor::sample_autorange_() { cali_config.bitwidth = ADC_BITWIDTH_DEFAULT; err = adc_cali_create_scheme_curve_fitting(&cali_config, &handle); + ESP_LOGVV(TAG, "Autorange atten=%d: Calibration handle creation %s (err=%d)", atten, + (err == ESP_OK) ? "SUCCESS" : "FAILED", err); #else adc_cali_line_fitting_config_t cali_config = { .unit_id = this->adc_unit_, @@ -251,10 +253,14 @@ float ADCSensor::sample_autorange_() { #endif }; err = adc_cali_create_scheme_line_fitting(&cali_config, &handle); + ESP_LOGVV(TAG, "Autorange atten=%d: Calibration handle creation %s (err=%d)", atten, + (err == ESP_OK) ? "SUCCESS" : "FAILED", err); #endif int raw; err = adc_oneshot_read(this->adc_handle_, this->channel_, &raw); + ESP_LOGVV(TAG, "Autorange atten=%d: Raw ADC read %s, value=%d (err=%d)", atten, + (err == ESP_OK) ? "SUCCESS" : "FAILED", raw, err); if (err != ESP_OK) { ESP_LOGW(TAG, "ADC read failed in autorange with error %d", err); @@ -275,8 +281,10 @@ float ADCSensor::sample_autorange_() { err = adc_cali_raw_to_voltage(handle, raw, &voltage_mv); if (err == ESP_OK) { voltage = voltage_mv / 1000.0f; + ESP_LOGVV(TAG, "Autorange atten=%d: CALIBRATED - raw=%d -> %dmV -> %.6fV", atten, raw, voltage_mv, voltage); } else { voltage = raw * 3.3f / 4095.0f; + ESP_LOGVV(TAG, "Autorange atten=%d: UNCALIBRATED FALLBACK - raw=%d -> %.6fV (3.3V ref)", atten, raw, voltage); } // Clean up calibration handle #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ @@ -287,6 +295,7 @@ float ADCSensor::sample_autorange_() { #endif } else { voltage = raw * 3.3f / 4095.0f; + ESP_LOGVV(TAG, "Autorange atten=%d: NO CALIBRATION - raw=%d -> %.6fV (3.3V ref)", atten, raw, voltage); } return {raw, voltage}; @@ -324,18 +333,32 @@ float ADCSensor::sample_autorange_() { } const int adc_half = 2048; - uint32_t c12 = std::min(raw12, adc_half); - uint32_t c6 = adc_half - std::abs(raw6 - adc_half); - uint32_t c2 = adc_half - std::abs(raw2 - adc_half); - uint32_t c0 = std::min(4095 - raw0, adc_half); - uint32_t csum = c12 + c6 + c2 + c0; + const uint32_t c12 = std::min(raw12, adc_half); + + const int32_t c6_signed = adc_half - std::abs(raw6 - adc_half); + const uint32_t c6 = (c6_signed > 0) ? c6_signed : 0; // Clamp to prevent underflow + + const int32_t c2_signed = adc_half - std::abs(raw2 - adc_half); + const uint32_t c2 = (c2_signed > 0) ? c2_signed : 0; // Clamp to prevent underflow + + const uint32_t c0 = std::min(4095 - raw0, adc_half); + const uint32_t csum = c12 + c6 + c2 + c0; + + ESP_LOGVV(TAG, "Autorange summary:"); + ESP_LOGVV(TAG, " Raw readings: 12db=%d, 6db=%d, 2.5db=%d, 0db=%d", raw12, raw6, raw2, raw0); + ESP_LOGVV(TAG, " Voltages: 12db=%.6f, 6db=%.6f, 2.5db=%.6f, 0db=%.6f", mv12, mv6, mv2, mv0); + ESP_LOGVV(TAG, " Coefficients: c12=%u, c6=%u, c2=%u, c0=%u, sum=%u", c12, c6, c2, c0, csum); if (csum == 0) { ESP_LOGE(TAG, "Invalid weight sum in autorange calculation"); return NAN; } - return (mv12 * c12 + mv6 * c6 + mv2 * c2 + mv0 * c0) / csum; + const float final_result = (mv12 * c12 + mv6 * c6 + mv2 * c2 + mv0 * c0) / csum; + ESP_LOGV(TAG, "Autorange final: (%.6f*%u + %.6f*%u + %.6f*%u + %.6f*%u)/%u = %.6fV", mv12, c12, mv6, c6, mv2, c2, mv0, + c0, csum, final_result); + + return final_result; } } // namespace adc From f5f84fe825f86c5f5d3d9d240eb08d996efd9caf Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Tue, 9 Sep 2025 02:57:24 +0200 Subject: [PATCH 192/208] [nextion] Increase delay before reboot to prevent TFT upload interruption (#10402) --- esphome/components/nextion/nextion_upload.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/esphome/components/nextion/nextion_upload.cpp b/esphome/components/nextion/nextion_upload.cpp index c47b393f99..7ddd7a2f08 100644 --- a/esphome/components/nextion/nextion_upload.cpp +++ b/esphome/components/nextion/nextion_upload.cpp @@ -11,7 +11,10 @@ static const char *const TAG = "nextion.upload"; bool Nextion::upload_end_(bool successful) { if (successful) { ESP_LOGD(TAG, "Upload successful"); - delay(1500); // NOLINT + for (uint8_t i = 0; i <= 5; i++) { + delay(1000); // NOLINT + App.feed_wdt(); // Feed the watchdog timer. + } App.safe_reboot(); } else { ESP_LOGE(TAG, "Upload failed"); From 59e62a1f44373186dfe65c5d15c76abc04d3e41f Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 9 Sep 2025 14:26:22 +1200 Subject: [PATCH 193/208] Sort codeowners using case-insensitive (#10651) Co-authored-by: J. Nick Koston --- CODEOWNERS | 26 +++++++++++++------------- script/build_codeowners.py | 2 +- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 3f963bf960..a77a7ba86e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -88,7 +88,7 @@ esphome/components/bp1658cj/* @Cossid esphome/components/bp5758d/* @Cossid esphome/components/button/* @esphome/core esphome/components/bytebuffer/* @clydebarrow -esphome/components/camera/* @DT-art1 @bdraco +esphome/components/camera/* @bdraco @DT-art1 esphome/components/camera_encoder/* @DT-art1 esphome/components/canbus/* @danielschramm @mvturnho esphome/components/cap1188/* @mreditor97 @@ -145,9 +145,9 @@ esphome/components/es8156/* @kbx81 esphome/components/es8311/* @kahrendt @kroimon esphome/components/es8388/* @P4uLT esphome/components/esp32/* @esphome/core -esphome/components/esp32_ble/* @Rapsssito @bdraco @jesserockz +esphome/components/esp32_ble/* @bdraco @jesserockz @Rapsssito esphome/components/esp32_ble_client/* @bdraco @jesserockz -esphome/components/esp32_ble_server/* @Rapsssito @clydebarrow @jesserockz +esphome/components/esp32_ble_server/* @clydebarrow @jesserockz @Rapsssito esphome/components/esp32_ble_tracker/* @bdraco esphome/components/esp32_camera_web_server/* @ayufan esphome/components/esp32_can/* @Sympatron @@ -167,7 +167,7 @@ esphome/components/ezo_pmp/* @carlos-sarmiento esphome/components/factory_reset/* @anatoly-savchenkov esphome/components/fastled_base/* @OttoWinter esphome/components/feedback/* @ianchi -esphome/components/fingerprint_grow/* @OnFreund @alexborro @loongyh +esphome/components/fingerprint_grow/* @alexborro @loongyh @OnFreund esphome/components/font/* @clydebarrow @esphome/core esphome/components/fs3000/* @kahrendt esphome/components/ft5x06/* @clydebarrow @@ -203,7 +203,7 @@ esphome/components/heatpumpir/* @rob-deutsch esphome/components/hitachi_ac424/* @sourabhjaiswal esphome/components/hm3301/* @freekode esphome/components/hmac_md5/* @dwmw2 -esphome/components/homeassistant/* @OttoWinter @esphome/core +esphome/components/homeassistant/* @esphome/core @OttoWinter esphome/components/homeassistant/number/* @landonr esphome/components/homeassistant/switch/* @Links2004 esphome/components/honeywell_hih_i2c/* @Benichou34 @@ -228,7 +228,7 @@ esphome/components/iaqcore/* @yozik04 esphome/components/ili9xxx/* @clydebarrow @nielsnl68 esphome/components/improv_base/* @esphome/core esphome/components/improv_serial/* @esphome/core -esphome/components/ina226/* @Sergio303 @latonita +esphome/components/ina226/* @latonita @Sergio303 esphome/components/ina260/* @mreditor97 esphome/components/ina2xx_base/* @latonita esphome/components/ina2xx_i2c/* @latonita @@ -277,8 +277,8 @@ esphome/components/max7219digit/* @rspaargaren esphome/components/max9611/* @mckaymatthew esphome/components/mcp23008/* @jesserockz esphome/components/mcp23017/* @jesserockz -esphome/components/mcp23s08/* @SenexCrenshaw @jesserockz -esphome/components/mcp23s17/* @SenexCrenshaw @jesserockz +esphome/components/mcp23s08/* @jesserockz @SenexCrenshaw +esphome/components/mcp23s17/* @jesserockz @SenexCrenshaw esphome/components/mcp23x08_base/* @jesserockz esphome/components/mcp23x17_base/* @jesserockz esphome/components/mcp23xxx_base/* @jesserockz @@ -354,9 +354,9 @@ esphome/components/pm2005/* @andrewjswan esphome/components/pmsa003i/* @sjtrny esphome/components/pmsx003/* @ximex esphome/components/pmwcs3/* @SeByDocKy -esphome/components/pn532/* @OttoWinter @jesserockz -esphome/components/pn532_i2c/* @OttoWinter @jesserockz -esphome/components/pn532_spi/* @OttoWinter @jesserockz +esphome/components/pn532/* @jesserockz @OttoWinter +esphome/components/pn532_i2c/* @jesserockz @OttoWinter +esphome/components/pn532_spi/* @jesserockz @OttoWinter esphome/components/pn7150/* @jesserockz @kbx81 esphome/components/pn7150_i2c/* @jesserockz @kbx81 esphome/components/pn7160/* @jesserockz @kbx81 @@ -365,7 +365,7 @@ esphome/components/pn7160_spi/* @jesserockz @kbx81 esphome/components/power_supply/* @esphome/core esphome/components/preferences/* @esphome/core esphome/components/psram/* @esphome/core -esphome/components/pulse_meter/* @TrentHouliston @cstaahl @stevebaxter +esphome/components/pulse_meter/* @cstaahl @stevebaxter @TrentHouliston esphome/components/pvvx_mithermometer/* @pasiz esphome/components/pylontech/* @functionpointer esphome/components/qmp6988/* @andrewpc @@ -406,7 +406,7 @@ esphome/components/sensirion_common/* @martgras esphome/components/sensor/* @esphome/core esphome/components/sfa30/* @ghsensdev esphome/components/sgp40/* @SenexCrenshaw -esphome/components/sgp4x/* @SenexCrenshaw @martgras +esphome/components/sgp4x/* @martgras @SenexCrenshaw esphome/components/shelly_dimmer/* @edge90 @rnauber esphome/components/sht3xd/* @mrtoy-me esphome/components/sht4x/* @sjtrny diff --git a/script/build_codeowners.py b/script/build_codeowners.py index 4581620095..27ea82611b 100755 --- a/script/build_codeowners.py +++ b/script/build_codeowners.py @@ -82,7 +82,7 @@ for path in components_dir.iterdir(): for path, owners in sorted(codeowners.items()): - owners = sorted(set(owners)) + owners = sorted(set(owners), key=str.casefold) if not owners: continue for owner in owners: From dd8815ec9d4c61d3e509862b77e9614004f61fe0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 9 Sep 2025 01:17:30 -0500 Subject: [PATCH 194/208] [core] Reduce flash usage by refactoring looping component partitioning (#10652) --- esphome/core/application.cpp | 13 ++++++------- esphome/core/application.h | 1 + 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index b78f6fb903..5371d1b56f 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -361,20 +361,19 @@ void Application::calculate_looping_components_() { // Add all components with loop override that aren't already LOOP_DONE // Some components (like logger) may call disable_loop() during initialization // before setup runs, so we need to respect their LOOP_DONE state - for (auto *obj : this->components_) { - if (obj->has_overridden_loop() && - (obj->get_component_state() & COMPONENT_STATE_MASK) != COMPONENT_STATE_LOOP_DONE) { - this->looping_components_.push_back(obj); - } - } + this->add_looping_components_by_state_(false); this->looping_components_active_end_ = this->looping_components_.size(); // Then add any components that are already LOOP_DONE to the inactive section // This handles components that called disable_loop() during initialization + this->add_looping_components_by_state_(true); +} + +void Application::add_looping_components_by_state_(bool match_loop_done) { for (auto *obj : this->components_) { if (obj->has_overridden_loop() && - (obj->get_component_state() & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE) { + ((obj->get_component_state() & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE) == match_loop_done) { this->looping_components_.push_back(obj); } } diff --git a/esphome/core/application.h b/esphome/core/application.h index 9cb2a4c638..1f22499051 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -431,6 +431,7 @@ class Application { void register_component_(Component *comp); void calculate_looping_components_(); + void add_looping_components_by_state_(bool match_loop_done); // These methods are called by Component::disable_loop() and Component::enable_loop() // Components should not call these directly - use this->disable_loop() or this->enable_loop() From 7adad0ee4920e47ef0735cf4a5f5a0b18bff6ef4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 9 Sep 2025 02:03:35 -0500 Subject: [PATCH 195/208] [core] Refactor insertion sort functions to eliminate code duplication (#10653) --- esphome/core/application.cpp | 33 +++++++++------------------------ 1 file changed, 9 insertions(+), 24 deletions(-) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 5371d1b56f..1be193bb7e 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -34,37 +34,20 @@ namespace esphome { static const char *const TAG = "app"; -// Helper function for insertion sort of components by setup priority +// Helper function for insertion sort of components by priority // Using insertion sort instead of std::stable_sort saves ~1.3KB of flash // by avoiding template instantiations (std::rotate, std::stable_sort, lambdas) // IMPORTANT: This sort is stable (preserves relative order of equal elements), // which is necessary to maintain user-defined component order for same priority -template static void insertion_sort_by_setup_priority(Iterator first, Iterator last) { +template +static void insertion_sort_by_priority(Iterator first, Iterator last) { for (auto it = first + 1; it != last; ++it) { auto key = *it; - float key_priority = key->get_actual_setup_priority(); + float key_priority = (key->*GetPriority)(); auto j = it - 1; // Using '<' (not '<=') ensures stability - equal priority components keep their order - while (j >= first && (*j)->get_actual_setup_priority() < key_priority) { - *(j + 1) = *j; - j--; - } - *(j + 1) = key; - } -} - -// Helper function for insertion sort of components by loop priority -// IMPORTANT: This sort is stable (preserves relative order of equal elements), -// which is required when components are re-sorted during setup() if they block -template static void insertion_sort_by_loop_priority(Iterator first, Iterator last) { - for (auto it = first + 1; it != last; ++it) { - auto key = *it; - float key_priority = key->get_loop_priority(); - auto j = it - 1; - - // Using '<' (not '<=') ensures stability - equal priority components keep their order - while (j >= first && (*j)->get_loop_priority() < key_priority) { + while (j >= first && ((*j)->*GetPriority)() < key_priority) { *(j + 1) = *j; j--; } @@ -91,7 +74,8 @@ void Application::setup() { ESP_LOGV(TAG, "Sorting components by setup priority"); // Sort by setup priority using our helper function - insertion_sort_by_setup_priority(this->components_.begin(), this->components_.end()); + insertion_sort_by_prioritycomponents_.begin()), &Component::get_actual_setup_priority>( + this->components_.begin(), this->components_.end()); // Initialize looping_components_ early so enable_pending_loops_() works during setup this->calculate_looping_components_(); @@ -108,7 +92,8 @@ void Application::setup() { continue; // Sort components 0 through i by loop priority - insertion_sort_by_loop_priority(this->components_.begin(), this->components_.begin() + i + 1); + insertion_sort_by_prioritycomponents_.begin()), &Component::get_loop_priority>( + this->components_.begin(), this->components_.begin() + i + 1); do { uint8_t new_app_state = STATUS_LED_WARNING; From 8993f4e6b43dd1d88fbecdfa7d4584fdfcc98606 Mon Sep 17 00:00:00 2001 From: Mischa Siekmann <45062894+gnumpi@users.noreply.github.com> Date: Tue, 9 Sep 2025 15:39:47 +0200 Subject: [PATCH 196/208] RingBuffer: Make partial writing optional (#10302) --- esphome/core/ring_buffer.cpp | 8 ++++++-- esphome/core/ring_buffer.h | 3 ++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/esphome/core/ring_buffer.cpp b/esphome/core/ring_buffer.cpp index b77a02b2a7..6a2232599f 100644 --- a/esphome/core/ring_buffer.cpp +++ b/esphome/core/ring_buffer.cpp @@ -78,9 +78,13 @@ size_t RingBuffer::write(const void *data, size_t len) { return this->write_without_replacement(data, len, 0); } -size_t RingBuffer::write_without_replacement(const void *data, size_t len, TickType_t ticks_to_wait) { +size_t RingBuffer::write_without_replacement(const void *data, size_t len, TickType_t ticks_to_wait, + bool write_partial) { if (!xRingbufferSend(this->handle_, data, len, ticks_to_wait)) { - // Couldn't fit all the data, so only write what will fit + if (!write_partial) { + return 0; // Not enough space available and not allowed to write partial data + } + // Couldn't fit all the data, write what will fit size_t free = std::min(this->free(), len); if (xRingbufferSend(this->handle_, data, free, 0)) { return free; diff --git a/esphome/core/ring_buffer.h b/esphome/core/ring_buffer.h index bad96d3181..98a273781f 100644 --- a/esphome/core/ring_buffer.h +++ b/esphome/core/ring_buffer.h @@ -50,7 +50,8 @@ class RingBuffer { * @param ticks_to_wait Maximum number of FreeRTOS ticks to wait (default: 0) * @return Number of bytes written */ - size_t write_without_replacement(const void *data, size_t len, TickType_t ticks_to_wait = 0); + size_t write_without_replacement(const void *data, size_t len, TickType_t ticks_to_wait = 0, + bool write_partial = true); /** * @brief Returns the number of available bytes in the ring buffer. From 39212f0d7f09fed94b41a049e0e464b77f8eff31 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Tue, 9 Sep 2025 18:45:42 +0200 Subject: [PATCH 197/208] allow to implement show_logs as external component (#10523) --- esphome/__main__.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/esphome/__main__.py b/esphome/__main__.py index e1f683397f..280f491924 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -459,6 +459,13 @@ def upload_program( def show_logs(config: ConfigType, args: ArgsProtocol, devices: list[str]) -> int | None: + try: + module = importlib.import_module("esphome.components." + CORE.target_platform) + if getattr(module, "show_logs")(config, args, devices): + return 0 + except AttributeError: + pass + if "logger" not in config: raise EsphomeError("Logger is not configured!") From 01ff09064d4707d673d8ba74f537217283e9b720 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Tue, 9 Sep 2025 21:29:49 +0200 Subject: [PATCH 198/208] [nrf52] add more tests (#10591) --- tests/components/alarm_control_panel/test.nrf52-adafruit.yaml | 1 + tests/components/alarm_control_panel/test.nrf52-mcumgr.yaml | 1 + tests/components/binary_sensor/test.nrf52-adafruit.yaml | 1 + tests/components/binary_sensor/test.nrf52-mcumgr.yaml | 1 + tests/components/button/test.nrf52-adafruit.yaml | 1 + tests/components/button/test.nrf52-mcumgr.yaml | 1 + tests/components/duty_cycle/test.nrf52-adafruit.yaml | 1 + tests/components/duty_cycle/test.nrf52-mcumgr.yaml | 1 + tests/components/duty_time/test.nrf52-adafruit.yaml | 1 + tests/components/duty_time/test.nrf52-mcumgr.yaml | 1 + tests/components/esphome/test.nrf52-adafruit.yaml | 1 + tests/components/esphome/test.nrf52-mcumgr.yaml | 1 + tests/components/event/test.nrf52-adafruit.yaml | 1 + tests/components/event/test.nrf52-mcumgr.yaml | 1 + tests/components/interval/test.nrf52-adafruit.yaml | 1 + tests/components/interval/test.nrf52-mcumgr.yaml | 1 + tests/components/lock/test.nrf52-adafruit.yaml | 1 + tests/components/lock/test.nrf52-mcumgr.yaml | 1 + tests/components/power_supply/test.nrf52-adafruit.yaml | 1 + tests/components/power_supply/test.nrf52-mcumgr.yaml | 1 + tests/components/pulse_counter/test.nrf52-adafruit.yaml | 1 + tests/components/pulse_counter/test.nrf52-mcumgr.yaml | 1 + tests/components/pulse_meter/test.nrf52-adafruit.yaml | 1 + tests/components/pulse_meter/test.nrf52-mcumgr.yaml | 1 + tests/components/pulse_width/test.nrf52-adafruit.yaml | 1 + tests/components/pulse_width/test.nrf52-mcumgr.yaml | 1 + tests/components/switch/test.nrf52-adafruit.yaml | 1 + tests/components/switch/test.nrf52-mcumgr.yaml | 1 + tests/components/version/test.nrf52-adafruit.yaml | 1 + tests/components/version/test.nrf52-mcumgr.yaml | 1 + 30 files changed, 30 insertions(+) create mode 100644 tests/components/alarm_control_panel/test.nrf52-adafruit.yaml create mode 100644 tests/components/alarm_control_panel/test.nrf52-mcumgr.yaml create mode 100644 tests/components/binary_sensor/test.nrf52-adafruit.yaml create mode 100644 tests/components/binary_sensor/test.nrf52-mcumgr.yaml create mode 100644 tests/components/button/test.nrf52-adafruit.yaml create mode 100644 tests/components/button/test.nrf52-mcumgr.yaml create mode 100644 tests/components/duty_cycle/test.nrf52-adafruit.yaml create mode 100644 tests/components/duty_cycle/test.nrf52-mcumgr.yaml create mode 100644 tests/components/duty_time/test.nrf52-adafruit.yaml create mode 100644 tests/components/duty_time/test.nrf52-mcumgr.yaml create mode 100644 tests/components/esphome/test.nrf52-adafruit.yaml create mode 100644 tests/components/esphome/test.nrf52-mcumgr.yaml create mode 100644 tests/components/event/test.nrf52-adafruit.yaml create mode 100644 tests/components/event/test.nrf52-mcumgr.yaml create mode 100644 tests/components/interval/test.nrf52-adafruit.yaml create mode 100644 tests/components/interval/test.nrf52-mcumgr.yaml create mode 100644 tests/components/lock/test.nrf52-adafruit.yaml create mode 100644 tests/components/lock/test.nrf52-mcumgr.yaml create mode 100644 tests/components/power_supply/test.nrf52-adafruit.yaml create mode 100644 tests/components/power_supply/test.nrf52-mcumgr.yaml create mode 100644 tests/components/pulse_counter/test.nrf52-adafruit.yaml create mode 100644 tests/components/pulse_counter/test.nrf52-mcumgr.yaml create mode 100644 tests/components/pulse_meter/test.nrf52-adafruit.yaml create mode 100644 tests/components/pulse_meter/test.nrf52-mcumgr.yaml create mode 100644 tests/components/pulse_width/test.nrf52-adafruit.yaml create mode 100644 tests/components/pulse_width/test.nrf52-mcumgr.yaml create mode 100644 tests/components/switch/test.nrf52-adafruit.yaml create mode 100644 tests/components/switch/test.nrf52-mcumgr.yaml create mode 100644 tests/components/version/test.nrf52-adafruit.yaml create mode 100644 tests/components/version/test.nrf52-mcumgr.yaml diff --git a/tests/components/alarm_control_panel/test.nrf52-adafruit.yaml b/tests/components/alarm_control_panel/test.nrf52-adafruit.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/alarm_control_panel/test.nrf52-adafruit.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/alarm_control_panel/test.nrf52-mcumgr.yaml b/tests/components/alarm_control_panel/test.nrf52-mcumgr.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/alarm_control_panel/test.nrf52-mcumgr.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/binary_sensor/test.nrf52-adafruit.yaml b/tests/components/binary_sensor/test.nrf52-adafruit.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/binary_sensor/test.nrf52-adafruit.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/binary_sensor/test.nrf52-mcumgr.yaml b/tests/components/binary_sensor/test.nrf52-mcumgr.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/binary_sensor/test.nrf52-mcumgr.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/button/test.nrf52-adafruit.yaml b/tests/components/button/test.nrf52-adafruit.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/button/test.nrf52-adafruit.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/button/test.nrf52-mcumgr.yaml b/tests/components/button/test.nrf52-mcumgr.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/button/test.nrf52-mcumgr.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/duty_cycle/test.nrf52-adafruit.yaml b/tests/components/duty_cycle/test.nrf52-adafruit.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/duty_cycle/test.nrf52-adafruit.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/duty_cycle/test.nrf52-mcumgr.yaml b/tests/components/duty_cycle/test.nrf52-mcumgr.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/duty_cycle/test.nrf52-mcumgr.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/duty_time/test.nrf52-adafruit.yaml b/tests/components/duty_time/test.nrf52-adafruit.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/duty_time/test.nrf52-adafruit.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/duty_time/test.nrf52-mcumgr.yaml b/tests/components/duty_time/test.nrf52-mcumgr.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/duty_time/test.nrf52-mcumgr.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/esphome/test.nrf52-adafruit.yaml b/tests/components/esphome/test.nrf52-adafruit.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/esphome/test.nrf52-adafruit.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/esphome/test.nrf52-mcumgr.yaml b/tests/components/esphome/test.nrf52-mcumgr.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/esphome/test.nrf52-mcumgr.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/event/test.nrf52-adafruit.yaml b/tests/components/event/test.nrf52-adafruit.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/event/test.nrf52-adafruit.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/event/test.nrf52-mcumgr.yaml b/tests/components/event/test.nrf52-mcumgr.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/event/test.nrf52-mcumgr.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/interval/test.nrf52-adafruit.yaml b/tests/components/interval/test.nrf52-adafruit.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/interval/test.nrf52-adafruit.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/interval/test.nrf52-mcumgr.yaml b/tests/components/interval/test.nrf52-mcumgr.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/interval/test.nrf52-mcumgr.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/lock/test.nrf52-adafruit.yaml b/tests/components/lock/test.nrf52-adafruit.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/lock/test.nrf52-adafruit.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/lock/test.nrf52-mcumgr.yaml b/tests/components/lock/test.nrf52-mcumgr.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/lock/test.nrf52-mcumgr.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/power_supply/test.nrf52-adafruit.yaml b/tests/components/power_supply/test.nrf52-adafruit.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/power_supply/test.nrf52-adafruit.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/power_supply/test.nrf52-mcumgr.yaml b/tests/components/power_supply/test.nrf52-mcumgr.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/power_supply/test.nrf52-mcumgr.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/pulse_counter/test.nrf52-adafruit.yaml b/tests/components/pulse_counter/test.nrf52-adafruit.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/pulse_counter/test.nrf52-adafruit.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/pulse_counter/test.nrf52-mcumgr.yaml b/tests/components/pulse_counter/test.nrf52-mcumgr.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/pulse_counter/test.nrf52-mcumgr.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/pulse_meter/test.nrf52-adafruit.yaml b/tests/components/pulse_meter/test.nrf52-adafruit.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/pulse_meter/test.nrf52-adafruit.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/pulse_meter/test.nrf52-mcumgr.yaml b/tests/components/pulse_meter/test.nrf52-mcumgr.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/pulse_meter/test.nrf52-mcumgr.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/pulse_width/test.nrf52-adafruit.yaml b/tests/components/pulse_width/test.nrf52-adafruit.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/pulse_width/test.nrf52-adafruit.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/pulse_width/test.nrf52-mcumgr.yaml b/tests/components/pulse_width/test.nrf52-mcumgr.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/pulse_width/test.nrf52-mcumgr.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/switch/test.nrf52-adafruit.yaml b/tests/components/switch/test.nrf52-adafruit.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/switch/test.nrf52-adafruit.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/switch/test.nrf52-mcumgr.yaml b/tests/components/switch/test.nrf52-mcumgr.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/switch/test.nrf52-mcumgr.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/version/test.nrf52-adafruit.yaml b/tests/components/version/test.nrf52-adafruit.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/version/test.nrf52-adafruit.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/version/test.nrf52-mcumgr.yaml b/tests/components/version/test.nrf52-mcumgr.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/version/test.nrf52-mcumgr.yaml @@ -0,0 +1 @@ +<<: !include common.yaml From 8976ea243602db975a241c9d28b6513578a8df7f Mon Sep 17 00:00:00 2001 From: mrtoy-me <118446898+mrtoy-me@users.noreply.github.com> Date: Wed, 10 Sep 2025 05:31:06 +1000 Subject: [PATCH 199/208] [ms5611] remove delay in setup (#10658) --- esphome/components/ms5611/ms5611.cpp | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/esphome/components/ms5611/ms5611.cpp b/esphome/components/ms5611/ms5611.cpp index 8f8c05eb7d..5a7622e783 100644 --- a/esphome/components/ms5611/ms5611.cpp +++ b/esphome/components/ms5611/ms5611.cpp @@ -19,13 +19,14 @@ void MS5611Component::setup() { this->mark_failed(); return; } - delay(100); // NOLINT - for (uint8_t offset = 0; offset < 6; offset++) { - if (!this->read_byte_16(MS5611_CMD_READ_PROM + (offset * 2), &this->prom_[offset])) { - this->mark_failed(); - return; + this->set_timeout(100, [this]() { + for (uint8_t offset = 0; offset < 6; offset++) { + if (!this->read_byte_16(MS5611_CMD_READ_PROM + (offset * 2), &this->prom_[offset])) { + this->mark_failed(); + return; + } } - } + }); } void MS5611Component::dump_config() { ESP_LOGCONFIG(TAG, "MS5611:"); From cfb90b7b1830403bb88c9b7de141acd101c8d9e6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Sep 2025 15:15:53 -0500 Subject: [PATCH 200/208] Bump pytest-cov from 6.3.0 to 7.0.0 (#10660) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 1760af75ba..bae9246768 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -6,7 +6,7 @@ pre-commit # Unit tests pytest==8.4.2 -pytest-cov==6.3.0 +pytest-cov==7.0.0 pytest-mock==3.15.0 pytest-asyncio==1.1.0 pytest-xdist==3.8.0 From e972e1f8c21735d5bceaad51efd00932ed4f0653 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Sep 2025 20:33:36 +0000 Subject: [PATCH 201/208] Bump aioesphomeapi from 40.0.2 to 40.1.0 (#10662) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 6021b6de70..0b9a37005d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile esptool==5.0.2 click==8.1.7 esphome-dashboard==20250904.0 -aioesphomeapi==40.0.2 +aioesphomeapi==40.1.0 zeroconf==0.147.2 puremagic==1.30 ruamel.yaml==0.18.15 # dashboard_import From 422d2097864235f7ef10a56efea443b2b699f2db Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 9 Sep 2025 15:54:50 -0500 Subject: [PATCH 202/208] [api] Add timezone support to GetTimeResponse for automatic timezone synchronization (#10661) --- esphome/components/api/api.proto | 1 + esphome/components/api/api_connection.cpp | 8 +++++++- esphome/components/api/api_pb2.cpp | 20 ++++++++++++++++++-- esphome/components/api/api_pb2.h | 6 +++++- esphome/components/api/api_pb2_dump.cpp | 12 +++++++++++- 5 files changed, 42 insertions(+), 5 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 9707e714e7..208187d598 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -818,6 +818,7 @@ message GetTimeResponse { option (no_delay) = true; fixed32 epoch_seconds = 1; + string timezone = 2; } // ==================== USER-DEFINES SERVICES ==================== diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 02b1d61368..99a0bc9044 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1070,8 +1070,14 @@ void APIConnection::camera_image(const CameraImageRequest &msg) { #ifdef USE_HOMEASSISTANT_TIME void APIConnection::on_get_time_response(const GetTimeResponse &value) { - if (homeassistant::global_homeassistant_time != nullptr) + if (homeassistant::global_homeassistant_time != nullptr) { homeassistant::global_homeassistant_time->set_epoch_time(value.epoch_seconds); +#ifdef USE_TIME_TIMEZONE + if (!value.timezone.empty() && value.timezone != homeassistant::global_homeassistant_time->get_timezone()) { + homeassistant::global_homeassistant_time->set_timezone(value.timezone); + } +#endif + } } #endif diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index de60ed3fdb..022ac55cf3 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -901,6 +901,16 @@ bool HomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDel return true; } #endif +bool GetTimeResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 2: + this->timezone = value.as_string(); + break; + default: + return false; + } + return true; +} bool GetTimeResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { switch (field_id) { case 1: @@ -911,8 +921,14 @@ bool GetTimeResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { } return true; } -void GetTimeResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_fixed32(1, this->epoch_seconds); } -void GetTimeResponse::calculate_size(ProtoSize &size) const { size.add_fixed32(1, this->epoch_seconds); } +void GetTimeResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_fixed32(1, this->epoch_seconds); + buffer.encode_string(2, this->timezone_ref_); +} +void GetTimeResponse::calculate_size(ProtoSize &size) const { + size.add_fixed32(1, this->epoch_seconds); + size.add_length(1, this->timezone_ref_.size()); +} #ifdef USE_API_SERVICES void ListEntitiesServicesArgument::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(1, this->name_ref_); diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 3f2c2ea763..fd124e7bfe 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -1174,11 +1174,14 @@ class GetTimeRequest final : public ProtoMessage { class GetTimeResponse final : public ProtoDecodableMessage { public: static constexpr uint8_t MESSAGE_TYPE = 37; - static constexpr uint8_t ESTIMATED_SIZE = 5; + static constexpr uint8_t ESTIMATED_SIZE = 14; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "get_time_response"; } #endif uint32_t epoch_seconds{0}; + std::string timezone{}; + StringRef timezone_ref_{}; + void set_timezone(const StringRef &ref) { this->timezone_ref_ = ref; } void encode(ProtoWriteBuffer buffer) const override; void calculate_size(ProtoSize &size) const override; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -1187,6 +1190,7 @@ class GetTimeResponse final : public ProtoDecodableMessage { protected: bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; }; #ifdef USE_API_SERVICES class ListEntitiesServicesArgument final : public ProtoMessage { diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index 1d7d315419..9795999953 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -1110,7 +1110,17 @@ void HomeAssistantStateResponse::dump_to(std::string &out) const { } #endif void GetTimeRequest::dump_to(std::string &out) const { out.append("GetTimeRequest {}"); } -void GetTimeResponse::dump_to(std::string &out) const { dump_field(out, "epoch_seconds", this->epoch_seconds); } +void GetTimeResponse::dump_to(std::string &out) const { + MessageDumpHelper helper(out, "GetTimeResponse"); + dump_field(out, "epoch_seconds", this->epoch_seconds); + out.append(" timezone: "); + if (!this->timezone_ref_.empty()) { + out.append("'").append(this->timezone_ref_.c_str()).append("'"); + } else { + out.append("'").append(this->timezone).append("'"); + } + out.append("\n"); +} #ifdef USE_API_SERVICES void ListEntitiesServicesArgument::dump_to(std::string &out) const { MessageDumpHelper helper(out, "ListEntitiesServicesArgument"); From e218f16f0fc538fb5f03fbef7fa281ddd986266f Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 9 Sep 2025 19:06:59 -0400 Subject: [PATCH 203/208] Allow both files and directories to be passed to update-all (#10575) Co-authored-by: J. Nick Koston --- esphome/util.py | 15 ++-- tests/unit_tests/test_util.py | 143 ++++++++++++++++++++++++++++++++++ 2 files changed, 152 insertions(+), 6 deletions(-) create mode 100644 tests/unit_tests/test_util.py diff --git a/esphome/util.py b/esphome/util.py index 6362260fde..23a66be4eb 100644 --- a/esphome/util.py +++ b/esphome/util.py @@ -272,12 +272,15 @@ class OrderedDict(collections.OrderedDict): return dict(self).__repr__() -def list_yaml_files(folders: list[str]) -> list[str]: - files = filter_yaml_files( - [os.path.join(folder, p) for folder in folders for p in os.listdir(folder)] - ) - files.sort() - return files +def list_yaml_files(configs: list[str]) -> list[str]: + files: list[str] = [] + for config in configs: + if os.path.isfile(config): + files.append(config) + else: + files.extend(os.path.join(config, p) for p in os.listdir(config)) + files = filter_yaml_files(files) + return sorted(files) def filter_yaml_files(files: list[str]) -> list[str]: diff --git a/tests/unit_tests/test_util.py b/tests/unit_tests/test_util.py new file mode 100644 index 0000000000..74d6a74709 --- /dev/null +++ b/tests/unit_tests/test_util.py @@ -0,0 +1,143 @@ +"""Tests for esphome.util module.""" + +from pathlib import Path + +import pytest + +from esphome import util + + +def test_list_yaml_files_with_files_and_directories(tmp_path: Path) -> None: + """Test that list_yaml_files handles both files and directories.""" + # Create directory structure + dir1 = tmp_path / "configs" + dir1.mkdir() + dir2 = tmp_path / "more_configs" + dir2.mkdir() + + # Create YAML files in directories + (dir1 / "config1.yaml").write_text("test: 1") + (dir1 / "config2.yml").write_text("test: 2") + (dir1 / "not_yaml.txt").write_text("not yaml") + + (dir2 / "config3.yaml").write_text("test: 3") + + # Create standalone YAML files + standalone1 = tmp_path / "standalone.yaml" + standalone1.write_text("test: 4") + standalone2 = tmp_path / "another.yml" + standalone2.write_text("test: 5") + + # Test with mixed input (directories and files) + configs = [ + str(dir1), + str(standalone1), + str(dir2), + str(standalone2), + ] + + result = util.list_yaml_files(configs) + + # Should include all YAML files but not the .txt file + assert set(result) == { + str(dir1 / "config1.yaml"), + str(dir1 / "config2.yml"), + str(dir2 / "config3.yaml"), + str(standalone1), + str(standalone2), + } + # Check that results are sorted + assert result == sorted(result) + + +def test_list_yaml_files_only_directories(tmp_path: Path) -> None: + """Test list_yaml_files with only directories.""" + dir1 = tmp_path / "dir1" + dir1.mkdir() + dir2 = tmp_path / "dir2" + dir2.mkdir() + + (dir1 / "a.yaml").write_text("test: a") + (dir1 / "b.yml").write_text("test: b") + (dir2 / "c.yaml").write_text("test: c") + + result = util.list_yaml_files([str(dir1), str(dir2)]) + + assert set(result) == { + str(dir1 / "a.yaml"), + str(dir1 / "b.yml"), + str(dir2 / "c.yaml"), + } + assert result == sorted(result) + + +def test_list_yaml_files_only_files(tmp_path: Path) -> None: + """Test list_yaml_files with only files.""" + file1 = tmp_path / "file1.yaml" + file2 = tmp_path / "file2.yml" + file3 = tmp_path / "file3.yaml" + non_yaml = tmp_path / "not_yaml.json" + + file1.write_text("test: 1") + file2.write_text("test: 2") + file3.write_text("test: 3") + non_yaml.write_text("{}") + + # Include a non-YAML file to test filtering + result = util.list_yaml_files( + [ + str(file1), + str(file2), + str(file3), + str(non_yaml), + ] + ) + + assert set(result) == { + str(file1), + str(file2), + str(file3), + } + assert result == sorted(result) + + +def test_list_yaml_files_empty_directory(tmp_path: Path) -> None: + """Test list_yaml_files with an empty directory.""" + empty_dir = tmp_path / "empty" + empty_dir.mkdir() + + result = util.list_yaml_files([str(empty_dir)]) + + assert result == [] + + +def test_list_yaml_files_nonexistent_path(tmp_path: Path) -> None: + """Test list_yaml_files with a nonexistent path raises an error.""" + nonexistent = tmp_path / "nonexistent" + existing = tmp_path / "existing.yaml" + existing.write_text("test: 1") + + # Should raise an error for non-existent directory + with pytest.raises(FileNotFoundError): + util.list_yaml_files([str(nonexistent), str(existing)]) + + +def test_list_yaml_files_mixed_extensions(tmp_path: Path) -> None: + """Test that both .yaml and .yml extensions are recognized.""" + dir1 = tmp_path / "configs" + dir1.mkdir() + + yaml_file = dir1 / "config.yaml" + yml_file = dir1 / "config.yml" + other_file = dir1 / "config.txt" + + yaml_file.write_text("test: yaml") + yml_file.write_text("test: yml") + other_file.write_text("test: txt") + + result = util.list_yaml_files([str(dir1)]) + + assert set(result) == { + str(yaml_file), + str(yml_file), + } From d9f625e5c889ce0f91b73998e8a2ba3e17a2fda8 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Tue, 9 Sep 2025 20:24:50 -0500 Subject: [PATCH 204/208] [thermostat] General clean-up, optimization, properly support "auto" mode (#10561) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/thermostat/climate.py | 18 +- .../thermostat/thermostat_climate.cpp | 313 ++++++++++-------- .../thermostat/thermostat_climate.h | 73 ++-- esphome/const.py | 1 + 4 files changed, 220 insertions(+), 185 deletions(-) diff --git a/esphome/components/thermostat/climate.py b/esphome/components/thermostat/climate.py index 5d0d9442e8..57abbc67b9 100644 --- a/esphome/components/thermostat/climate.py +++ b/esphome/components/thermostat/climate.py @@ -32,6 +32,7 @@ from esphome.const import ( CONF_FAN_WITH_COOLING, CONF_FAN_WITH_HEATING, CONF_HEAT_ACTION, + CONF_HEAT_COOL_MODE, CONF_HEAT_DEADBAND, CONF_HEAT_MODE, CONF_HEAT_OVERRUN, @@ -150,7 +151,7 @@ def generate_comparable_preset(config, name): def validate_thermostat(config): # verify corresponding action(s) exist(s) for any defined climate mode or action requirements = { - CONF_AUTO_MODE: [ + CONF_HEAT_COOL_MODE: [ CONF_COOL_ACTION, CONF_HEAT_ACTION, CONF_MIN_COOLING_OFF_TIME, @@ -540,6 +541,9 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_FAN_ONLY_MODE): automation.validate_automation( single=True ), + cv.Optional(CONF_HEAT_COOL_MODE): automation.validate_automation( + single=True + ), cv.Optional(CONF_HEAT_MODE): automation.validate_automation(single=True), cv.Optional(CONF_OFF_MODE): automation.validate_automation(single=True), cv.Optional(CONF_FAN_MODE_ON_ACTION): automation.validate_automation( @@ -644,7 +648,6 @@ async def to_code(config): var = await climate.new_climate(config) await cg.register_component(var, config) - heat_cool_mode_available = CONF_HEAT_ACTION in config and CONF_COOL_ACTION in config two_points_available = CONF_HEAT_ACTION in config and ( CONF_COOL_ACTION in config or (config[CONF_FAN_ONLY_COOLING] and CONF_FAN_ONLY_ACTION in config) @@ -739,11 +742,6 @@ async def to_code(config): var.get_idle_action_trigger(), [], config[CONF_IDLE_ACTION] ) - if heat_cool_mode_available is True: - cg.add(var.set_supports_heat_cool(True)) - else: - cg.add(var.set_supports_heat_cool(False)) - if CONF_COOL_ACTION in config: await automation.build_automation( var.get_cool_action_trigger(), [], config[CONF_COOL_ACTION] @@ -780,6 +778,7 @@ async def to_code(config): await automation.build_automation( var.get_auto_mode_trigger(), [], config[CONF_AUTO_MODE] ) + cg.add(var.set_supports_auto(True)) if CONF_COOL_MODE in config: await automation.build_automation( var.get_cool_mode_trigger(), [], config[CONF_COOL_MODE] @@ -800,6 +799,11 @@ async def to_code(config): var.get_heat_mode_trigger(), [], config[CONF_HEAT_MODE] ) cg.add(var.set_supports_heat(True)) + if CONF_HEAT_COOL_MODE in config: + await automation.build_automation( + var.get_heat_cool_mode_trigger(), [], config[CONF_HEAT_COOL_MODE] + ) + cg.add(var.set_supports_heat_cool(True)) if CONF_OFF_MODE in config: await automation.build_automation( var.get_off_mode_trigger(), [], config[CONF_OFF_MODE] diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index 404e585aff..f05730b369 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -1,4 +1,6 @@ #include "thermostat_climate.h" +#include "esphome/core/application.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" namespace esphome { @@ -64,7 +66,7 @@ void ThermostatClimate::setup() { void ThermostatClimate::loop() { for (auto &timer : this->timer_) { - if (timer.active && (timer.started + timer.time < millis())) { + if (timer.active && (timer.started + timer.time < App.get_loop_component_start_time())) { timer.active = false; timer.func(); } @@ -127,26 +129,35 @@ bool ThermostatClimate::hysteresis_valid() { return true; } +bool ThermostatClimate::limit_setpoints_for_heat_cool() { + return this->mode == climate::CLIMATE_MODE_HEAT_COOL || + (this->mode == climate::CLIMATE_MODE_AUTO && this->supports_heat_cool_); +} + void ThermostatClimate::validate_target_temperature() { if (std::isnan(this->target_temperature)) { + // default to the midpoint between visual min and max this->target_temperature = ((this->get_traits().get_visual_max_temperature() - this->get_traits().get_visual_min_temperature()) / 2) + this->get_traits().get_visual_min_temperature(); } else { // target_temperature must be between the visual minimum and the visual maximum - if (this->target_temperature < this->get_traits().get_visual_min_temperature()) - this->target_temperature = this->get_traits().get_visual_min_temperature(); - if (this->target_temperature > this->get_traits().get_visual_max_temperature()) - this->target_temperature = this->get_traits().get_visual_max_temperature(); + this->target_temperature = clamp(this->target_temperature, this->get_traits().get_visual_min_temperature(), + this->get_traits().get_visual_max_temperature()); } } -void ThermostatClimate::validate_target_temperatures() { - if (this->supports_two_points_) { +void ThermostatClimate::validate_target_temperatures(const bool pin_target_temperature_high) { + if (!this->supports_two_points_) { + this->validate_target_temperature(); + } else if (pin_target_temperature_high) { + // if target_temperature_high is set less than target_temperature_low, move down target_temperature_low this->validate_target_temperature_low(); this->validate_target_temperature_high(); } else { - this->validate_target_temperature(); + // if target_temperature_low is set greater than target_temperature_high, move up target_temperature_high + this->validate_target_temperature_high(); + this->validate_target_temperature_low(); } } @@ -154,18 +165,13 @@ void ThermostatClimate::validate_target_temperature_low() { if (std::isnan(this->target_temperature_low)) { this->target_temperature_low = this->get_traits().get_visual_min_temperature(); } else { - // target_temperature_low must not be lower than the visual minimum - if (this->target_temperature_low < this->get_traits().get_visual_min_temperature()) - this->target_temperature_low = this->get_traits().get_visual_min_temperature(); - // target_temperature_low must not be greater than the visual maximum minus set_point_minimum_differential_ - if (this->target_temperature_low > - this->get_traits().get_visual_max_temperature() - this->set_point_minimum_differential_) { - this->target_temperature_low = - this->get_traits().get_visual_max_temperature() - this->set_point_minimum_differential_; - } - // if target_temperature_low is set greater than target_temperature_high, move up target_temperature_high - if (this->target_temperature_low > this->target_temperature_high - this->set_point_minimum_differential_) - this->target_temperature_high = this->target_temperature_low + this->set_point_minimum_differential_; + float target_temperature_low_upper_limit = + this->limit_setpoints_for_heat_cool() + ? clamp(this->target_temperature_high - this->set_point_minimum_differential_, + this->get_traits().get_visual_min_temperature(), this->get_traits().get_visual_max_temperature()) + : this->get_traits().get_visual_max_temperature(); + this->target_temperature_low = clamp(this->target_temperature_low, this->get_traits().get_visual_min_temperature(), + target_temperature_low_upper_limit); } } @@ -173,62 +179,64 @@ void ThermostatClimate::validate_target_temperature_high() { if (std::isnan(this->target_temperature_high)) { this->target_temperature_high = this->get_traits().get_visual_max_temperature(); } else { - // target_temperature_high must not be lower than the visual maximum - if (this->target_temperature_high > this->get_traits().get_visual_max_temperature()) - this->target_temperature_high = this->get_traits().get_visual_max_temperature(); - // target_temperature_high must not be lower than the visual minimum plus set_point_minimum_differential_ - if (this->target_temperature_high < - this->get_traits().get_visual_min_temperature() + this->set_point_minimum_differential_) { - this->target_temperature_high = - this->get_traits().get_visual_min_temperature() + this->set_point_minimum_differential_; - } - // if target_temperature_high is set less than target_temperature_low, move down target_temperature_low - if (this->target_temperature_high < this->target_temperature_low + this->set_point_minimum_differential_) - this->target_temperature_low = this->target_temperature_high - this->set_point_minimum_differential_; + float target_temperature_high_lower_limit = + this->limit_setpoints_for_heat_cool() + ? clamp(this->target_temperature_low + this->set_point_minimum_differential_, + this->get_traits().get_visual_min_temperature(), this->get_traits().get_visual_max_temperature()) + : this->get_traits().get_visual_min_temperature(); + this->target_temperature_high = clamp(this->target_temperature_high, target_temperature_high_lower_limit, + this->get_traits().get_visual_max_temperature()); } } void ThermostatClimate::control(const climate::ClimateCall &call) { + bool target_temperature_high_changed = false; + if (call.get_preset().has_value()) { // setup_complete_ blocks modifying/resetting the temps immediately after boot if (this->setup_complete_) { - this->change_preset_(*call.get_preset()); + this->change_preset_(call.get_preset().value()); } else { - this->preset = *call.get_preset(); + this->preset = call.get_preset().value(); } } if (call.get_custom_preset().has_value()) { // setup_complete_ blocks modifying/resetting the temps immediately after boot if (this->setup_complete_) { - this->change_custom_preset_(*call.get_custom_preset()); + this->change_custom_preset_(call.get_custom_preset().value()); } else { - this->custom_preset = *call.get_custom_preset(); + this->custom_preset = call.get_custom_preset().value(); } } - if (call.get_mode().has_value()) - this->mode = *call.get_mode(); - if (call.get_fan_mode().has_value()) - this->fan_mode = *call.get_fan_mode(); - if (call.get_swing_mode().has_value()) - this->swing_mode = *call.get_swing_mode(); + if (call.get_mode().has_value()) { + this->mode = call.get_mode().value(); + } + if (call.get_fan_mode().has_value()) { + this->fan_mode = call.get_fan_mode().value(); + } + if (call.get_swing_mode().has_value()) { + this->swing_mode = call.get_swing_mode().value(); + } if (this->supports_two_points_) { if (call.get_target_temperature_low().has_value()) { - this->target_temperature_low = *call.get_target_temperature_low(); - validate_target_temperature_low(); + this->target_temperature_low = call.get_target_temperature_low().value(); } if (call.get_target_temperature_high().has_value()) { - this->target_temperature_high = *call.get_target_temperature_high(); - validate_target_temperature_high(); + target_temperature_high_changed = this->target_temperature_high != call.get_target_temperature_high().value(); + this->target_temperature_high = call.get_target_temperature_high().value(); } + // ensure the two set points are valid and adjust one of them if necessary + this->validate_target_temperatures(target_temperature_high_changed || + (this->prev_mode_ == climate::CLIMATE_MODE_COOL)); } else { if (call.get_target_temperature().has_value()) { - this->target_temperature = *call.get_target_temperature(); - validate_target_temperature(); + this->target_temperature = call.get_target_temperature().value(); + this->validate_target_temperature(); } } // make any changes happen - refresh(); + this->refresh(); } climate::ClimateTraits ThermostatClimate::traits() { @@ -237,47 +245,47 @@ climate::ClimateTraits ThermostatClimate::traits() { if (this->humidity_sensor_ != nullptr) traits.set_supports_current_humidity(true); - if (supports_auto_) + if (this->supports_auto_) traits.add_supported_mode(climate::CLIMATE_MODE_AUTO); - if (supports_heat_cool_) + if (this->supports_heat_cool_) traits.add_supported_mode(climate::CLIMATE_MODE_HEAT_COOL); - if (supports_cool_) + if (this->supports_cool_) traits.add_supported_mode(climate::CLIMATE_MODE_COOL); - if (supports_dry_) + if (this->supports_dry_) traits.add_supported_mode(climate::CLIMATE_MODE_DRY); - if (supports_fan_only_) + if (this->supports_fan_only_) traits.add_supported_mode(climate::CLIMATE_MODE_FAN_ONLY); - if (supports_heat_) + if (this->supports_heat_) traits.add_supported_mode(climate::CLIMATE_MODE_HEAT); - if (supports_fan_mode_on_) + if (this->supports_fan_mode_on_) traits.add_supported_fan_mode(climate::CLIMATE_FAN_ON); - if (supports_fan_mode_off_) + if (this->supports_fan_mode_off_) traits.add_supported_fan_mode(climate::CLIMATE_FAN_OFF); - if (supports_fan_mode_auto_) + if (this->supports_fan_mode_auto_) traits.add_supported_fan_mode(climate::CLIMATE_FAN_AUTO); - if (supports_fan_mode_low_) + if (this->supports_fan_mode_low_) traits.add_supported_fan_mode(climate::CLIMATE_FAN_LOW); - if (supports_fan_mode_medium_) + if (this->supports_fan_mode_medium_) traits.add_supported_fan_mode(climate::CLIMATE_FAN_MEDIUM); - if (supports_fan_mode_high_) + if (this->supports_fan_mode_high_) traits.add_supported_fan_mode(climate::CLIMATE_FAN_HIGH); - if (supports_fan_mode_middle_) + if (this->supports_fan_mode_middle_) traits.add_supported_fan_mode(climate::CLIMATE_FAN_MIDDLE); - if (supports_fan_mode_focus_) + if (this->supports_fan_mode_focus_) traits.add_supported_fan_mode(climate::CLIMATE_FAN_FOCUS); - if (supports_fan_mode_diffuse_) + if (this->supports_fan_mode_diffuse_) traits.add_supported_fan_mode(climate::CLIMATE_FAN_DIFFUSE); - if (supports_fan_mode_quiet_) + if (this->supports_fan_mode_quiet_) traits.add_supported_fan_mode(climate::CLIMATE_FAN_QUIET); - if (supports_swing_mode_both_) + if (this->supports_swing_mode_both_) traits.add_supported_swing_mode(climate::CLIMATE_SWING_BOTH); - if (supports_swing_mode_horizontal_) + if (this->supports_swing_mode_horizontal_) traits.add_supported_swing_mode(climate::CLIMATE_SWING_HORIZONTAL); - if (supports_swing_mode_off_) + if (this->supports_swing_mode_off_) traits.add_supported_swing_mode(climate::CLIMATE_SWING_OFF); - if (supports_swing_mode_vertical_) + if (this->supports_swing_mode_vertical_) traits.add_supported_swing_mode(climate::CLIMATE_SWING_VERTICAL); for (auto &it : this->preset_config_) { @@ -300,13 +308,13 @@ climate::ClimateAction ThermostatClimate::compute_action_(const bool ignore_time } // do not change the action if an "ON" timer is running if ((!ignore_timers) && - (timer_active_(thermostat::TIMER_IDLE_ON) || timer_active_(thermostat::TIMER_COOLING_ON) || - timer_active_(thermostat::TIMER_FANNING_ON) || timer_active_(thermostat::TIMER_HEATING_ON))) { + (this->timer_active_(thermostat::TIMER_IDLE_ON) || this->timer_active_(thermostat::TIMER_COOLING_ON) || + this->timer_active_(thermostat::TIMER_FANNING_ON) || this->timer_active_(thermostat::TIMER_HEATING_ON))) { return this->action; } // ensure set point(s) is/are valid before computing the action - this->validate_target_temperatures(); + this->validate_target_temperatures(this->prev_mode_ == climate::CLIMATE_MODE_COOL); // everything has been validated so we can now safely compute the action switch (this->mode) { // if the climate mode is OFF then the climate action must be OFF @@ -340,6 +348,22 @@ climate::ClimateAction ThermostatClimate::compute_action_(const bool ignore_time target_action = climate::CLIMATE_ACTION_HEATING; } break; + case climate::CLIMATE_MODE_AUTO: + if (this->supports_two_points_) { + if (this->cooling_required_() && this->heating_required_()) { + // this is bad and should never happen, so just stop. + // target_action = climate::CLIMATE_ACTION_IDLE; + } else if (this->cooling_required_()) { + target_action = climate::CLIMATE_ACTION_COOLING; + } else if (this->heating_required_()) { + target_action = climate::CLIMATE_ACTION_HEATING; + } + } else if (this->supports_cool_ && this->cooling_required_()) { + target_action = climate::CLIMATE_ACTION_COOLING; + } else if (this->supports_heat_ && this->heating_required_()) { + target_action = climate::CLIMATE_ACTION_HEATING; + } + break; default: break; } @@ -362,7 +386,7 @@ climate::ClimateAction ThermostatClimate::compute_supplemental_action_() { } // ensure set point(s) is/are valid before computing the action - this->validate_target_temperatures(); + this->validate_target_temperatures(this->prev_mode_ == climate::CLIMATE_MODE_COOL); // everything has been validated so we can now safely compute the action switch (this->mode) { // if the climate mode is OFF then the climate action must be OFF @@ -653,13 +677,13 @@ void ThermostatClimate::switch_to_mode_(climate::ClimateMode mode, bool publish_ this->prev_mode_trigger_->stop_action(); this->prev_mode_trigger_ = nullptr; } - Trigger<> *trig = this->auto_mode_trigger_; + Trigger<> *trig = this->off_mode_trigger_; switch (mode) { - case climate::CLIMATE_MODE_OFF: - trig = this->off_mode_trigger_; + case climate::CLIMATE_MODE_AUTO: + trig = this->auto_mode_trigger_; break; case climate::CLIMATE_MODE_HEAT_COOL: - // trig = this->auto_mode_trigger_; + trig = this->heat_cool_mode_trigger_; break; case climate::CLIMATE_MODE_COOL: trig = this->cool_mode_trigger_; @@ -673,11 +697,12 @@ void ThermostatClimate::switch_to_mode_(climate::ClimateMode mode, bool publish_ case climate::CLIMATE_MODE_DRY: trig = this->dry_mode_trigger_; break; + case climate::CLIMATE_MODE_OFF: default: // we cannot report an invalid mode back to HA (even if it asked for one) // and must assume some valid value - mode = climate::CLIMATE_MODE_HEAT_COOL; - // trig = this->auto_mode_trigger_; + mode = climate::CLIMATE_MODE_OFF; + // trig = this->off_mode_trigger_; } if (trig != nullptr) { trig->trigger(); @@ -685,8 +710,9 @@ void ThermostatClimate::switch_to_mode_(climate::ClimateMode mode, bool publish_ this->mode = mode; this->prev_mode_ = mode; this->prev_mode_trigger_ = trig; - if (publish_state) + if (publish_state) { this->publish_state(); + } } void ThermostatClimate::switch_to_swing_mode_(climate::ClimateSwingMode swing_mode, bool publish_state) { @@ -958,37 +984,25 @@ bool ThermostatClimate::supplemental_heating_required_() { (this->supplemental_action_ == climate::CLIMATE_ACTION_HEATING)); } -void ThermostatClimate::dump_preset_config_(const char *preset_name, const ThermostatClimateTargetTempConfig &config, - bool is_default_preset) { - ESP_LOGCONFIG(TAG, " %s Is Default: %s", preset_name, YESNO(is_default_preset)); - +void ThermostatClimate::dump_preset_config_(const char *preset_name, const ThermostatClimateTargetTempConfig &config) { if (this->supports_heat_) { - if (this->supports_two_points_) { - ESP_LOGCONFIG(TAG, " %s Default Target Temperature Low: %.1f°C", preset_name, - config.default_temperature_low); - } else { - ESP_LOGCONFIG(TAG, " %s Default Target Temperature Low: %.1f°C", preset_name, config.default_temperature); - } + ESP_LOGCONFIG(TAG, " Default Target Temperature Low: %.1f°C", + this->supports_two_points_ ? config.default_temperature_low : config.default_temperature); } if ((this->supports_cool_) || (this->supports_fan_only_)) { - if (this->supports_two_points_) { - ESP_LOGCONFIG(TAG, " %s Default Target Temperature High: %.1f°C", preset_name, - config.default_temperature_high); - } else { - ESP_LOGCONFIG(TAG, " %s Default Target Temperature High: %.1f°C", preset_name, config.default_temperature); - } + ESP_LOGCONFIG(TAG, " Default Target Temperature High: %.1f°C", + this->supports_two_points_ ? config.default_temperature_high : config.default_temperature); } if (config.mode_.has_value()) { - ESP_LOGCONFIG(TAG, " %s Default Mode: %s", preset_name, - LOG_STR_ARG(climate::climate_mode_to_string(*config.mode_))); + ESP_LOGCONFIG(TAG, " Default Mode: %s", LOG_STR_ARG(climate::climate_mode_to_string(*config.mode_))); } if (config.fan_mode_.has_value()) { - ESP_LOGCONFIG(TAG, " %s Default Fan Mode: %s", preset_name, + ESP_LOGCONFIG(TAG, " Default Fan Mode: %s", LOG_STR_ARG(climate::climate_fan_mode_to_string(*config.fan_mode_))); } if (config.swing_mode_.has_value()) { - ESP_LOGCONFIG(TAG, " %s Default Swing Mode: %s", preset_name, + ESP_LOGCONFIG(TAG, " Default Swing Mode: %s", LOG_STR_ARG(climate::climate_swing_mode_to_string(*config.swing_mode_))); } } @@ -1106,6 +1120,7 @@ ThermostatClimate::ThermostatClimate() heat_action_trigger_(new Trigger<>()), supplemental_heat_action_trigger_(new Trigger<>()), heat_mode_trigger_(new Trigger<>()), + heat_cool_mode_trigger_(new Trigger<>()), auto_mode_trigger_(new Trigger<>()), idle_action_trigger_(new Trigger<>()), off_mode_trigger_(new Trigger<>()), @@ -1274,6 +1289,7 @@ Trigger<> *ThermostatClimate::get_cool_mode_trigger() const { return this->cool_ Trigger<> *ThermostatClimate::get_dry_mode_trigger() const { return this->dry_mode_trigger_; } Trigger<> *ThermostatClimate::get_fan_only_mode_trigger() const { return this->fan_only_mode_trigger_; } Trigger<> *ThermostatClimate::get_heat_mode_trigger() const { return this->heat_mode_trigger_; } +Trigger<> *ThermostatClimate::get_heat_cool_mode_trigger() const { return this->heat_cool_mode_trigger_; } Trigger<> *ThermostatClimate::get_off_mode_trigger() const { return this->off_mode_trigger_; } Trigger<> *ThermostatClimate::get_fan_mode_on_trigger() const { return this->fan_mode_on_trigger_; } Trigger<> *ThermostatClimate::get_fan_mode_off_trigger() const { return this->fan_mode_off_trigger_; } @@ -1295,52 +1311,55 @@ Trigger<> *ThermostatClimate::get_preset_change_trigger() const { return this->p void ThermostatClimate::dump_config() { LOG_CLIMATE("", "Thermostat", this); + ESP_LOGCONFIG(TAG, + " On boot, restore from: %s\n" + " Use Start-up Delay: %s", + this->on_boot_restore_from_ == thermostat::DEFAULT_PRESET ? "DEFAULT_PRESET" : "MEMORY", + YESNO(this->use_startup_delay_)); if (this->supports_two_points_) { ESP_LOGCONFIG(TAG, " Minimum Set Point Differential: %.1f°C", this->set_point_minimum_differential_); } - ESP_LOGCONFIG(TAG, " Use Start-up Delay: %s", YESNO(this->use_startup_delay_)); if (this->supports_cool_) { ESP_LOGCONFIG(TAG, " Cooling Parameters:\n" " Deadband: %.1f°C\n" - " Overrun: %.1f°C", - this->cooling_deadband_, this->cooling_overrun_); - if ((this->supplemental_cool_delta_ > 0) || (this->timer_duration_(thermostat::TIMER_COOLING_MAX_RUN_TIME) > 0)) { - ESP_LOGCONFIG(TAG, - " Supplemental Delta: %.1f°C\n" - " Maximum Run Time: %" PRIu32 "s", - this->supplemental_cool_delta_, - this->timer_duration_(thermostat::TIMER_COOLING_MAX_RUN_TIME) / 1000); - } - ESP_LOGCONFIG(TAG, + " Overrun: %.1f°C\n" " Minimum Off Time: %" PRIu32 "s\n" " Minimum Run Time: %" PRIu32 "s", + this->cooling_deadband_, this->cooling_overrun_, this->timer_duration_(thermostat::TIMER_COOLING_OFF) / 1000, this->timer_duration_(thermostat::TIMER_COOLING_ON) / 1000); + if ((this->supplemental_cool_delta_ > 0) || (this->timer_duration_(thermostat::TIMER_COOLING_MAX_RUN_TIME) > 0)) { + ESP_LOGCONFIG(TAG, + " Maximum Run Time: %" PRIu32 "s\n" + " Supplemental Delta: %.1f°C", + this->timer_duration_(thermostat::TIMER_COOLING_MAX_RUN_TIME) / 1000, + this->supplemental_cool_delta_); + } } if (this->supports_heat_) { ESP_LOGCONFIG(TAG, " Heating Parameters:\n" " Deadband: %.1f°C\n" - " Overrun: %.1f°C", - this->heating_deadband_, this->heating_overrun_); - if ((this->supplemental_heat_delta_ > 0) || (this->timer_duration_(thermostat::TIMER_HEATING_MAX_RUN_TIME) > 0)) { - ESP_LOGCONFIG(TAG, - " Supplemental Delta: %.1f°C\n" - " Maximum Run Time: %" PRIu32 "s", - this->supplemental_heat_delta_, - this->timer_duration_(thermostat::TIMER_HEATING_MAX_RUN_TIME) / 1000); - } - ESP_LOGCONFIG(TAG, + " Overrun: %.1f°C\n" " Minimum Off Time: %" PRIu32 "s\n" " Minimum Run Time: %" PRIu32 "s", + this->heating_deadband_, this->heating_overrun_, this->timer_duration_(thermostat::TIMER_HEATING_OFF) / 1000, this->timer_duration_(thermostat::TIMER_HEATING_ON) / 1000); + if ((this->supplemental_heat_delta_ > 0) || (this->timer_duration_(thermostat::TIMER_HEATING_MAX_RUN_TIME) > 0)) { + ESP_LOGCONFIG(TAG, + " Maximum Run Time: %" PRIu32 "s\n" + " Supplemental Delta: %.1f°C", + this->timer_duration_(thermostat::TIMER_HEATING_MAX_RUN_TIME) / 1000, + this->supplemental_heat_delta_); + } } if (this->supports_fan_only_) { ESP_LOGCONFIG(TAG, - " Fanning Minimum Off Time: %" PRIu32 "s\n" - " Fanning Minimum Run Time: %" PRIu32 "s", + " Fan Parameters:\n" + " Minimum Off Time: %" PRIu32 "s\n" + " Minimum Run Time: %" PRIu32 "s", this->timer_duration_(thermostat::TIMER_FANNING_OFF) / 1000, this->timer_duration_(thermostat::TIMER_FANNING_ON) / 1000); } @@ -1351,8 +1370,8 @@ void ThermostatClimate::dump_config() { ESP_LOGCONFIG(TAG, " Minimum Fan Mode Switching Time: %" PRIu32 "s", this->timer_duration_(thermostat::TIMER_FAN_MODE) / 1000); } - ESP_LOGCONFIG(TAG, " Minimum Idle Time: %" PRIu32 "s", this->timer_[thermostat::TIMER_IDLE_ON].time / 1000); ESP_LOGCONFIG(TAG, + " Minimum Idle Time: %" PRIu32 "s\n" " Supported MODES:\n" " AUTO: %s\n" " HEAT/COOL: %s\n" @@ -1362,8 +1381,9 @@ void ThermostatClimate::dump_config() { " FAN_ONLY: %s\n" " FAN_ONLY_ACTION_USES_FAN_MODE_TIMER: %s\n" " FAN_ONLY_COOLING: %s", - YESNO(this->supports_auto_), YESNO(this->supports_heat_cool_), YESNO(this->supports_heat_), - YESNO(this->supports_cool_), YESNO(this->supports_dry_), YESNO(this->supports_fan_only_), + this->timer_[thermostat::TIMER_IDLE_ON].time / 1000, YESNO(this->supports_auto_), + YESNO(this->supports_heat_cool_), YESNO(this->supports_heat_), YESNO(this->supports_cool_), + YESNO(this->supports_dry_), YESNO(this->supports_fan_only_), YESNO(this->supports_fan_only_action_uses_fan_mode_timer_), YESNO(this->supports_fan_only_cooling_)); if (this->supports_cool_) { ESP_LOGCONFIG(TAG, " FAN_WITH_COOLING: %s", YESNO(this->supports_fan_with_cooling_)); @@ -1382,40 +1402,39 @@ void ThermostatClimate::dump_config() { " MIDDLE: %s\n" " FOCUS: %s\n" " DIFFUSE: %s\n" - " QUIET: %s", - YESNO(this->supports_fan_mode_on_), YESNO(this->supports_fan_mode_off_), - YESNO(this->supports_fan_mode_auto_), YESNO(this->supports_fan_mode_low_), - YESNO(this->supports_fan_mode_medium_), YESNO(this->supports_fan_mode_high_), - YESNO(this->supports_fan_mode_middle_), YESNO(this->supports_fan_mode_focus_), - YESNO(this->supports_fan_mode_diffuse_), YESNO(this->supports_fan_mode_quiet_)); - ESP_LOGCONFIG(TAG, + " QUIET: %s\n" " Supported SWING MODES:\n" " BOTH: %s\n" " OFF: %s\n" " HORIZONTAL: %s\n" " VERTICAL: %s\n" " Supports TWO SET POINTS: %s", + YESNO(this->supports_fan_mode_on_), YESNO(this->supports_fan_mode_off_), + YESNO(this->supports_fan_mode_auto_), YESNO(this->supports_fan_mode_low_), + YESNO(this->supports_fan_mode_medium_), YESNO(this->supports_fan_mode_high_), + YESNO(this->supports_fan_mode_middle_), YESNO(this->supports_fan_mode_focus_), + YESNO(this->supports_fan_mode_diffuse_), YESNO(this->supports_fan_mode_quiet_), YESNO(this->supports_swing_mode_both_), YESNO(this->supports_swing_mode_off_), YESNO(this->supports_swing_mode_horizontal_), YESNO(this->supports_swing_mode_vertical_), YESNO(this->supports_two_points_)); - ESP_LOGCONFIG(TAG, " Supported PRESETS: "); - for (auto &it : this->preset_config_) { - const auto *preset_name = LOG_STR_ARG(climate::climate_preset_to_string(it.first)); - - ESP_LOGCONFIG(TAG, " Supports %s: %s", preset_name, YESNO(true)); - this->dump_preset_config_(preset_name, it.second, it.first == this->default_preset_); + if (!this->preset_config_.empty()) { + ESP_LOGCONFIG(TAG, " Supported PRESETS:"); + for (auto &it : this->preset_config_) { + const auto *preset_name = LOG_STR_ARG(climate::climate_preset_to_string(it.first)); + ESP_LOGCONFIG(TAG, " %s:%s", preset_name, it.first == this->default_preset_ ? " (default)" : ""); + this->dump_preset_config_(preset_name, it.second); + } } - ESP_LOGCONFIG(TAG, " Supported CUSTOM PRESETS: "); - for (auto &it : this->custom_preset_config_) { - const auto *preset_name = it.first.c_str(); - - ESP_LOGCONFIG(TAG, " Supports %s: %s", preset_name, YESNO(true)); - this->dump_preset_config_(preset_name, it.second, it.first == this->default_custom_preset_); + if (!this->custom_preset_config_.empty()) { + ESP_LOGCONFIG(TAG, " Supported CUSTOM PRESETS:"); + for (auto &it : this->custom_preset_config_) { + const auto *preset_name = it.first.c_str(); + ESP_LOGCONFIG(TAG, " %s:%s", preset_name, it.first == this->default_custom_preset_ ? " (default)" : ""); + this->dump_preset_config_(preset_name, it.second); + } } - ESP_LOGCONFIG(TAG, " On boot, restore from: %s", - this->on_boot_restore_from_ == thermostat::DEFAULT_PRESET ? "DEFAULT_PRESET" : "MEMORY"); } ThermostatClimateTargetTempConfig::ThermostatClimateTargetTempConfig() = default; diff --git a/esphome/components/thermostat/thermostat_climate.h b/esphome/components/thermostat/thermostat_climate.h index 007d7297d5..e2c7b00266 100644 --- a/esphome/components/thermostat/thermostat_climate.h +++ b/esphome/components/thermostat/thermostat_climate.h @@ -6,9 +6,9 @@ #include "esphome/components/climate/climate.h" #include "esphome/components/sensor/sensor.h" +#include #include #include -#include namespace esphome { namespace thermostat { @@ -24,6 +24,7 @@ enum ThermostatClimateTimerIndex : uint8_t { TIMER_HEATING_OFF = 7, TIMER_HEATING_ON = 8, TIMER_IDLE_ON = 9, + TIMER_COUNT = 10, }; enum OnBootRestoreFrom : uint8_t { @@ -131,6 +132,7 @@ class ThermostatClimate : public climate::Climate, public Component { Trigger<> *get_dry_mode_trigger() const; Trigger<> *get_fan_only_mode_trigger() const; Trigger<> *get_heat_mode_trigger() const; + Trigger<> *get_heat_cool_mode_trigger() const; Trigger<> *get_off_mode_trigger() const; Trigger<> *get_fan_mode_on_trigger() const; Trigger<> *get_fan_mode_off_trigger() const; @@ -163,9 +165,10 @@ class ThermostatClimate : public climate::Climate, public Component { /// Returns the fan mode that is locked in (check fan_mode_change_delayed(), first!) climate::ClimateFanMode locked_fan_mode(); /// Set point and hysteresis validation - bool hysteresis_valid(); // returns true if valid + bool hysteresis_valid(); // returns true if valid + bool limit_setpoints_for_heat_cool(); // returns true if set points should be further limited within visual range void validate_target_temperature(); - void validate_target_temperatures(); + void validate_target_temperatures(bool pin_target_temperature_high); void validate_target_temperature_low(); void validate_target_temperature_high(); @@ -241,12 +244,28 @@ class ThermostatClimate : public climate::Climate, public Component { bool supplemental_cooling_required_(); bool supplemental_heating_required_(); - void dump_preset_config_(const char *preset_name, const ThermostatClimateTargetTempConfig &config, - bool is_default_preset); + void dump_preset_config_(const char *preset_name, const ThermostatClimateTargetTempConfig &config); /// Minimum allowable duration in seconds for action timers const uint8_t min_timer_duration_{1}; + /// Store previously-known states + /// + /// These are used to determine when a trigger/action needs to be called + climate::ClimateFanMode prev_fan_mode_{climate::CLIMATE_FAN_ON}; + climate::ClimateMode prev_mode_{climate::CLIMATE_MODE_OFF}; + climate::ClimateSwingMode prev_swing_mode_{climate::CLIMATE_SWING_OFF}; + + /// The current supplemental action + climate::ClimateAction supplemental_action_{climate::CLIMATE_ACTION_OFF}; + + /// Default standard preset to use on start up + climate::ClimatePreset default_preset_{}; + + /// If set to DEFAULT_PRESET then the default preset is always used. When MEMORY prior + /// state will attempt to be restored if possible + OnBootRestoreFrom on_boot_restore_from_{OnBootRestoreFrom::MEMORY}; + /// Whether the controller supports auto/cooling/drying/fanning/heating. /// /// A false value for any given attribute means that the controller has no such action @@ -362,9 +381,15 @@ class ThermostatClimate : public climate::Climate, public Component { Trigger<> *supplemental_heat_action_trigger_{nullptr}; Trigger<> *heat_mode_trigger_{nullptr}; + /// The trigger to call when the controller should switch to heat/cool mode. + /// + /// In heat/cool mode, the controller will enable heating/cooling as necessary and switch + /// to idle when the temperature is within the thresholds/set points. + Trigger<> *heat_cool_mode_trigger_{nullptr}; + /// The trigger to call when the controller should switch to auto mode. /// - /// In auto mode, the controller will enable heating/cooling as necessary and switch + /// In auto mode, the controller will enable heating/cooling as supported/necessary and switch /// to idle when the temperature is within the thresholds/set points. Trigger<> *auto_mode_trigger_{nullptr}; @@ -438,35 +463,21 @@ class ThermostatClimate : public climate::Climate, public Component { Trigger<> *prev_mode_trigger_{nullptr}; Trigger<> *prev_swing_mode_trigger_{nullptr}; - /// If set to DEFAULT_PRESET then the default preset is always used. When MEMORY prior - /// state will attempt to be restored if possible - OnBootRestoreFrom on_boot_restore_from_{OnBootRestoreFrom::MEMORY}; - - /// Store previously-known states - /// - /// These are used to determine when a trigger/action needs to be called - climate::ClimateAction supplemental_action_{climate::CLIMATE_ACTION_OFF}; - climate::ClimateFanMode prev_fan_mode_{climate::CLIMATE_FAN_ON}; - climate::ClimateMode prev_mode_{climate::CLIMATE_MODE_OFF}; - climate::ClimateSwingMode prev_swing_mode_{climate::CLIMATE_SWING_OFF}; - - /// Default standard preset to use on start up - climate::ClimatePreset default_preset_{}; /// Default custom preset to use on start up std::string default_custom_preset_{}; /// Climate action timers - std::vector timer_{ - {false, 0, 0, std::bind(&ThermostatClimate::cooling_max_run_time_timer_callback_, this)}, - {false, 0, 0, std::bind(&ThermostatClimate::cooling_off_timer_callback_, this)}, - {false, 0, 0, std::bind(&ThermostatClimate::cooling_on_timer_callback_, this)}, - {false, 0, 0, std::bind(&ThermostatClimate::fan_mode_timer_callback_, this)}, - {false, 0, 0, std::bind(&ThermostatClimate::fanning_off_timer_callback_, this)}, - {false, 0, 0, std::bind(&ThermostatClimate::fanning_on_timer_callback_, this)}, - {false, 0, 0, std::bind(&ThermostatClimate::heating_max_run_time_timer_callback_, this)}, - {false, 0, 0, std::bind(&ThermostatClimate::heating_off_timer_callback_, this)}, - {false, 0, 0, std::bind(&ThermostatClimate::heating_on_timer_callback_, this)}, - {false, 0, 0, std::bind(&ThermostatClimate::idle_on_timer_callback_, this)}, + std::array timer_{ + ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::cooling_max_run_time_timer_callback_, this)), + ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::cooling_off_timer_callback_, this)), + ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::cooling_on_timer_callback_, this)), + ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::fan_mode_timer_callback_, this)), + ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::fanning_off_timer_callback_, this)), + ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::fanning_on_timer_callback_, this)), + ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::heating_max_run_time_timer_callback_, this)), + ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::heating_off_timer_callback_, this)), + ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::heating_on_timer_callback_, this)), + ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::idle_on_timer_callback_, this)), }; /// The set of standard preset configurations this thermostat supports (Eg. AWAY, ECO, etc) diff --git a/esphome/const.py b/esphome/const.py index f27adff9be..fae6020b88 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -424,6 +424,7 @@ CONF_HEAD = "head" CONF_HEADING = "heading" CONF_HEARTBEAT = "heartbeat" CONF_HEAT_ACTION = "heat_action" +CONF_HEAT_COOL_MODE = "heat_cool_mode" CONF_HEAT_DEADBAND = "heat_deadband" CONF_HEAT_MODE = "heat_mode" CONF_HEAT_OVERRUN = "heat_overrun" From ac61b8f8936c5e50c454ee8bfd82574dca72d23f Mon Sep 17 00:00:00 2001 From: Daniel M Date: Wed, 10 Sep 2025 13:50:49 +1200 Subject: [PATCH 205/208] [bl0940] extend configuration options of bl0940 device (#8158) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- CODEOWNERS | 2 +- esphome/components/bl0940/__init__.py | 7 +- esphome/components/bl0940/bl0940.cpp | 219 ++++++++++++++---- esphome/components/bl0940/bl0940.h | 185 +++++++++------ esphome/components/bl0940/button/__init__.py | 27 +++ .../button/calibration_reset_button.cpp | 20 ++ .../bl0940/button/calibration_reset_button.h | 19 ++ esphome/components/bl0940/number/__init__.py | 94 ++++++++ .../bl0940/number/calibration_number.cpp | 29 +++ .../bl0940/number/calibration_number.h | 26 +++ esphome/components/bl0940/sensor.py | 153 +++++++++++- tests/components/bl0940/common.yaml | 21 ++ 12 files changed, 687 insertions(+), 115 deletions(-) create mode 100644 esphome/components/bl0940/button/__init__.py create mode 100644 esphome/components/bl0940/button/calibration_reset_button.cpp create mode 100644 esphome/components/bl0940/button/calibration_reset_button.h create mode 100644 esphome/components/bl0940/number/__init__.py create mode 100644 esphome/components/bl0940/number/calibration_number.cpp create mode 100644 esphome/components/bl0940/number/calibration_number.h diff --git a/CODEOWNERS b/CODEOWNERS index a77a7ba86e..acf8acb2ab 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -66,7 +66,7 @@ esphome/components/binary_sensor/* @esphome/core esphome/components/bk72xx/* @kuba2k2 esphome/components/bl0906/* @athom-tech @jesserockz @tarontop esphome/components/bl0939/* @ziceva -esphome/components/bl0940/* @tobias- +esphome/components/bl0940/* @dan-s-github @tobias- esphome/components/bl0942/* @dbuezas @dwmw2 esphome/components/ble_client/* @buxtronix @clydebarrow esphome/components/bluetooth_proxy/* @bdraco @jesserockz diff --git a/esphome/components/bl0940/__init__.py b/esphome/components/bl0940/__init__.py index 087626a4e7..066c2818b6 100644 --- a/esphome/components/bl0940/__init__.py +++ b/esphome/components/bl0940/__init__.py @@ -1 +1,6 @@ -CODEOWNERS = ["@tobias-"] +import esphome.codegen as cg + +CODEOWNERS = ["@tobias-", "@dan-s-github"] + +CONF_BL0940_ID = "bl0940_id" +bl0940_ns = cg.esphome_ns.namespace("bl0940") diff --git a/esphome/components/bl0940/bl0940.cpp b/esphome/components/bl0940/bl0940.cpp index 24990d5482..42e20eb69b 100644 --- a/esphome/components/bl0940/bl0940.cpp +++ b/esphome/components/bl0940/bl0940.cpp @@ -7,28 +7,26 @@ namespace bl0940 { static const char *const TAG = "bl0940"; -static const uint8_t BL0940_READ_COMMAND = 0x50; // 0x58 according to documentation static const uint8_t BL0940_FULL_PACKET = 0xAA; -static const uint8_t BL0940_PACKET_HEADER = 0x55; // 0x58 according to documentation +static const uint8_t BL0940_PACKET_HEADER = 0x55; // 0x58 according to en doc but 0x55 in cn doc -static const uint8_t BL0940_WRITE_COMMAND = 0xA0; // 0xA8 according to documentation static const uint8_t BL0940_REG_I_FAST_RMS_CTRL = 0x10; static const uint8_t BL0940_REG_MODE = 0x18; static const uint8_t BL0940_REG_SOFT_RESET = 0x19; static const uint8_t BL0940_REG_USR_WRPROT = 0x1A; static const uint8_t BL0940_REG_TPS_CTRL = 0x1B; -const uint8_t BL0940_INIT[5][6] = { +static const uint8_t BL0940_INIT[5][5] = { // Reset to default - {BL0940_WRITE_COMMAND, BL0940_REG_SOFT_RESET, 0x5A, 0x5A, 0x5A, 0x38}, + {BL0940_REG_SOFT_RESET, 0x5A, 0x5A, 0x5A, 0x38}, // Enable User Operation Write - {BL0940_WRITE_COMMAND, BL0940_REG_USR_WRPROT, 0x55, 0x00, 0x00, 0xF0}, + {BL0940_REG_USR_WRPROT, 0x55, 0x00, 0x00, 0xF0}, // 0x0100 = CF_UNABLE energy pulse, AC_FREQ_SEL 50Hz, RMS_UPDATE_SEL 800mS - {BL0940_WRITE_COMMAND, BL0940_REG_MODE, 0x00, 0x10, 0x00, 0x37}, + {BL0940_REG_MODE, 0x00, 0x10, 0x00, 0x37}, // 0x47FF = Over-current and leakage alarm on, Automatic temperature measurement, Interval 100mS - {BL0940_WRITE_COMMAND, BL0940_REG_TPS_CTRL, 0xFF, 0x47, 0x00, 0xFE}, + {BL0940_REG_TPS_CTRL, 0xFF, 0x47, 0x00, 0xFE}, // 0x181C = Half cycle, Fast RMS threshold 6172 - {BL0940_WRITE_COMMAND, BL0940_REG_I_FAST_RMS_CTRL, 0x1C, 0x18, 0x00, 0x1B}}; + {BL0940_REG_I_FAST_RMS_CTRL, 0x1C, 0x18, 0x00, 0x1B}}; void BL0940::loop() { DataPacket buffer; @@ -36,8 +34,8 @@ void BL0940::loop() { return; } if (read_array((uint8_t *) &buffer, sizeof(buffer))) { - if (validate_checksum(&buffer)) { - received_package_(&buffer); + if (this->validate_checksum_(&buffer)) { + this->received_package_(&buffer); } } else { ESP_LOGW(TAG, "Junk on wire. Throwing away partial message"); @@ -46,35 +44,151 @@ void BL0940::loop() { } } -bool BL0940::validate_checksum(const DataPacket *data) { - uint8_t checksum = BL0940_READ_COMMAND; +bool BL0940::validate_checksum_(DataPacket *data) { + uint8_t checksum = this->read_command_; // Whole package but checksum - for (uint32_t i = 0; i < sizeof(data->raw) - 1; i++) { - checksum += data->raw[i]; + uint8_t *raw = (uint8_t *) data; + for (uint32_t i = 0; i < sizeof(*data) - 1; i++) { + checksum += raw[i]; } checksum ^= 0xFF; if (checksum != data->checksum) { - ESP_LOGW(TAG, "BL0940 invalid checksum! 0x%02X != 0x%02X", checksum, data->checksum); + ESP_LOGW(TAG, "Invalid checksum! 0x%02X != 0x%02X", checksum, data->checksum); } return checksum == data->checksum; } void BL0940::update() { this->flush(); - this->write_byte(BL0940_READ_COMMAND); + this->write_byte(this->read_command_); this->write_byte(BL0940_FULL_PACKET); } void BL0940::setup() { +#ifdef USE_NUMBER + // add calibration callbacks + if (this->voltage_calibration_number_ != nullptr) { + this->voltage_calibration_number_->add_on_state_callback( + [this](float state) { this->voltage_calibration_callback_(state); }); + if (this->voltage_calibration_number_->has_state()) { + this->voltage_calibration_callback_(this->voltage_calibration_number_->state); + } + } + + if (this->current_calibration_number_ != nullptr) { + this->current_calibration_number_->add_on_state_callback( + [this](float state) { this->current_calibration_callback_(state); }); + if (this->current_calibration_number_->has_state()) { + this->current_calibration_callback_(this->current_calibration_number_->state); + } + } + + if (this->power_calibration_number_ != nullptr) { + this->power_calibration_number_->add_on_state_callback( + [this](float state) { this->power_calibration_callback_(state); }); + if (this->power_calibration_number_->has_state()) { + this->power_calibration_callback_(this->power_calibration_number_->state); + } + } + + if (this->energy_calibration_number_ != nullptr) { + this->energy_calibration_number_->add_on_state_callback( + [this](float state) { this->energy_calibration_callback_(state); }); + if (this->energy_calibration_number_->has_state()) { + this->energy_calibration_callback_(this->energy_calibration_number_->state); + } + } +#endif + + // calculate calibrated reference values + this->voltage_reference_cal_ = this->voltage_reference_ / this->voltage_cal_; + this->current_reference_cal_ = this->current_reference_ / this->current_cal_; + this->power_reference_cal_ = this->power_reference_ / this->power_cal_; + this->energy_reference_cal_ = this->energy_reference_ / this->energy_cal_; + for (auto *i : BL0940_INIT) { - this->write_array(i, 6); + this->write_byte(this->write_command_), this->write_array(i, 5); delay(1); } this->flush(); } -float BL0940::update_temp_(sensor::Sensor *sensor, ube16_t temperature) const { - auto tb = (float) (temperature.h << 8 | temperature.l); +float BL0940::calculate_power_reference_() { + // calculate power reference based on voltage and current reference + return this->voltage_reference_cal_ * this->current_reference_cal_ * 4046 / 324004 / 79931; +} + +float BL0940::calculate_energy_reference_() { + // formula: 3600000 * 4046 * RL * R1 * 1000 / (1638.4 * 256) / Vref² / (R1 + R2) + // or: power_reference_ * 3600000 / (1638.4 * 256) + return this->power_reference_cal_ * 3600000 / (1638.4 * 256); +} + +float BL0940::calculate_calibration_value_(float state) { return (100 + state) / 100; } + +void BL0940::reset_calibration() { +#ifdef USE_NUMBER + if (this->current_calibration_number_ != nullptr && this->current_cal_ != 1) { + this->current_calibration_number_->make_call().set_value(0).perform(); + } + if (this->voltage_calibration_number_ != nullptr && this->voltage_cal_ != 1) { + this->voltage_calibration_number_->make_call().set_value(0).perform(); + } + if (this->power_calibration_number_ != nullptr && this->power_cal_ != 1) { + this->power_calibration_number_->make_call().set_value(0).perform(); + } + if (this->energy_calibration_number_ != nullptr && this->energy_cal_ != 1) { + this->energy_calibration_number_->make_call().set_value(0).perform(); + } +#endif + ESP_LOGD(TAG, "external calibration values restored to initial state"); +} + +void BL0940::current_calibration_callback_(float state) { + this->current_cal_ = this->calculate_calibration_value_(state); + ESP_LOGV(TAG, "update current calibration state: %f", this->current_cal_); + this->recalibrate_(); +} +void BL0940::voltage_calibration_callback_(float state) { + this->voltage_cal_ = this->calculate_calibration_value_(state); + ESP_LOGV(TAG, "update voltage calibration state: %f", this->voltage_cal_); + this->recalibrate_(); +} +void BL0940::power_calibration_callback_(float state) { + this->power_cal_ = this->calculate_calibration_value_(state); + ESP_LOGV(TAG, "update power calibration state: %f", this->power_cal_); + this->recalibrate_(); +} +void BL0940::energy_calibration_callback_(float state) { + this->energy_cal_ = this->calculate_calibration_value_(state); + ESP_LOGV(TAG, "update energy calibration state: %f", this->energy_cal_); + this->recalibrate_(); +} + +void BL0940::recalibrate_() { + ESP_LOGV(TAG, "Recalibrating reference values"); + this->voltage_reference_cal_ = this->voltage_reference_ / this->voltage_cal_; + this->current_reference_cal_ = this->current_reference_ / this->current_cal_; + + if (this->voltage_cal_ != 1 || this->current_cal_ != 1) { + this->power_reference_ = this->calculate_power_reference_(); + } + this->power_reference_cal_ = this->power_reference_ / this->power_cal_; + + if (this->voltage_cal_ != 1 || this->current_cal_ != 1 || this->power_cal_ != 1) { + this->energy_reference_ = this->calculate_energy_reference_(); + } + this->energy_reference_cal_ = this->energy_reference_ / this->energy_cal_; + + ESP_LOGD(TAG, + "Recalibrated reference values:\n" + "Voltage: %f\n, Current: %f\n, Power: %f\n, Energy: %f\n", + this->voltage_reference_cal_, this->current_reference_cal_, this->power_reference_cal_, + this->energy_reference_cal_); +} + +float BL0940::update_temp_(sensor::Sensor *sensor, uint16_le_t temperature) const { + auto tb = (float) temperature; float converted_temp = ((float) 170 / 448) * (tb / 2 - 32) - 45; if (sensor != nullptr) { if (sensor->has_state() && std::abs(converted_temp - sensor->get_state()) > max_temperature_diff_) { @@ -87,33 +201,40 @@ float BL0940::update_temp_(sensor::Sensor *sensor, ube16_t temperature) const { return converted_temp; } -void BL0940::received_package_(const DataPacket *data) const { +void BL0940::received_package_(DataPacket *data) { // Bad header if (data->frame_header != BL0940_PACKET_HEADER) { ESP_LOGI(TAG, "Invalid data. Header mismatch: %d", data->frame_header); return; } - float v_rms = (float) to_uint32_t(data->v_rms) / voltage_reference_; - float i_rms = (float) to_uint32_t(data->i_rms) / current_reference_; - float watt = (float) to_int32_t(data->watt) / power_reference_; - uint32_t cf_cnt = to_uint32_t(data->cf_cnt); - float total_energy_consumption = (float) cf_cnt / energy_reference_; + // cf_cnt is only 24 bits, so track overflows + uint32_t cf_cnt = (uint24_t) data->cf_cnt; + cf_cnt |= this->prev_cf_cnt_ & 0xff000000; + if (cf_cnt < this->prev_cf_cnt_) { + cf_cnt += 0x1000000; + } + this->prev_cf_cnt_ = cf_cnt; - float tps1 = update_temp_(internal_temperature_sensor_, data->tps1); - float tps2 = update_temp_(external_temperature_sensor_, data->tps2); + float v_rms = (uint24_t) data->v_rms / this->voltage_reference_cal_; + float i_rms = (uint24_t) data->i_rms / this->current_reference_cal_; + float watt = (int24_t) data->watt / this->power_reference_cal_; + float total_energy_consumption = cf_cnt / this->energy_reference_cal_; - if (voltage_sensor_ != nullptr) { - voltage_sensor_->publish_state(v_rms); + float tps1 = update_temp_(this->internal_temperature_sensor_, data->tps1); + float tps2 = update_temp_(this->external_temperature_sensor_, data->tps2); + + if (this->voltage_sensor_ != nullptr) { + this->voltage_sensor_->publish_state(v_rms); } - if (current_sensor_ != nullptr) { - current_sensor_->publish_state(i_rms); + if (this->current_sensor_ != nullptr) { + this->current_sensor_->publish_state(i_rms); } - if (power_sensor_ != nullptr) { - power_sensor_->publish_state(watt); + if (this->power_sensor_ != nullptr) { + this->power_sensor_->publish_state(watt); } - if (energy_sensor_ != nullptr) { - energy_sensor_->publish_state(total_energy_consumption); + if (this->energy_sensor_ != nullptr) { + this->energy_sensor_->publish_state(total_energy_consumption); } ESP_LOGV(TAG, "BL0940: U %fV, I %fA, P %fW, Cnt %" PRId32 ", ∫P %fkWh, T1 %f°C, T2 %f°C", v_rms, i_rms, watt, cf_cnt, @@ -121,7 +242,27 @@ void BL0940::received_package_(const DataPacket *data) const { } void BL0940::dump_config() { // NOLINT(readability-function-cognitive-complexity) - ESP_LOGCONFIG(TAG, "BL0940:"); + ESP_LOGCONFIG(TAG, + "BL0940:\n" + " LEGACY MODE: %s\n" + " READ CMD: 0x%02X\n" + " WRITE CMD: 0x%02X\n" + " ------------------\n" + " Current reference: %f\n" + " Energy reference: %f\n" + " Power reference: %f\n" + " Voltage reference: %f\n", + TRUEFALSE(this->legacy_mode_enabled_), this->read_command_, this->write_command_, + this->current_reference_, this->energy_reference_, this->power_reference_, this->voltage_reference_); +#ifdef USE_NUMBER + ESP_LOGCONFIG(TAG, + "BL0940:\n" + " Current calibration: %f\n" + " Energy calibration: %f\n" + " Power calibration: %f\n" + " Voltage calibration: %f\n", + this->current_cal_, this->energy_cal_, this->power_cal_, this->voltage_cal_); +#endif LOG_SENSOR("", "Voltage", this->voltage_sensor_); LOG_SENSOR("", "Current", this->current_sensor_); LOG_SENSOR("", "Power", this->power_sensor_); @@ -130,9 +271,5 @@ void BL0940::dump_config() { // NOLINT(readability-function-cognitive-complexit LOG_SENSOR("", "External temperature", this->external_temperature_sensor_); } -uint32_t BL0940::to_uint32_t(ube24_t input) { return input.h << 16 | input.m << 8 | input.l; } - -int32_t BL0940::to_int32_t(sbe24_t input) { return input.h << 16 | input.m << 8 | input.l; } - } // namespace bl0940 } // namespace esphome diff --git a/esphome/components/bl0940/bl0940.h b/esphome/components/bl0940/bl0940.h index 2d4e7ccaac..93d54003f5 100644 --- a/esphome/components/bl0940/bl0940.h +++ b/esphome/components/bl0940/bl0940.h @@ -1,66 +1,48 @@ #pragma once #include "esphome/core/component.h" -#include "esphome/components/uart/uart.h" +#include "esphome/core/datatypes.h" +#include "esphome/core/defines.h" +#ifdef USE_BUTTON +#include "esphome/components/button/button.h" +#endif +#ifdef USE_NUMBER +#include "esphome/components/number/number.h" +#endif #include "esphome/components/sensor/sensor.h" +#include "esphome/components/uart/uart.h" namespace esphome { namespace bl0940 { -static const float BL0940_PREF = 1430; -static const float BL0940_UREF = 33000; -static const float BL0940_IREF = 275000; // 2750 from tasmota. Seems to generate values 100 times too high - -// Measured to 297J per click according to power consumption of 5 minutes -// Converted to kWh (3.6MJ per kwH). Used to be 256 * 1638.4 -static const float BL0940_EREF = 3.6e6 / 297; - -struct ube24_t { // NOLINT(readability-identifier-naming,altera-struct-pack-align) - uint8_t l; - uint8_t m; - uint8_t h; -} __attribute__((packed)); - -struct ube16_t { // NOLINT(readability-identifier-naming,altera-struct-pack-align) - uint8_t l; - uint8_t h; -} __attribute__((packed)); - -struct sbe24_t { // NOLINT(readability-identifier-naming,altera-struct-pack-align) - uint8_t l; - uint8_t m; - int8_t h; -} __attribute__((packed)); - // Caveat: All these values are big endian (low - middle - high) - -union DataPacket { // NOLINT(altera-struct-pack-align) - uint8_t raw[35]; - struct { - uint8_t frame_header; // value of 0x58 according to docs. 0x55 according to Tasmota real world tests. Reality wins. - ube24_t i_fast_rms; // 0x00 - ube24_t i_rms; // 0x04 - ube24_t RESERVED0; // reserved - ube24_t v_rms; // 0x06 - ube24_t RESERVED1; // reserved - sbe24_t watt; // 0x08 - ube24_t RESERVED2; // reserved - ube24_t cf_cnt; // 0x0A - ube24_t RESERVED3; // reserved - ube16_t tps1; // 0x0c - uint8_t RESERVED4; // value of 0x00 - ube16_t tps2; // 0x0c - uint8_t RESERVED5; // value of 0x00 - uint8_t checksum; // checksum - }; +struct DataPacket { + uint8_t frame_header; // Packet header (0x58 in EN docs, 0x55 in CN docs and Tasmota tests) + uint24_le_t i_fast_rms; // Fast RMS current + uint24_le_t i_rms; // RMS current + uint24_t RESERVED0; // Reserved + uint24_le_t v_rms; // RMS voltage + uint24_t RESERVED1; // Reserved + int24_le_t watt; // Active power (can be negative for bidirectional measurement) + uint24_t RESERVED2; // Reserved + uint24_le_t cf_cnt; // Energy pulse count + uint24_t RESERVED3; // Reserved + uint16_le_t tps1; // Internal temperature sensor 1 + uint8_t RESERVED4; // Reserved (should be 0x00) + uint16_le_t tps2; // Internal temperature sensor 2 + uint8_t RESERVED5; // Reserved (should be 0x00) + uint8_t checksum; // Packet checksum } __attribute__((packed)); class BL0940 : public PollingComponent, public uart::UARTDevice { public: + // Sensor setters void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; } void set_current_sensor(sensor::Sensor *current_sensor) { current_sensor_ = current_sensor; } void set_power_sensor(sensor::Sensor *power_sensor) { power_sensor_ = power_sensor; } void set_energy_sensor(sensor::Sensor *energy_sensor) { energy_sensor_ = energy_sensor; } + + // Temperature sensor setters void set_internal_temperature_sensor(sensor::Sensor *internal_temperature_sensor) { internal_temperature_sensor_ = internal_temperature_sensor; } @@ -68,42 +50,105 @@ class BL0940 : public PollingComponent, public uart::UARTDevice { external_temperature_sensor_ = external_temperature_sensor; } - void loop() override; + // Configuration setters + void set_legacy_mode(bool enable) { this->legacy_mode_enabled_ = enable; } + void set_read_command(uint8_t read_command) { this->read_command_ = read_command; } + void set_write_command(uint8_t write_command) { this->write_command_ = write_command; } + // Reference value setters (used for calibration and conversion) + void set_current_reference(float current_ref) { this->current_reference_ = current_ref; } + void set_energy_reference(float energy_ref) { this->energy_reference_ = energy_ref; } + void set_power_reference(float power_ref) { this->power_reference_ = power_ref; } + void set_voltage_reference(float voltage_ref) { this->voltage_reference_ = voltage_ref; } + +#ifdef USE_NUMBER + // Calibration number setters (for Home Assistant number entities) + void set_current_calibration_number(number::Number *num) { this->current_calibration_number_ = num; } + void set_voltage_calibration_number(number::Number *num) { this->voltage_calibration_number_ = num; } + void set_power_calibration_number(number::Number *num) { this->power_calibration_number_ = num; } + void set_energy_calibration_number(number::Number *num) { this->energy_calibration_number_ = num; } +#endif + +#ifdef USE_BUTTON + // Resets all calibration values to defaults (can be triggered by a button) + void reset_calibration(); +#endif + + // Core component methods + void loop() override; void update() override; void setup() override; void dump_config() override; protected: - sensor::Sensor *voltage_sensor_{nullptr}; - sensor::Sensor *current_sensor_{nullptr}; - // NB This may be negative as the circuits is seemingly able to measure - // power in both directions - sensor::Sensor *power_sensor_{nullptr}; - sensor::Sensor *energy_sensor_{nullptr}; - sensor::Sensor *internal_temperature_sensor_{nullptr}; - sensor::Sensor *external_temperature_sensor_{nullptr}; + // --- Sensor pointers --- + sensor::Sensor *voltage_sensor_{nullptr}; // Voltage sensor + sensor::Sensor *current_sensor_{nullptr}; // Current sensor + sensor::Sensor *power_sensor_{nullptr}; // Power sensor (can be negative for bidirectional) + sensor::Sensor *energy_sensor_{nullptr}; // Energy sensor + sensor::Sensor *internal_temperature_sensor_{nullptr}; // Internal temperature sensor + sensor::Sensor *external_temperature_sensor_{nullptr}; // External temperature sensor - // Max difference between two measurements of the temperature. Used to avoid noise. - float max_temperature_diff_{0}; - // Divide by this to turn into Watt - float power_reference_ = BL0940_PREF; - // Divide by this to turn into Volt - float voltage_reference_ = BL0940_UREF; - // Divide by this to turn into Ampere - float current_reference_ = BL0940_IREF; - // Divide by this to turn into kWh - float energy_reference_ = BL0940_EREF; +#ifdef USE_NUMBER + // --- Calibration number entities (for dynamic calibration via HA UI) --- + number::Number *voltage_calibration_number_{nullptr}; + number::Number *current_calibration_number_{nullptr}; + number::Number *power_calibration_number_{nullptr}; + number::Number *energy_calibration_number_{nullptr}; +#endif - float update_temp_(sensor::Sensor *sensor, ube16_t packed_temperature) const; + // --- Internal state --- + uint32_t prev_cf_cnt_ = 0; // Previous energy pulse count (for energy calculation) + float max_temperature_diff_{0}; // Max allowed temperature difference between two measurements (noise filter) - static uint32_t to_uint32_t(ube24_t input); + // --- Reference values for conversion --- + float power_reference_; // Divider for raw power to get Watts + float power_reference_cal_; // Calibrated power reference + float voltage_reference_; // Divider for raw voltage to get Volts + float voltage_reference_cal_; // Calibrated voltage reference + float current_reference_; // Divider for raw current to get Amperes + float current_reference_cal_; // Calibrated current reference + float energy_reference_; // Divider for raw energy to get kWh + float energy_reference_cal_; // Calibrated energy reference - static int32_t to_int32_t(sbe24_t input); + // --- Home Assistant calibration values (multipliers, default 1) --- + float current_cal_{1}; + float voltage_cal_{1}; + float power_cal_{1}; + float energy_cal_{1}; - static bool validate_checksum(const DataPacket *data); + // --- Protocol commands --- + uint8_t read_command_; + uint8_t write_command_; - void received_package_(const DataPacket *data) const; + // --- Mode flags --- + bool legacy_mode_enabled_ = true; + + // --- Methods --- + // Converts packed temperature value to float and updates the sensor + float update_temp_(sensor::Sensor *sensor, uint16_le_t packed_temperature) const; + + // Validates the checksum of a received data packet + bool validate_checksum_(DataPacket *data); + + // Handles a received data packet + void received_package_(DataPacket *data); + + // Calculates reference values for calibration and conversion + float calculate_energy_reference_(); + float calculate_power_reference_(); + float calculate_calibration_value_(float state); + + // Calibration update callbacks (used with number entities) + void current_calibration_callback_(float state); + void voltage_calibration_callback_(float state); + void power_calibration_callback_(float state); + void energy_calibration_callback_(float state); + void reset_calibration_callback_(); + + // Recalculates all reference values after calibration changes + void recalibrate_(); }; + } // namespace bl0940 } // namespace esphome diff --git a/esphome/components/bl0940/button/__init__.py b/esphome/components/bl0940/button/__init__.py new file mode 100644 index 0000000000..04d11e6e30 --- /dev/null +++ b/esphome/components/bl0940/button/__init__.py @@ -0,0 +1,27 @@ +import esphome.codegen as cg +from esphome.components import button +import esphome.config_validation as cv +from esphome.const import ENTITY_CATEGORY_CONFIG, ICON_RESTART + +from .. import CONF_BL0940_ID, bl0940_ns +from ..sensor import BL0940 + +CalibrationResetButton = bl0940_ns.class_( + "CalibrationResetButton", button.Button, cg.Component +) + +CONFIG_SCHEMA = cv.All( + button.button_schema( + CalibrationResetButton, + entity_category=ENTITY_CATEGORY_CONFIG, + icon=ICON_RESTART, + ) + .extend({cv.GenerateID(CONF_BL0940_ID): cv.use_id(BL0940)}) + .extend(cv.COMPONENT_SCHEMA) +) + + +async def to_code(config): + var = await button.new_button(config) + await cg.register_component(var, config) + await cg.register_parented(var, config[CONF_BL0940_ID]) diff --git a/esphome/components/bl0940/button/calibration_reset_button.cpp b/esphome/components/bl0940/button/calibration_reset_button.cpp new file mode 100644 index 0000000000..79a6b872d8 --- /dev/null +++ b/esphome/components/bl0940/button/calibration_reset_button.cpp @@ -0,0 +1,20 @@ +#include "calibration_reset_button.h" +#include "../bl0940.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" + +namespace esphome { +namespace bl0940 { + +static const char *const TAG = "bl0940.button.calibration_reset"; + +void CalibrationResetButton::dump_config() { LOG_BUTTON("", "Calibration Reset Button", this); } + +void CalibrationResetButton::press_action() { + ESP_LOGI(TAG, "Resetting calibration defaults..."); + this->parent_->reset_calibration(); +} + +} // namespace bl0940 +} // namespace esphome diff --git a/esphome/components/bl0940/button/calibration_reset_button.h b/esphome/components/bl0940/button/calibration_reset_button.h new file mode 100644 index 0000000000..6ea3b35cb4 --- /dev/null +++ b/esphome/components/bl0940/button/calibration_reset_button.h @@ -0,0 +1,19 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/button/button.h" + +namespace esphome { +namespace bl0940 { + +class BL0940; // Forward declaration of BL0940 class + +class CalibrationResetButton : public button::Button, public Component, public Parented { + public: + void dump_config() override; + + void press_action() override; +}; + +} // namespace bl0940 +} // namespace esphome diff --git a/esphome/components/bl0940/number/__init__.py b/esphome/components/bl0940/number/__init__.py new file mode 100644 index 0000000000..a640c2ae08 --- /dev/null +++ b/esphome/components/bl0940/number/__init__.py @@ -0,0 +1,94 @@ +import esphome.codegen as cg +from esphome.components import number +import esphome.config_validation as cv +from esphome.const import ( + CONF_MAX_VALUE, + CONF_MIN_VALUE, + CONF_MODE, + CONF_RESTORE_VALUE, + CONF_STEP, + ENTITY_CATEGORY_CONFIG, + UNIT_PERCENT, +) + +from .. import CONF_BL0940_ID, bl0940_ns +from ..sensor import BL0940 + +# Define calibration types +CONF_CURRENT_CALIBRATION = "current_calibration" +CONF_VOLTAGE_CALIBRATION = "voltage_calibration" +CONF_POWER_CALIBRATION = "power_calibration" +CONF_ENERGY_CALIBRATION = "energy_calibration" + +BL0940Number = bl0940_ns.class_("BL0940Number") + +CalibrationNumber = bl0940_ns.class_( + "CalibrationNumber", number.Number, cg.PollingComponent +) + + +def validate_min_max(config): + if config[CONF_MAX_VALUE] <= config[CONF_MIN_VALUE]: + raise cv.Invalid("max_value must be greater than min_value") + return config + + +CALIBRATION_SCHEMA = cv.All( + number.number_schema( + CalibrationNumber, + entity_category=ENTITY_CATEGORY_CONFIG, + unit_of_measurement=UNIT_PERCENT, + ) + .extend( + { + cv.Optional(CONF_MODE, default="BOX"): cv.enum(number.NUMBER_MODES), + cv.Optional(CONF_MAX_VALUE, default=10): cv.All( + cv.float_, cv.Range(max=50) + ), + cv.Optional(CONF_MIN_VALUE, default=-10): cv.All( + cv.float_, cv.Range(min=-50) + ), + cv.Optional(CONF_STEP, default=0.1): cv.positive_float, + cv.Optional(CONF_RESTORE_VALUE): cv.boolean, + } + ) + .extend(cv.COMPONENT_SCHEMA), + validate_min_max, +) + +# Configuration schema for BL0940 numbers +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(BL0940Number), + cv.GenerateID(CONF_BL0940_ID): cv.use_id(BL0940), + cv.Optional(CONF_CURRENT_CALIBRATION): CALIBRATION_SCHEMA, + cv.Optional(CONF_VOLTAGE_CALIBRATION): CALIBRATION_SCHEMA, + cv.Optional(CONF_POWER_CALIBRATION): CALIBRATION_SCHEMA, + cv.Optional(CONF_ENERGY_CALIBRATION): CALIBRATION_SCHEMA, + } +) + + +async def to_code(config): + # Get the BL0940 component instance + bl0940 = await cg.get_variable(config[CONF_BL0940_ID]) + + # Process all calibration types + for cal_type, setter_method in [ + (CONF_CURRENT_CALIBRATION, "set_current_calibration_number"), + (CONF_VOLTAGE_CALIBRATION, "set_voltage_calibration_number"), + (CONF_POWER_CALIBRATION, "set_power_calibration_number"), + (CONF_ENERGY_CALIBRATION, "set_energy_calibration_number"), + ]: + if conf := config.get(cal_type): + var = await number.new_number( + conf, + min_value=conf.get(CONF_MIN_VALUE), + max_value=conf.get(CONF_MAX_VALUE), + step=conf.get(CONF_STEP), + ) + await cg.register_component(var, conf) + + if restore_value := config.get(CONF_RESTORE_VALUE): + cg.add(var.set_restore_value(restore_value)) + cg.add(getattr(bl0940, setter_method)(var)) diff --git a/esphome/components/bl0940/number/calibration_number.cpp b/esphome/components/bl0940/number/calibration_number.cpp new file mode 100644 index 0000000000..cdb26cd298 --- /dev/null +++ b/esphome/components/bl0940/number/calibration_number.cpp @@ -0,0 +1,29 @@ +#include "calibration_number.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace bl0940 { + +static const char *const TAG = "bl0940.number"; + +void CalibrationNumber::setup() { + float value = 0.0f; + if (this->restore_value_) { + this->pref_ = global_preferences->make_preference(this->get_object_id_hash()); + if (!this->pref_.load(&value)) { + value = 0.0f; + } + } + this->publish_state(value); +} + +void CalibrationNumber::control(float value) { + this->publish_state(value); + if (this->restore_value_) + this->pref_.save(&value); +} + +void CalibrationNumber::dump_config() { LOG_NUMBER("", "Calibration Number", this); } + +} // namespace bl0940 +} // namespace esphome diff --git a/esphome/components/bl0940/number/calibration_number.h b/esphome/components/bl0940/number/calibration_number.h new file mode 100644 index 0000000000..3a19e36dc9 --- /dev/null +++ b/esphome/components/bl0940/number/calibration_number.h @@ -0,0 +1,26 @@ +#pragma once + +#include "esphome/components/number/number.h" +#include "esphome/core/component.h" +#include "esphome/core/preferences.h" + +namespace esphome { +namespace bl0940 { + +class CalibrationNumber : public number::Number, public Component { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::HARDWARE; } + + void set_restore_value(bool restore_value) { this->restore_value_ = restore_value; } + + protected: + void control(float value) override; + bool restore_value_{true}; + + ESPPreferenceObject pref_; +}; + +} // namespace bl0940 +} // namespace esphome diff --git a/esphome/components/bl0940/sensor.py b/esphome/components/bl0940/sensor.py index f49e961f0a..d2e0ea435d 100644 --- a/esphome/components/bl0940/sensor.py +++ b/esphome/components/bl0940/sensor.py @@ -8,6 +8,7 @@ from esphome.const import ( CONF_ID, CONF_INTERNAL_TEMPERATURE, CONF_POWER, + CONF_REFERENCE_VOLTAGE, CONF_VOLTAGE, DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, @@ -23,12 +24,133 @@ from esphome.const import ( UNIT_WATT, ) +from . import bl0940_ns + DEPENDENCIES = ["uart"] - -bl0940_ns = cg.esphome_ns.namespace("bl0940") BL0940 = bl0940_ns.class_("BL0940", cg.PollingComponent, uart.UARTDevice) +CONF_LEGACY_MODE = "legacy_mode" + +CONF_READ_COMMAND = "read_command" +CONF_WRITE_COMMAND = "write_command" + +CONF_RESISTOR_SHUNT = "resistor_shunt" +CONF_RESISTOR_ONE = "resistor_one" +CONF_RESISTOR_TWO = "resistor_two" + +CONF_CURRENT_REFERENCE = "current_reference" +CONF_ENERGY_REFERENCE = "energy_reference" +CONF_POWER_REFERENCE = "power_reference" +CONF_VOLTAGE_REFERENCE = "voltage_reference" + +DEFAULT_BL0940_READ_COMMAND = 0x58 +DEFAULT_BL0940_WRITE_COMMAND = 0xA1 + +# Values according to BL0940 application note: +# https://www.belling.com.cn/media/file_object/bel_product/BL0940/guide/BL0940_APPNote_TSSOP14_V1.04_EN.pdf +DEFAULT_BL0940_VREF = 1.218 # Vref = 1.218 +DEFAULT_BL0940_RL = 1 # RL = 1 mΩ +DEFAULT_BL0940_R1 = 0.51 # R1 = 0.51 kΩ +DEFAULT_BL0940_R2 = 1950 # R2 = 5 x 390 kΩ -> 1950 kΩ + +# ---------------------------------------------------- +# values from initial implementation +DEFAULT_BL0940_LEGACY_READ_COMMAND = 0x50 +DEFAULT_BL0940_LEGACY_WRITE_COMMAND = 0xA0 + +DEFAULT_BL0940_LEGACY_UREF = 33000 +DEFAULT_BL0940_LEGACY_IREF = 275000 +DEFAULT_BL0940_LEGACY_PREF = 1430 +# Measured to 297J per click according to power consumption of 5 minutes +# Converted to kWh (3.6MJ per kwH). Used to be 256 * 1638.4 +DEFAULT_BL0940_LEGACY_EREF = 3.6e6 / 297 +# ---------------------------------------------------- + + +# methods to calculate voltage and current reference values +def calculate_voltage_reference(vref, r_one, r_two): + # formula: 79931 / Vref * (R1 * 1000) / (R1 + R2) + return 79931 / vref * (r_one * 1000) / (r_one + r_two) + + +def calculate_current_reference(vref, r_shunt): + # formula: 324004 * RL / Vref + return 324004 * r_shunt / vref + + +def calculate_power_reference(voltage_reference, current_reference): + # calculate power reference based on voltage and current reference + return voltage_reference * current_reference * 4046 / 324004 / 79931 + + +def calculate_energy_reference(power_reference): + # formula: power_reference * 3600000 / (1638.4 * 256) + return power_reference * 3600000 / (1638.4 * 256) + + +def validate_legacy_mode(config): + # Only allow schematic calibration options if legacy_mode is False + if config.get(CONF_LEGACY_MODE, True): + forbidden = [ + CONF_REFERENCE_VOLTAGE, + CONF_RESISTOR_SHUNT, + CONF_RESISTOR_ONE, + CONF_RESISTOR_TWO, + ] + for key in forbidden: + if key in config: + raise cv.Invalid( + f"Option '{key}' is only allowed when legacy_mode: false" + ) + return config + + +def set_command_defaults(config): + # Set defaults for read_command and write_command based on legacy_mode + legacy = config.get(CONF_LEGACY_MODE, True) + if legacy: + config.setdefault(CONF_READ_COMMAND, DEFAULT_BL0940_LEGACY_READ_COMMAND) + config.setdefault(CONF_WRITE_COMMAND, DEFAULT_BL0940_LEGACY_WRITE_COMMAND) + else: + config.setdefault(CONF_READ_COMMAND, DEFAULT_BL0940_READ_COMMAND) + config.setdefault(CONF_WRITE_COMMAND, DEFAULT_BL0940_WRITE_COMMAND) + return config + + +def set_reference_values(config): + # Set default reference values based on legacy_mode + if config.get(CONF_LEGACY_MODE, True): + config.setdefault(CONF_VOLTAGE_REFERENCE, DEFAULT_BL0940_LEGACY_UREF) + config.setdefault(CONF_CURRENT_REFERENCE, DEFAULT_BL0940_LEGACY_IREF) + config.setdefault(CONF_POWER_REFERENCE, DEFAULT_BL0940_LEGACY_PREF) + config.setdefault(CONF_ENERGY_REFERENCE, DEFAULT_BL0940_LEGACY_PREF) + else: + vref = config.get(CONF_VOLTAGE_REFERENCE, DEFAULT_BL0940_VREF) + r_one = config.get(CONF_RESISTOR_ONE, DEFAULT_BL0940_R1) + r_two = config.get(CONF_RESISTOR_TWO, DEFAULT_BL0940_R2) + r_shunt = config.get(CONF_RESISTOR_SHUNT, DEFAULT_BL0940_RL) + + config.setdefault( + CONF_VOLTAGE_REFERENCE, calculate_voltage_reference(vref, r_one, r_two) + ) + config.setdefault( + CONF_CURRENT_REFERENCE, calculate_current_reference(vref, r_shunt) + ) + config.setdefault( + CONF_POWER_REFERENCE, + calculate_power_reference( + config.get(CONF_VOLTAGE_REFERENCE), config.get(CONF_CURRENT_REFERENCE) + ), + ) + config.setdefault( + CONF_ENERGY_REFERENCE, + calculate_energy_reference(config.get(CONF_POWER_REFERENCE)), + ) + + return config + + CONFIG_SCHEMA = ( cv.Schema( { @@ -69,10 +191,24 @@ CONFIG_SCHEMA = ( device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, ), + cv.Optional(CONF_LEGACY_MODE, default=True): cv.boolean, + cv.Optional(CONF_READ_COMMAND): cv.hex_uint8_t, + cv.Optional(CONF_WRITE_COMMAND): cv.hex_uint8_t, + cv.Optional(CONF_REFERENCE_VOLTAGE): cv.float_, + cv.Optional(CONF_RESISTOR_SHUNT): cv.float_, + cv.Optional(CONF_RESISTOR_ONE): cv.float_, + cv.Optional(CONF_RESISTOR_TWO): cv.float_, + cv.Optional(CONF_CURRENT_REFERENCE): cv.float_, + cv.Optional(CONF_ENERGY_REFERENCE): cv.float_, + cv.Optional(CONF_POWER_REFERENCE): cv.float_, + cv.Optional(CONF_VOLTAGE_REFERENCE): cv.float_, } ) .extend(cv.polling_component_schema("60s")) .extend(uart.UART_DEVICE_SCHEMA) + .add_extra(validate_legacy_mode) + .add_extra(set_command_defaults) + .add_extra(set_reference_values) ) @@ -99,3 +235,16 @@ async def to_code(config): if external_temperature_config := config.get(CONF_EXTERNAL_TEMPERATURE): sens = await sensor.new_sensor(external_temperature_config) cg.add(var.set_external_temperature_sensor(sens)) + + # enable legacy mode + cg.add(var.set_legacy_mode(config.get(CONF_LEGACY_MODE))) + + # Set bl0940 commands after validator has determined which defaults to use if not set + cg.add(var.set_read_command(config.get(CONF_READ_COMMAND))) + cg.add(var.set_write_command(config.get(CONF_WRITE_COMMAND))) + + # Set reference values after validator has set the values either from defaults or calculated + cg.add(var.set_current_reference(config.get(CONF_CURRENT_REFERENCE))) + cg.add(var.set_voltage_reference(config.get(CONF_VOLTAGE_REFERENCE))) + cg.add(var.set_power_reference(config.get(CONF_POWER_REFERENCE))) + cg.add(var.set_energy_reference(config.get(CONF_ENERGY_REFERENCE))) diff --git a/tests/components/bl0940/common.yaml b/tests/components/bl0940/common.yaml index 97a997d2b4..443f3b0ff0 100644 --- a/tests/components/bl0940/common.yaml +++ b/tests/components/bl0940/common.yaml @@ -4,8 +4,14 @@ uart: rx_pin: ${rx_pin} baud_rate: 9600 +button: + - platform: bl0940 + bl0940_id: test_id + name: Cal Reset + sensor: - platform: bl0940 + id: test_id voltage: name: BL0940 Voltage current: @@ -18,3 +24,18 @@ sensor: name: BL0940 Internal temperature external_temperature: name: BL0940 External temperature + +number: + - platform: bl0940 + id: bl0940_number_id + bl0940_id: test_id + current_calibration: + name: Cal Current + min_value: -5 + max_value: 5 + voltage_calibration: + name: Cal Voltage + step: 0.01 + power_calibration: + name: Cal Power + disabled_by_default: true From 52a7e26c6de63cabb0758027b05121958fd6bbbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josip=20=C5=A0imun=20Ku=C4=8Di?= <131808461+JosipKuci@users.noreply.github.com> Date: Wed, 10 Sep 2025 08:46:58 +0200 Subject: [PATCH 206/208] [inkplate] Rename component and fix grayscale (#10200) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- CODEOWNERS | 2 +- esphome/components/inkplate/__init__.py | 1 + esphome/components/inkplate/const.py | 105 ++++++++ esphome/components/inkplate/display.py | 238 ++++++++++++++++++ .../{inkplate6 => inkplate}/inkplate.cpp | 230 +++++++++++------ .../{inkplate6 => inkplate}/inkplate.h | 96 +++---- esphome/components/inkplate6/__init__.py | 1 - esphome/components/inkplate6/display.py | 215 +--------------- .../{inkplate6 => inkplate}/common.yaml | 4 +- .../test.esp32-ard.yaml | 0 .../test.esp32-idf.yaml | 0 11 files changed, 534 insertions(+), 358 deletions(-) create mode 100644 esphome/components/inkplate/__init__.py create mode 100644 esphome/components/inkplate/const.py create mode 100644 esphome/components/inkplate/display.py rename esphome/components/{inkplate6 => inkplate}/inkplate.cpp (82%) rename esphome/components/{inkplate6 => inkplate}/inkplate.h (56%) rename tests/components/{inkplate6 => inkplate}/common.yaml (96%) rename tests/components/{inkplate6 => inkplate}/test.esp32-ard.yaml (100%) rename tests/components/{inkplate6 => inkplate}/test.esp32-idf.yaml (100%) diff --git a/CODEOWNERS b/CODEOWNERS index acf8acb2ab..dc567ca5c0 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -234,7 +234,7 @@ esphome/components/ina2xx_base/* @latonita esphome/components/ina2xx_i2c/* @latonita esphome/components/ina2xx_spi/* @latonita esphome/components/inkbird_ibsth1_mini/* @fkirill -esphome/components/inkplate6/* @jesserockz +esphome/components/inkplate/* @jesserockz @JosipKuci esphome/components/integration/* @OttoWinter esphome/components/internal_temperature/* @Mat931 esphome/components/interval/* @esphome/core diff --git a/esphome/components/inkplate/__init__.py b/esphome/components/inkplate/__init__.py new file mode 100644 index 0000000000..1c6013793a --- /dev/null +++ b/esphome/components/inkplate/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@jesserockz", "@JosipKuci"] diff --git a/esphome/components/inkplate/const.py b/esphome/components/inkplate/const.py new file mode 100644 index 0000000000..77bf933320 --- /dev/null +++ b/esphome/components/inkplate/const.py @@ -0,0 +1,105 @@ +WAVEFORMS = { + "inkplate_6": ( + (0, 1, 1, 0, 0, 1, 1, 0, 0), + (0, 1, 2, 1, 1, 2, 1, 0, 0), + (1, 1, 1, 2, 2, 1, 0, 0, 0), + (0, 0, 0, 1, 1, 1, 2, 0, 0), + (2, 1, 1, 1, 2, 1, 2, 0, 0), + (2, 2, 1, 1, 2, 1, 2, 0, 0), + (1, 1, 1, 2, 1, 2, 2, 0, 0), + (0, 0, 0, 0, 0, 0, 2, 0, 0), + ), + "inkplate_10": ( + (0, 0, 0, 0, 0, 0, 0, 1, 0), + (0, 0, 0, 2, 2, 2, 1, 1, 0), + (0, 0, 2, 1, 1, 2, 2, 1, 0), + (0, 1, 2, 2, 1, 2, 2, 1, 0), + (0, 0, 2, 1, 2, 2, 2, 1, 0), + (0, 2, 2, 2, 2, 2, 2, 1, 0), + (0, 0, 0, 0, 0, 2, 1, 2, 0), + (0, 0, 0, 2, 2, 2, 2, 2, 0), + ), + "inkplate_6_plus": ( + (0, 0, 0, 0, 0, 2, 1, 1, 0), + (0, 0, 2, 1, 1, 1, 2, 1, 0), + (0, 2, 2, 2, 1, 1, 2, 1, 0), + (0, 0, 2, 2, 2, 1, 2, 1, 0), + (0, 0, 0, 0, 2, 2, 2, 1, 0), + (0, 0, 2, 1, 2, 1, 1, 2, 0), + (0, 0, 2, 2, 2, 1, 1, 2, 0), + (0, 0, 0, 0, 2, 2, 2, 2, 0), + ), + "inkplate_6_v2": ( + (1, 0, 1, 0, 1, 1, 1, 0, 0), + (0, 0, 0, 1, 1, 1, 1, 0, 0), + (1, 1, 1, 1, 0, 2, 1, 0, 0), + (1, 1, 1, 2, 2, 1, 1, 0, 0), + (1, 1, 1, 1, 2, 2, 1, 0, 0), + (0, 1, 1, 1, 2, 2, 1, 0, 0), + (0, 0, 0, 0, 1, 1, 2, 0, 0), + (0, 0, 0, 0, 0, 1, 2, 0, 0), + ), + "inkplate_5": ( + (0, 0, 1, 1, 0, 1, 1, 1, 0), + (0, 1, 1, 1, 1, 2, 0, 1, 0), + (1, 2, 2, 0, 2, 1, 1, 1, 0), + (1, 1, 1, 2, 0, 1, 1, 2, 0), + (0, 1, 1, 1, 2, 0, 1, 2, 0), + (0, 0, 0, 1, 1, 2, 1, 2, 0), + (1, 1, 1, 2, 0, 2, 1, 2, 0), + (0, 0, 0, 0, 0, 0, 0, 0, 0), + ), + "inkplate_5_v2": ( + (0, 0, 1, 1, 2, 1, 1, 1, 0), + (1, 1, 2, 2, 1, 2, 1, 1, 0), + (0, 1, 2, 2, 1, 1, 2, 1, 0), + (0, 0, 1, 1, 1, 1, 1, 2, 0), + (1, 2, 1, 2, 1, 1, 1, 2, 0), + (0, 1, 1, 1, 2, 0, 1, 2, 0), + (1, 1, 1, 2, 2, 2, 1, 2, 0), + (0, 0, 0, 0, 0, 0, 0, 0, 0), + ), +} + +INKPLATE_10_CUSTOM_WAVEFORMS = ( + ( + (0, 0, 0, 0, 0, 0, 0, 0, 0), + (0, 0, 0, 2, 1, 2, 1, 1, 0), + (0, 0, 0, 2, 2, 1, 2, 1, 0), + (0, 0, 2, 2, 1, 2, 2, 1, 0), + (0, 0, 0, 2, 1, 1, 1, 2, 0), + (0, 0, 2, 2, 2, 1, 1, 2, 0), + (0, 0, 0, 0, 0, 1, 2, 2, 0), + (0, 0, 0, 0, 2, 2, 2, 2, 0), + ), + ( + (0, 3, 3, 3, 3, 3, 3, 3, 0), + (0, 1, 2, 1, 1, 2, 2, 1, 0), + (0, 2, 2, 2, 1, 2, 2, 1, 0), + (0, 0, 2, 2, 2, 2, 2, 1, 0), + (0, 3, 3, 2, 1, 1, 1, 2, 0), + (0, 3, 3, 2, 2, 1, 1, 2, 0), + (0, 2, 1, 2, 1, 2, 1, 2, 0), + (0, 3, 3, 3, 2, 2, 2, 2, 0), + ), + ( + (0, 0, 0, 0, 0, 0, 0, 1, 0), + (0, 0, 0, 2, 2, 2, 1, 1, 0), + (0, 0, 2, 1, 1, 2, 2, 1, 0), + (1, 1, 2, 2, 1, 2, 2, 1, 0), + (0, 0, 2, 1, 2, 2, 2, 1, 0), + (0, 1, 2, 2, 2, 2, 2, 1, 0), + (0, 0, 0, 2, 2, 2, 1, 2, 0), + (0, 0, 0, 2, 2, 2, 2, 2, 0), + ), + ( + (0, 0, 0, 0, 0, 0, 0, 1, 0), + (0, 0, 0, 2, 2, 2, 1, 1, 0), + (2, 2, 2, 1, 0, 2, 1, 0, 0), + (2, 1, 1, 2, 1, 1, 1, 2, 0), + (2, 2, 2, 1, 1, 1, 0, 2, 0), + (2, 2, 2, 1, 1, 2, 1, 2, 0), + (0, 0, 0, 0, 2, 1, 2, 2, 0), + (0, 0, 0, 0, 2, 2, 2, 2, 0), + ), +) diff --git a/esphome/components/inkplate/display.py b/esphome/components/inkplate/display.py new file mode 100644 index 0000000000..a0b0265cf1 --- /dev/null +++ b/esphome/components/inkplate/display.py @@ -0,0 +1,238 @@ +from esphome import pins +import esphome.codegen as cg +from esphome.components import display, i2c +from esphome.components.esp32 import CONF_CPU_FREQUENCY +import esphome.config_validation as cv +from esphome.const import ( + CONF_FULL_UPDATE_EVERY, + CONF_ID, + CONF_LAMBDA, + CONF_MIRROR_X, + CONF_MIRROR_Y, + CONF_MODEL, + CONF_OE_PIN, + CONF_PAGES, + CONF_TRANSFORM, + CONF_WAKEUP_PIN, + PLATFORM_ESP32, +) +import esphome.final_validate as fv + +from .const import INKPLATE_10_CUSTOM_WAVEFORMS, WAVEFORMS + +DEPENDENCIES = ["i2c", "esp32"] +AUTO_LOAD = ["psram"] + +CONF_DISPLAY_DATA_0_PIN = "display_data_0_pin" +CONF_DISPLAY_DATA_1_PIN = "display_data_1_pin" +CONF_DISPLAY_DATA_2_PIN = "display_data_2_pin" +CONF_DISPLAY_DATA_3_PIN = "display_data_3_pin" +CONF_DISPLAY_DATA_4_PIN = "display_data_4_pin" +CONF_DISPLAY_DATA_5_PIN = "display_data_5_pin" +CONF_DISPLAY_DATA_6_PIN = "display_data_6_pin" +CONF_DISPLAY_DATA_7_PIN = "display_data_7_pin" + +CONF_CL_PIN = "cl_pin" +CONF_CKV_PIN = "ckv_pin" +CONF_GREYSCALE = "greyscale" +CONF_GMOD_PIN = "gmod_pin" +CONF_GPIO0_ENABLE_PIN = "gpio0_enable_pin" +CONF_LE_PIN = "le_pin" +CONF_PARTIAL_UPDATING = "partial_updating" +CONF_POWERUP_PIN = "powerup_pin" +CONF_SPH_PIN = "sph_pin" +CONF_SPV_PIN = "spv_pin" +CONF_VCOM_PIN = "vcom_pin" + +inkplate_ns = cg.esphome_ns.namespace("inkplate") +Inkplate = inkplate_ns.class_( + "Inkplate", + cg.PollingComponent, + i2c.I2CDevice, + display.Display, + display.DisplayBuffer, +) + +InkplateModel = inkplate_ns.enum("InkplateModel") + +MODELS = { + "inkplate_6": InkplateModel.INKPLATE_6, + "inkplate_10": InkplateModel.INKPLATE_10, + "inkplate_6_plus": InkplateModel.INKPLATE_6_PLUS, + "inkplate_6_v2": InkplateModel.INKPLATE_6_V2, + "inkplate_5": InkplateModel.INKPLATE_5, + "inkplate_5_v2": InkplateModel.INKPLATE_5_V2, +} + +CONF_CUSTOM_WAVEFORM = "custom_waveform" + + +def _validate_custom_waveform(config): + if CONF_CUSTOM_WAVEFORM in config and config[CONF_MODEL] != "inkplate_10": + raise cv.Invalid("Custom waveforms are only supported on the Inkplate 10") + return config + + +CONFIG_SCHEMA = cv.All( + display.FULL_DISPLAY_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(Inkplate), + cv.Optional(CONF_GREYSCALE, default=False): cv.boolean, + cv.Optional(CONF_CUSTOM_WAVEFORM): cv.All( + cv.uint8_t, cv.Range(min=1, max=len(INKPLATE_10_CUSTOM_WAVEFORMS)) + ), + cv.Optional(CONF_TRANSFORM): cv.Schema( + { + cv.Optional(CONF_MIRROR_X, default=False): cv.boolean, + cv.Optional(CONF_MIRROR_Y, default=False): cv.boolean, + } + ), + cv.Optional(CONF_PARTIAL_UPDATING, default=True): cv.boolean, + cv.Optional(CONF_FULL_UPDATE_EVERY, default=10): cv.uint32_t, + cv.Optional(CONF_MODEL, default="inkplate_6"): cv.enum( + MODELS, lower=True, space="_" + ), + # Control pins + cv.Required(CONF_CKV_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_GMOD_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_GPIO0_ENABLE_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_OE_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_POWERUP_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_SPH_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_SPV_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_VCOM_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_WAKEUP_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_CL_PIN, default=0): pins.internal_gpio_output_pin_schema, + cv.Optional(CONF_LE_PIN, default=2): pins.internal_gpio_output_pin_schema, + # Data pins + cv.Optional( + CONF_DISPLAY_DATA_0_PIN, default=4 + ): pins.internal_gpio_output_pin_schema, + cv.Optional( + CONF_DISPLAY_DATA_1_PIN, default=5 + ): pins.internal_gpio_output_pin_schema, + cv.Optional( + CONF_DISPLAY_DATA_2_PIN, default=18 + ): pins.internal_gpio_output_pin_schema, + cv.Optional( + CONF_DISPLAY_DATA_3_PIN, default=19 + ): pins.internal_gpio_output_pin_schema, + cv.Optional( + CONF_DISPLAY_DATA_4_PIN, default=23 + ): pins.internal_gpio_output_pin_schema, + cv.Optional( + CONF_DISPLAY_DATA_5_PIN, default=25 + ): pins.internal_gpio_output_pin_schema, + cv.Optional( + CONF_DISPLAY_DATA_6_PIN, default=26 + ): pins.internal_gpio_output_pin_schema, + cv.Optional( + CONF_DISPLAY_DATA_7_PIN, default=27 + ): pins.internal_gpio_output_pin_schema, + } + ) + .extend(cv.polling_component_schema("5s")) + .extend(i2c.i2c_device_schema(0x48)), + cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA), + _validate_custom_waveform, +) + + +def _validate_cpu_frequency(config): + esp32_config = fv.full_config.get()[PLATFORM_ESP32] + if esp32_config[CONF_CPU_FREQUENCY] != "240MHZ": + raise cv.Invalid( + "Inkplate requires 240MHz CPU frequency (set in esp32 component)" + ) + return config + + +FINAL_VALIDATE_SCHEMA = _validate_cpu_frequency + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + + await display.register_display(var, config) + await i2c.register_i2c_device(var, config) + + if CONF_LAMBDA in config: + lambda_ = await cg.process_lambda( + config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void + ) + cg.add(var.set_writer(lambda_)) + + cg.add(var.set_greyscale(config[CONF_GREYSCALE])) + if transform := config.get(CONF_TRANSFORM): + cg.add(var.set_mirror_x(transform[CONF_MIRROR_X])) + cg.add(var.set_mirror_y(transform[CONF_MIRROR_Y])) + cg.add(var.set_partial_updating(config[CONF_PARTIAL_UPDATING])) + cg.add(var.set_full_update_every(config[CONF_FULL_UPDATE_EVERY])) + + cg.add(var.set_model(config[CONF_MODEL])) + + if custom_waveform := config.get(CONF_CUSTOM_WAVEFORM): + waveform = INKPLATE_10_CUSTOM_WAVEFORMS[custom_waveform - 1] + waveform = [element for tupl in waveform for element in tupl] + cg.add(var.set_waveform(waveform, True)) + else: + waveform = WAVEFORMS[config[CONF_MODEL]] + waveform = [element for tupl in waveform for element in tupl] + cg.add(var.set_waveform(waveform, False)) + + ckv = await cg.gpio_pin_expression(config[CONF_CKV_PIN]) + cg.add(var.set_ckv_pin(ckv)) + + gmod = await cg.gpio_pin_expression(config[CONF_GMOD_PIN]) + cg.add(var.set_gmod_pin(gmod)) + + gpio0_enable = await cg.gpio_pin_expression(config[CONF_GPIO0_ENABLE_PIN]) + cg.add(var.set_gpio0_enable_pin(gpio0_enable)) + + oe = await cg.gpio_pin_expression(config[CONF_OE_PIN]) + cg.add(var.set_oe_pin(oe)) + + powerup = await cg.gpio_pin_expression(config[CONF_POWERUP_PIN]) + cg.add(var.set_powerup_pin(powerup)) + + sph = await cg.gpio_pin_expression(config[CONF_SPH_PIN]) + cg.add(var.set_sph_pin(sph)) + + spv = await cg.gpio_pin_expression(config[CONF_SPV_PIN]) + cg.add(var.set_spv_pin(spv)) + + vcom = await cg.gpio_pin_expression(config[CONF_VCOM_PIN]) + cg.add(var.set_vcom_pin(vcom)) + + wakeup = await cg.gpio_pin_expression(config[CONF_WAKEUP_PIN]) + cg.add(var.set_wakeup_pin(wakeup)) + + cl = await cg.gpio_pin_expression(config[CONF_CL_PIN]) + cg.add(var.set_cl_pin(cl)) + + le = await cg.gpio_pin_expression(config[CONF_LE_PIN]) + cg.add(var.set_le_pin(le)) + + display_data_0 = await cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_0_PIN]) + cg.add(var.set_display_data_0_pin(display_data_0)) + + display_data_1 = await cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_1_PIN]) + cg.add(var.set_display_data_1_pin(display_data_1)) + + display_data_2 = await cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_2_PIN]) + cg.add(var.set_display_data_2_pin(display_data_2)) + + display_data_3 = await cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_3_PIN]) + cg.add(var.set_display_data_3_pin(display_data_3)) + + display_data_4 = await cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_4_PIN]) + cg.add(var.set_display_data_4_pin(display_data_4)) + + display_data_5 = await cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_5_PIN]) + cg.add(var.set_display_data_5_pin(display_data_5)) + + display_data_6 = await cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_6_PIN]) + cg.add(var.set_display_data_6_pin(display_data_6)) + + display_data_7 = await cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_7_PIN]) + cg.add(var.set_display_data_7_pin(display_data_7)) diff --git a/esphome/components/inkplate6/inkplate.cpp b/esphome/components/inkplate/inkplate.cpp similarity index 82% rename from esphome/components/inkplate6/inkplate.cpp rename to esphome/components/inkplate/inkplate.cpp index b3d0b87e83..f96fb6905e 100644 --- a/esphome/components/inkplate6/inkplate.cpp +++ b/esphome/components/inkplate/inkplate.cpp @@ -6,11 +6,11 @@ #include namespace esphome { -namespace inkplate6 { +namespace inkplate { static const char *const TAG = "inkplate"; -void Inkplate6::setup() { +void Inkplate::setup() { for (uint32_t i = 0; i < 256; i++) { this->pin_lut_[i] = ((i & 0b00000011) << 4) | (((i & 0b00001100) >> 2) << 18) | (((i & 0b00010000) >> 4) << 23) | (((i & 0b11100000) >> 5) << 25); @@ -56,7 +56,7 @@ void Inkplate6::setup() { /** * Allocate buffers. May be called after setup to re-initialise if e.g. greyscale is changed. */ -void Inkplate6::initialize_() { +void Inkplate::initialize_() { RAMAllocator allocator; RAMAllocator allocator32; uint32_t buffer_size = this->get_buffer_length_(); @@ -81,29 +81,25 @@ void Inkplate6::initialize_() { return; } if (this->greyscale_) { - uint8_t glut_size = 9; - - this->glut_ = allocator32.allocate(256 * glut_size); + this->glut_ = allocator32.allocate(256 * GLUT_SIZE); if (this->glut_ == nullptr) { ESP_LOGE(TAG, "Could not allocate glut!"); this->mark_failed(); return; } - this->glut2_ = allocator32.allocate(256 * glut_size); + this->glut2_ = allocator32.allocate(256 * GLUT_SIZE); if (this->glut2_ == nullptr) { ESP_LOGE(TAG, "Could not allocate glut2!"); this->mark_failed(); return; } - const auto *const waveform3_bit = waveform3BitAll[this->model_]; - - for (int i = 0; i < glut_size; i++) { + for (uint8_t i = 0; i < GLUT_SIZE; i++) { for (uint32_t j = 0; j < 256; j++) { - uint8_t z = (waveform3_bit[j & 0x07][i] << 2) | (waveform3_bit[(j >> 4) & 0x07][i]); + uint8_t z = (this->waveform_[j & 0x07][i] << 2) | (this->waveform_[(j >> 4) & 0x07][i]); this->glut_[i * 256 + j] = ((z & 0b00000011) << 4) | (((z & 0b00001100) >> 2) << 18) | (((z & 0b00010000) >> 4) << 23) | (((z & 0b11100000) >> 5) << 25); - z = ((waveform3_bit[j & 0x07][i] << 2) | (waveform3_bit[(j >> 4) & 0x07][i])) << 4; + z = ((this->waveform_[j & 0x07][i] << 2) | (this->waveform_[(j >> 4) & 0x07][i])) << 4; this->glut2_[i * 256 + j] = ((z & 0b00000011) << 4) | (((z & 0b00001100) >> 2) << 18) | (((z & 0b00010000) >> 4) << 23) | (((z & 0b11100000) >> 5) << 25); } @@ -130,9 +126,9 @@ void Inkplate6::initialize_() { memset(this->buffer_, 0, buffer_size); } -float Inkplate6::get_setup_priority() const { return setup_priority::PROCESSOR; } +float Inkplate::get_setup_priority() const { return setup_priority::PROCESSOR; } -size_t Inkplate6::get_buffer_length_() { +size_t Inkplate::get_buffer_length_() { if (this->greyscale_) { return size_t(this->get_width_internal()) * size_t(this->get_height_internal()) / 2u; } else { @@ -140,7 +136,7 @@ size_t Inkplate6::get_buffer_length_() { } } -void Inkplate6::update() { +void Inkplate::update() { this->do_update_(); if (this->full_update_every_ > 0 && this->partial_updates_ >= this->full_update_every_) { @@ -150,7 +146,7 @@ void Inkplate6::update() { this->display(); } -void HOT Inkplate6::draw_absolute_pixel_internal(int x, int y, Color color) { +void HOT Inkplate::draw_absolute_pixel_internal(int x, int y, Color color) { if (x >= this->get_width_internal() || y >= this->get_height_internal() || x < 0 || y < 0) return; @@ -171,18 +167,18 @@ void HOT Inkplate6::draw_absolute_pixel_internal(int x, int y, Color color) { // uint8_t gs = (uint8_t)(px*7); uint8_t gs = ((color.red * 2126 / 10000) + (color.green * 7152 / 10000) + (color.blue * 722 / 10000)) >> 5; - this->buffer_[pos] = (pixelMaskGLUT[x_sub] & current) | (x_sub ? gs : gs << 4); + this->buffer_[pos] = (PIXEL_MASK_GLUT[x_sub] & current) | (x_sub ? gs : gs << 4); } else { int x1 = x / 8; int x_sub = x % 8; uint32_t pos = (x1 + y * (this->get_width_internal() / 8)); uint8_t current = this->partial_buffer_[pos]; - this->partial_buffer_[pos] = (~pixelMaskLUT[x_sub] & current) | (color.is_on() ? 0 : pixelMaskLUT[x_sub]); + this->partial_buffer_[pos] = (~PIXEL_MASK_LUT[x_sub] & current) | (color.is_on() ? 0 : PIXEL_MASK_LUT[x_sub]); } } -void Inkplate6::dump_config() { +void Inkplate::dump_config() { LOG_DISPLAY("", "Inkplate", this); ESP_LOGCONFIG(TAG, " Greyscale: %s\n" @@ -214,7 +210,7 @@ void Inkplate6::dump_config() { LOG_UPDATE_INTERVAL(this); } -void Inkplate6::eink_off_() { +void Inkplate::eink_off_() { ESP_LOGV(TAG, "Eink off called"); if (!panel_on_) return; @@ -242,7 +238,7 @@ void Inkplate6::eink_off_() { pins_z_state_(); } -void Inkplate6::eink_on_() { +void Inkplate::eink_on_() { ESP_LOGV(TAG, "Eink on called"); if (panel_on_) return; @@ -284,7 +280,7 @@ void Inkplate6::eink_on_() { this->oe_pin_->digital_write(true); } -bool Inkplate6::read_power_status_() { +bool Inkplate::read_power_status_() { uint8_t data; auto err = this->read_register(0x0F, &data, 1); if (err == i2c::ERROR_OK) { @@ -293,7 +289,7 @@ bool Inkplate6::read_power_status_() { return false; } -void Inkplate6::fill(Color color) { +void Inkplate::fill(Color color) { ESP_LOGV(TAG, "Fill called"); uint32_t start_time = millis(); @@ -308,7 +304,7 @@ void Inkplate6::fill(Color color) { ESP_LOGV(TAG, "Fill finished (%ums)", millis() - start_time); } -void Inkplate6::display() { +void Inkplate::display() { ESP_LOGV(TAG, "Display called"); uint32_t start_time = millis(); @@ -324,7 +320,7 @@ void Inkplate6::display() { ESP_LOGV(TAG, "Display finished (full) (%ums)", millis() - start_time); } -void Inkplate6::display1b_() { +void Inkplate::display1b_() { ESP_LOGV(TAG, "Display1b called"); uint32_t start_time = millis(); @@ -334,32 +330,71 @@ void Inkplate6::display1b_() { uint8_t buffer_value; const uint8_t *buffer_ptr; eink_on_(); - if (this->model_ == INKPLATE_6_PLUS) { - clean_fast_(0, 1); - clean_fast_(1, 15); - clean_fast_(2, 1); - clean_fast_(0, 5); - clean_fast_(2, 1); - clean_fast_(1, 15); - } else { - clean_fast_(0, 1); - clean_fast_(1, 21); - clean_fast_(2, 1); - clean_fast_(0, 12); - clean_fast_(2, 1); - clean_fast_(1, 21); - clean_fast_(2, 1); - clean_fast_(0, 12); - clean_fast_(2, 1); + uint8_t rep = 4; + switch (this->model_) { + case INKPLATE_10: + clean_fast_(0, 1); + clean_fast_(1, 10); + clean_fast_(2, 1); + clean_fast_(0, 10); + clean_fast_(2, 1); + clean_fast_(1, 10); + clean_fast_(2, 1); + clean_fast_(0, 10); + rep = 5; + break; + case INKPLATE_6_PLUS: + clean_fast_(0, 1); + clean_fast_(1, 15); + clean_fast_(2, 1); + clean_fast_(0, 5); + clean_fast_(2, 1); + clean_fast_(1, 15); + break; + case INKPLATE_6: + case INKPLATE_6_V2: + clean_fast_(0, 1); + clean_fast_(1, 18); + clean_fast_(2, 1); + clean_fast_(0, 18); + clean_fast_(2, 1); + clean_fast_(1, 18); + clean_fast_(2, 1); + clean_fast_(0, 18); + clean_fast_(2, 1); + if (this->model_ == INKPLATE_6_V2) + rep = 5; + break; + case INKPLATE_5: + clean_fast_(0, 1); + clean_fast_(1, 14); + clean_fast_(2, 1); + clean_fast_(0, 14); + clean_fast_(2, 1); + clean_fast_(1, 14); + clean_fast_(2, 1); + clean_fast_(0, 14); + clean_fast_(2, 1); + rep = 5; + break; + case INKPLATE_5_V2: + clean_fast_(0, 1); + clean_fast_(1, 11); + clean_fast_(2, 1); + clean_fast_(0, 11); + clean_fast_(2, 1); + clean_fast_(1, 11); + clean_fast_(2, 1); + clean_fast_(0, 11); + rep = 3; + break; } uint32_t clock = (1 << this->cl_pin_->get_pin()); uint32_t data_mask = this->get_data_pin_mask_(); ESP_LOGV(TAG, "Display1b start loops (%ums)", millis() - start_time); - int rep = (this->model_ == INKPLATE_6_V2) ? 5 : 4; - - for (int k = 0; k < rep; k++) { + for (uint8_t k = 0; k < rep; k++) { buffer_ptr = &this->buffer_[this->get_buffer_length_() - 1]; vscan_start_(); for (int i = 0, im = this->get_height_internal(); i < im; i++) { @@ -452,28 +487,75 @@ void Inkplate6::display1b_() { ESP_LOGV(TAG, "Display1b finished (%ums)", millis() - start_time); } -void Inkplate6::display3b_() { +void Inkplate::display3b_() { ESP_LOGV(TAG, "Display3b called"); uint32_t start_time = millis(); eink_on_(); - if (this->model_ == INKPLATE_6_PLUS) { - clean_fast_(0, 1); - clean_fast_(1, 15); - clean_fast_(2, 1); - clean_fast_(0, 5); - clean_fast_(2, 1); - clean_fast_(1, 15); - } else { - clean_fast_(0, 1); - clean_fast_(1, 21); - clean_fast_(2, 1); - clean_fast_(0, 12); - clean_fast_(2, 1); - clean_fast_(1, 21); - clean_fast_(2, 1); - clean_fast_(0, 12); - clean_fast_(2, 1); + + switch (this->model_) { + case INKPLATE_10: + if (this->custom_waveform_) { + clean_fast_(1, 1); + clean_fast_(0, 7); + clean_fast_(2, 1); + clean_fast_(1, 12); + clean_fast_(2, 1); + clean_fast_(0, 7); + clean_fast_(2, 1); + clean_fast_(1, 12); + } else { + clean_fast_(1, 1); + clean_fast_(0, 10); + clean_fast_(2, 1); + clean_fast_(1, 10); + clean_fast_(2, 1); + clean_fast_(0, 10); + clean_fast_(2, 1); + clean_fast_(1, 10); + } + break; + case INKPLATE_6_PLUS: + clean_fast_(0, 1); + clean_fast_(1, 15); + clean_fast_(2, 1); + clean_fast_(0, 5); + clean_fast_(2, 1); + clean_fast_(1, 15); + break; + case INKPLATE_6: + case INKPLATE_6_V2: + clean_fast_(0, 1); + clean_fast_(1, 18); + clean_fast_(2, 1); + clean_fast_(0, 18); + clean_fast_(2, 1); + clean_fast_(1, 18); + clean_fast_(2, 1); + clean_fast_(0, 18); + clean_fast_(2, 1); + break; + case INKPLATE_5: + clean_fast_(0, 1); + clean_fast_(1, 14); + clean_fast_(2, 1); + clean_fast_(0, 14); + clean_fast_(2, 1); + clean_fast_(1, 14); + clean_fast_(2, 1); + clean_fast_(0, 14); + clean_fast_(2, 1); + break; + case INKPLATE_5_V2: + clean_fast_(0, 1); + clean_fast_(1, 11); + clean_fast_(2, 1); + clean_fast_(0, 11); + clean_fast_(2, 1); + clean_fast_(1, 11); + clean_fast_(2, 1); + clean_fast_(0, 11); + break; } uint32_t clock = (1 << this->cl_pin_->get_pin()); @@ -518,7 +600,7 @@ void Inkplate6::display3b_() { ESP_LOGV(TAG, "Display3b finished (%ums)", millis() - start_time); } -bool Inkplate6::partial_update_() { +bool Inkplate::partial_update_() { ESP_LOGV(TAG, "Partial update called"); uint32_t start_time = millis(); if (this->greyscale_) @@ -560,7 +642,7 @@ bool Inkplate6::partial_update_() { GPIO.out_w1ts = this->pin_lut_[data] | clock; GPIO.out_w1tc = data_mask | clock; } - // New Inkplate6 panel doesn't need last clock + // New Inkplate panel doesn't need last clock if (this->model_ != INKPLATE_6_V2) { GPIO.out_w1ts = clock; GPIO.out_w1tc = data_mask | clock; @@ -580,7 +662,7 @@ bool Inkplate6::partial_update_() { return true; } -void Inkplate6::vscan_start_() { +void Inkplate::vscan_start_() { this->ckv_pin_->digital_write(true); delayMicroseconds(7); this->spv_pin_->digital_write(false); @@ -604,7 +686,7 @@ void Inkplate6::vscan_start_() { this->ckv_pin_->digital_write(true); } -void Inkplate6::hscan_start_(uint32_t d) { +void Inkplate::hscan_start_(uint32_t d) { uint8_t clock = (1 << this->cl_pin_->get_pin()); this->sph_pin_->digital_write(false); GPIO.out_w1ts = d | clock; @@ -613,14 +695,14 @@ void Inkplate6::hscan_start_(uint32_t d) { this->ckv_pin_->digital_write(true); } -void Inkplate6::vscan_end_() { +void Inkplate::vscan_end_() { this->ckv_pin_->digital_write(false); this->le_pin_->digital_write(true); this->le_pin_->digital_write(false); delayMicroseconds(0); } -void Inkplate6::clean() { +void Inkplate::clean() { ESP_LOGV(TAG, "Clean called"); uint32_t start_time = millis(); @@ -634,7 +716,7 @@ void Inkplate6::clean() { ESP_LOGV(TAG, "Clean finished (%ums)", millis() - start_time); } -void Inkplate6::clean_fast_(uint8_t c, uint8_t rep) { +void Inkplate::clean_fast_(uint8_t c, uint8_t rep) { ESP_LOGV(TAG, "Clean fast called with: (%d, %d)", c, rep); uint32_t start_time = millis(); @@ -666,7 +748,7 @@ void Inkplate6::clean_fast_(uint8_t c, uint8_t rep) { GPIO.out_w1ts = clock; GPIO.out_w1tc = clock; } - // New Inkplate6 panel doesn't need last clock + // New Inkplate panel doesn't need last clock if (this->model_ != INKPLATE_6_V2) { GPIO.out_w1ts = send | clock; GPIO.out_w1tc = clock; @@ -679,7 +761,7 @@ void Inkplate6::clean_fast_(uint8_t c, uint8_t rep) { ESP_LOGV(TAG, "Clean fast finished (%ums)", millis() - start_time); } -void Inkplate6::pins_z_state_() { +void Inkplate::pins_z_state_() { this->cl_pin_->pin_mode(gpio::FLAG_INPUT); this->le_pin_->pin_mode(gpio::FLAG_INPUT); this->ckv_pin_->pin_mode(gpio::FLAG_INPUT); @@ -699,7 +781,7 @@ void Inkplate6::pins_z_state_() { this->display_data_7_pin_->pin_mode(gpio::FLAG_INPUT); } -void Inkplate6::pins_as_outputs_() { +void Inkplate::pins_as_outputs_() { this->cl_pin_->pin_mode(gpio::FLAG_OUTPUT); this->le_pin_->pin_mode(gpio::FLAG_OUTPUT); this->ckv_pin_->pin_mode(gpio::FLAG_OUTPUT); @@ -719,5 +801,5 @@ void Inkplate6::pins_as_outputs_() { this->display_data_7_pin_->pin_mode(gpio::FLAG_OUTPUT); } -} // namespace inkplate6 +} // namespace inkplate } // namespace esphome diff --git a/esphome/components/inkplate6/inkplate.h b/esphome/components/inkplate/inkplate.h similarity index 56% rename from esphome/components/inkplate6/inkplate.h rename to esphome/components/inkplate/inkplate.h index d8918bdf2a..fb4674b522 100644 --- a/esphome/components/inkplate6/inkplate.h +++ b/esphome/components/inkplate/inkplate.h @@ -5,8 +5,10 @@ #include "esphome/core/component.h" #include "esphome/core/hal.h" +#include + namespace esphome { -namespace inkplate6 { +namespace inkplate { enum InkplateModel : uint8_t { INKPLATE_6 = 0, @@ -17,79 +19,35 @@ enum InkplateModel : uint8_t { INKPLATE_5_V2 = 5, }; -class Inkplate6 : public display::DisplayBuffer, public i2c::I2CDevice { +static constexpr uint8_t GLUT_SIZE = 9; +static constexpr uint8_t GLUT_COUNT = 8; + +static constexpr uint8_t LUT2[16] = {0xAA, 0xA9, 0xA6, 0xA5, 0x9A, 0x99, 0x96, 0x95, + 0x6A, 0x69, 0x66, 0x65, 0x5A, 0x59, 0x56, 0x55}; +static constexpr uint8_t LUTW[16] = {0xFF, 0xFE, 0xFB, 0xFA, 0xEF, 0xEE, 0xEB, 0xEA, + 0xBF, 0xBE, 0xBB, 0xBA, 0xAF, 0xAE, 0xAB, 0xAA}; +static constexpr uint8_t LUTB[16] = {0xFF, 0xFD, 0xF7, 0xF5, 0xDF, 0xDD, 0xD7, 0xD5, + 0x7F, 0x7D, 0x77, 0x75, 0x5F, 0x5D, 0x57, 0x55}; + +static constexpr uint8_t PIXEL_MASK_LUT[8] = {0x1, 0x2, 0x4, 0x8, 0x10, 0x20, 0x40, 0x80}; +static constexpr uint8_t PIXEL_MASK_GLUT[2] = {0x0F, 0xF0}; + +class Inkplate : public display::DisplayBuffer, public i2c::I2CDevice { public: - const uint8_t LUT2[16] = {0xAA, 0xA9, 0xA6, 0xA5, 0x9A, 0x99, 0x96, 0x95, - 0x6A, 0x69, 0x66, 0x65, 0x5A, 0x59, 0x56, 0x55}; - const uint8_t LUTW[16] = {0xFF, 0xFE, 0xFB, 0xFA, 0xEF, 0xEE, 0xEB, 0xEA, - 0xBF, 0xBE, 0xBB, 0xBA, 0xAF, 0xAE, 0xAB, 0xAA}; - const uint8_t LUTB[16] = {0xFF, 0xFD, 0xF7, 0xF5, 0xDF, 0xDD, 0xD7, 0xD5, - 0x7F, 0x7D, 0x77, 0x75, 0x5F, 0x5D, 0x57, 0x55}; - - const uint8_t pixelMaskLUT[8] = {0x1, 0x2, 0x4, 0x8, 0x10, 0x20, 0x40, 0x80}; - const uint8_t pixelMaskGLUT[2] = {0x0F, 0xF0}; - - const uint8_t waveform3BitAll[6][8][9] = {// INKPLATE_6 - {{0, 1, 1, 0, 0, 1, 1, 0, 0}, - {0, 1, 2, 1, 1, 2, 1, 0, 0}, - {1, 1, 1, 2, 2, 1, 0, 0, 0}, - {0, 0, 0, 1, 1, 1, 2, 0, 0}, - {2, 1, 1, 1, 2, 1, 2, 0, 0}, - {2, 2, 1, 1, 2, 1, 2, 0, 0}, - {1, 1, 1, 2, 1, 2, 2, 0, 0}, - {0, 0, 0, 0, 0, 0, 2, 0, 0}}, - // INKPLATE_10 - {{0, 0, 0, 0, 0, 0, 0, 1, 0}, - {0, 0, 0, 2, 2, 2, 1, 1, 0}, - {0, 0, 2, 1, 1, 2, 2, 1, 0}, - {0, 1, 2, 2, 1, 2, 2, 1, 0}, - {0, 0, 2, 1, 2, 2, 2, 1, 0}, - {0, 2, 2, 2, 2, 2, 2, 1, 0}, - {0, 0, 0, 0, 0, 2, 1, 2, 0}, - {0, 0, 0, 2, 2, 2, 2, 2, 0}}, - // INKPLATE_6_PLUS - {{0, 0, 0, 0, 0, 2, 1, 1, 0}, - {0, 0, 2, 1, 1, 1, 2, 1, 0}, - {0, 2, 2, 2, 1, 1, 2, 1, 0}, - {0, 0, 2, 2, 2, 1, 2, 1, 0}, - {0, 0, 0, 0, 2, 2, 2, 1, 0}, - {0, 0, 2, 1, 2, 1, 1, 2, 0}, - {0, 0, 2, 2, 2, 1, 1, 2, 0}, - {0, 0, 0, 0, 2, 2, 2, 2, 0}}, - // INKPLATE_6_V2 - {{1, 0, 1, 0, 1, 1, 1, 0, 0}, - {0, 0, 0, 1, 1, 1, 1, 0, 0}, - {1, 1, 1, 1, 0, 2, 1, 0, 0}, - {1, 1, 1, 2, 2, 1, 1, 0, 0}, - {1, 1, 1, 1, 2, 2, 1, 0, 0}, - {0, 1, 1, 1, 2, 2, 1, 0, 0}, - {0, 0, 0, 0, 1, 1, 2, 0, 0}, - {0, 0, 0, 0, 0, 1, 2, 0, 0}}, - // INKPLATE_5 - {{0, 0, 1, 1, 0, 1, 1, 1, 0}, - {0, 1, 1, 1, 1, 2, 0, 1, 0}, - {1, 2, 2, 0, 2, 1, 1, 1, 0}, - {1, 1, 1, 2, 0, 1, 1, 2, 0}, - {0, 1, 1, 1, 2, 0, 1, 2, 0}, - {0, 0, 0, 1, 1, 2, 1, 2, 0}, - {1, 1, 1, 2, 0, 2, 1, 2, 0}, - {0, 0, 0, 0, 0, 0, 0, 0, 0}}, - // INKPLATE_5_V2 - {{0, 0, 1, 1, 2, 1, 1, 1, 0}, - {1, 1, 2, 2, 1, 2, 1, 1, 0}, - {0, 1, 2, 2, 1, 1, 2, 1, 0}, - {0, 0, 1, 1, 1, 1, 1, 2, 0}, - {1, 2, 1, 2, 1, 1, 1, 2, 0}, - {0, 1, 1, 1, 2, 0, 1, 2, 0}, - {1, 1, 1, 2, 2, 2, 1, 2, 0}, - {0, 0, 0, 0, 0, 0, 0, 0, 0}}}; - void set_greyscale(bool greyscale) { this->greyscale_ = greyscale; this->block_partial_ = true; if (this->is_ready()) this->initialize_(); } + + void set_waveform(const std::array &waveform, bool is_custom) { + static_assert(sizeof(this->waveform_) == sizeof(uint8_t) * GLUT_COUNT * GLUT_SIZE, + "waveform_ buffer size must match input waveform array size"); + memmove(this->waveform_, waveform.data(), sizeof(this->waveform_)); + this->custom_waveform_ = is_custom; + } + void set_mirror_y(bool mirror_y) { this->mirror_y_ = mirror_y; } void set_mirror_x(bool mirror_x) { this->mirror_x_ = mirror_x; } @@ -225,6 +183,8 @@ class Inkplate6 : public display::DisplayBuffer, public i2c::I2CDevice { bool mirror_y_{false}; bool mirror_x_{false}; bool partial_updating_; + bool custom_waveform_{false}; + uint8_t waveform_[GLUT_COUNT][GLUT_SIZE]; InkplateModel model_; @@ -250,5 +210,5 @@ class Inkplate6 : public display::DisplayBuffer, public i2c::I2CDevice { GPIOPin *wakeup_pin_; }; -} // namespace inkplate6 +} // namespace inkplate } // namespace esphome diff --git a/esphome/components/inkplate6/__init__.py b/esphome/components/inkplate6/__init__.py index b1de57df8f..e69de29bb2 100644 --- a/esphome/components/inkplate6/__init__.py +++ b/esphome/components/inkplate6/__init__.py @@ -1 +0,0 @@ -CODEOWNERS = ["@jesserockz"] diff --git a/esphome/components/inkplate6/display.py b/esphome/components/inkplate6/display.py index 063fc8b0aa..ff14be5491 100644 --- a/esphome/components/inkplate6/display.py +++ b/esphome/components/inkplate6/display.py @@ -1,214 +1,5 @@ -from esphome import pins -import esphome.codegen as cg -from esphome.components import display, i2c -from esphome.components.esp32 import CONF_CPU_FREQUENCY import esphome.config_validation as cv -from esphome.const import ( - CONF_FULL_UPDATE_EVERY, - CONF_ID, - CONF_LAMBDA, - CONF_MIRROR_X, - CONF_MIRROR_Y, - CONF_MODEL, - CONF_OE_PIN, - CONF_PAGES, - CONF_TRANSFORM, - CONF_WAKEUP_PIN, - PLATFORM_ESP32, + +CONFIG_SCHEMA = cv.invalid( + "The inkplate6 display component has been renamed to inkplate." ) -import esphome.final_validate as fv - -DEPENDENCIES = ["i2c", "esp32"] -AUTO_LOAD = ["psram"] - -CONF_DISPLAY_DATA_0_PIN = "display_data_0_pin" -CONF_DISPLAY_DATA_1_PIN = "display_data_1_pin" -CONF_DISPLAY_DATA_2_PIN = "display_data_2_pin" -CONF_DISPLAY_DATA_3_PIN = "display_data_3_pin" -CONF_DISPLAY_DATA_4_PIN = "display_data_4_pin" -CONF_DISPLAY_DATA_5_PIN = "display_data_5_pin" -CONF_DISPLAY_DATA_6_PIN = "display_data_6_pin" -CONF_DISPLAY_DATA_7_PIN = "display_data_7_pin" - -CONF_CL_PIN = "cl_pin" -CONF_CKV_PIN = "ckv_pin" -CONF_GREYSCALE = "greyscale" -CONF_GMOD_PIN = "gmod_pin" -CONF_GPIO0_ENABLE_PIN = "gpio0_enable_pin" -CONF_LE_PIN = "le_pin" -CONF_PARTIAL_UPDATING = "partial_updating" -CONF_POWERUP_PIN = "powerup_pin" -CONF_SPH_PIN = "sph_pin" -CONF_SPV_PIN = "spv_pin" -CONF_VCOM_PIN = "vcom_pin" - -inkplate6_ns = cg.esphome_ns.namespace("inkplate6") -Inkplate6 = inkplate6_ns.class_( - "Inkplate6", - cg.PollingComponent, - i2c.I2CDevice, - display.Display, - display.DisplayBuffer, -) - -InkplateModel = inkplate6_ns.enum("InkplateModel") - -MODELS = { - "inkplate_6": InkplateModel.INKPLATE_6, - "inkplate_10": InkplateModel.INKPLATE_10, - "inkplate_6_plus": InkplateModel.INKPLATE_6_PLUS, - "inkplate_6_v2": InkplateModel.INKPLATE_6_V2, - "inkplate_5": InkplateModel.INKPLATE_5, - "inkplate_5_v2": InkplateModel.INKPLATE_5_V2, -} - -CONFIG_SCHEMA = cv.All( - display.FULL_DISPLAY_SCHEMA.extend( - { - cv.GenerateID(): cv.declare_id(Inkplate6), - cv.Optional(CONF_GREYSCALE, default=False): cv.boolean, - cv.Optional(CONF_TRANSFORM): cv.Schema( - { - cv.Optional(CONF_MIRROR_X, default=False): cv.boolean, - cv.Optional(CONF_MIRROR_Y, default=False): cv.boolean, - } - ), - cv.Optional(CONF_PARTIAL_UPDATING, default=True): cv.boolean, - cv.Optional(CONF_FULL_UPDATE_EVERY, default=10): cv.uint32_t, - cv.Optional(CONF_MODEL, default="inkplate_6"): cv.enum( - MODELS, lower=True, space="_" - ), - # Control pins - cv.Required(CONF_CKV_PIN): pins.gpio_output_pin_schema, - cv.Required(CONF_GMOD_PIN): pins.gpio_output_pin_schema, - cv.Required(CONF_GPIO0_ENABLE_PIN): pins.gpio_output_pin_schema, - cv.Required(CONF_OE_PIN): pins.gpio_output_pin_schema, - cv.Required(CONF_POWERUP_PIN): pins.gpio_output_pin_schema, - cv.Required(CONF_SPH_PIN): pins.gpio_output_pin_schema, - cv.Required(CONF_SPV_PIN): pins.gpio_output_pin_schema, - cv.Required(CONF_VCOM_PIN): pins.gpio_output_pin_schema, - cv.Required(CONF_WAKEUP_PIN): pins.gpio_output_pin_schema, - cv.Optional(CONF_CL_PIN, default=0): pins.internal_gpio_output_pin_schema, - cv.Optional(CONF_LE_PIN, default=2): pins.internal_gpio_output_pin_schema, - # Data pins - cv.Optional( - CONF_DISPLAY_DATA_0_PIN, default=4 - ): pins.internal_gpio_output_pin_schema, - cv.Optional( - CONF_DISPLAY_DATA_1_PIN, default=5 - ): pins.internal_gpio_output_pin_schema, - cv.Optional( - CONF_DISPLAY_DATA_2_PIN, default=18 - ): pins.internal_gpio_output_pin_schema, - cv.Optional( - CONF_DISPLAY_DATA_3_PIN, default=19 - ): pins.internal_gpio_output_pin_schema, - cv.Optional( - CONF_DISPLAY_DATA_4_PIN, default=23 - ): pins.internal_gpio_output_pin_schema, - cv.Optional( - CONF_DISPLAY_DATA_5_PIN, default=25 - ): pins.internal_gpio_output_pin_schema, - cv.Optional( - CONF_DISPLAY_DATA_6_PIN, default=26 - ): pins.internal_gpio_output_pin_schema, - cv.Optional( - CONF_DISPLAY_DATA_7_PIN, default=27 - ): pins.internal_gpio_output_pin_schema, - } - ) - .extend(cv.polling_component_schema("5s")) - .extend(i2c.i2c_device_schema(0x48)), - cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA), -) - - -def _validate_cpu_frequency(config): - esp32_config = fv.full_config.get()[PLATFORM_ESP32] - if esp32_config[CONF_CPU_FREQUENCY] != "240MHZ": - raise cv.Invalid( - "Inkplate requires 240MHz CPU frequency (set in esp32 component)" - ) - return config - - -FINAL_VALIDATE_SCHEMA = _validate_cpu_frequency - - -async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - - await display.register_display(var, config) - await i2c.register_i2c_device(var, config) - - if CONF_LAMBDA in config: - lambda_ = await cg.process_lambda( - config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void - ) - cg.add(var.set_writer(lambda_)) - - cg.add(var.set_greyscale(config[CONF_GREYSCALE])) - if transform := config.get(CONF_TRANSFORM): - cg.add(var.set_mirror_x(transform[CONF_MIRROR_X])) - cg.add(var.set_mirror_y(transform[CONF_MIRROR_Y])) - cg.add(var.set_partial_updating(config[CONF_PARTIAL_UPDATING])) - cg.add(var.set_full_update_every(config[CONF_FULL_UPDATE_EVERY])) - - cg.add(var.set_model(config[CONF_MODEL])) - - ckv = await cg.gpio_pin_expression(config[CONF_CKV_PIN]) - cg.add(var.set_ckv_pin(ckv)) - - gmod = await cg.gpio_pin_expression(config[CONF_GMOD_PIN]) - cg.add(var.set_gmod_pin(gmod)) - - gpio0_enable = await cg.gpio_pin_expression(config[CONF_GPIO0_ENABLE_PIN]) - cg.add(var.set_gpio0_enable_pin(gpio0_enable)) - - oe = await cg.gpio_pin_expression(config[CONF_OE_PIN]) - cg.add(var.set_oe_pin(oe)) - - powerup = await cg.gpio_pin_expression(config[CONF_POWERUP_PIN]) - cg.add(var.set_powerup_pin(powerup)) - - sph = await cg.gpio_pin_expression(config[CONF_SPH_PIN]) - cg.add(var.set_sph_pin(sph)) - - spv = await cg.gpio_pin_expression(config[CONF_SPV_PIN]) - cg.add(var.set_spv_pin(spv)) - - vcom = await cg.gpio_pin_expression(config[CONF_VCOM_PIN]) - cg.add(var.set_vcom_pin(vcom)) - - wakeup = await cg.gpio_pin_expression(config[CONF_WAKEUP_PIN]) - cg.add(var.set_wakeup_pin(wakeup)) - - cl = await cg.gpio_pin_expression(config[CONF_CL_PIN]) - cg.add(var.set_cl_pin(cl)) - - le = await cg.gpio_pin_expression(config[CONF_LE_PIN]) - cg.add(var.set_le_pin(le)) - - display_data_0 = await cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_0_PIN]) - cg.add(var.set_display_data_0_pin(display_data_0)) - - display_data_1 = await cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_1_PIN]) - cg.add(var.set_display_data_1_pin(display_data_1)) - - display_data_2 = await cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_2_PIN]) - cg.add(var.set_display_data_2_pin(display_data_2)) - - display_data_3 = await cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_3_PIN]) - cg.add(var.set_display_data_3_pin(display_data_3)) - - display_data_4 = await cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_4_PIN]) - cg.add(var.set_display_data_4_pin(display_data_4)) - - display_data_5 = await cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_5_PIN]) - cg.add(var.set_display_data_5_pin(display_data_5)) - - display_data_6 = await cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_6_PIN]) - cg.add(var.set_display_data_6_pin(display_data_6)) - - display_data_7 = await cg.gpio_pin_expression(config[CONF_DISPLAY_DATA_7_PIN]) - cg.add(var.set_display_data_7_pin(display_data_7)) diff --git a/tests/components/inkplate6/common.yaml b/tests/components/inkplate/common.yaml similarity index 96% rename from tests/components/inkplate6/common.yaml rename to tests/components/inkplate/common.yaml index 6cb5d055b6..7050b1739f 100644 --- a/tests/components/inkplate6/common.yaml +++ b/tests/components/inkplate/common.yaml @@ -1,5 +1,5 @@ i2c: - - id: i2c_inkplate6 + - id: i2c_inkplate scl: 16 sda: 17 @@ -7,7 +7,7 @@ esp32: cpu_frequency: 240MHz display: - - platform: inkplate6 + - platform: inkplate id: inkplate_display greyscale: false partial_updating: false diff --git a/tests/components/inkplate6/test.esp32-ard.yaml b/tests/components/inkplate/test.esp32-ard.yaml similarity index 100% rename from tests/components/inkplate6/test.esp32-ard.yaml rename to tests/components/inkplate/test.esp32-ard.yaml diff --git a/tests/components/inkplate6/test.esp32-idf.yaml b/tests/components/inkplate/test.esp32-idf.yaml similarity index 100% rename from tests/components/inkplate6/test.esp32-idf.yaml rename to tests/components/inkplate/test.esp32-idf.yaml From 2401f81be3fab1e87b675b6ed8dc5e2e34a2b4cd Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 10 Sep 2025 19:48:01 +1200 Subject: [PATCH 207/208] Bump version to 2025.9.0b1 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index f312ca45e2..a7f591cbf5 100644 --- a/Doxyfile +++ b/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.9.0-dev +PROJECT_NUMBER = 2025.9.0b1 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/esphome/const.py b/esphome/const.py index fae6020b88..a77f98c292 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2025.9.0-dev" +__version__ = "2025.9.0b1" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 56e85b3ef96277c0a97a29d8d87c9a25cade385b Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Wed, 10 Sep 2025 05:58:07 -0500 Subject: [PATCH 208/208] [thermostat] Rename timer enums to mitigate naming conflict (#10666) --- .../thermostat/thermostat_climate.cpp | 144 ++++++++++-------- .../thermostat/thermostat_climate.h | 24 +-- 2 files changed, 90 insertions(+), 78 deletions(-) diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index f05730b369..e2db3ca5e1 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -11,11 +11,11 @@ static const char *const TAG = "thermostat.climate"; void ThermostatClimate::setup() { if (this->use_startup_delay_) { // start timers so that no actions are called for a moment - this->start_timer_(thermostat::TIMER_COOLING_OFF); - this->start_timer_(thermostat::TIMER_FANNING_OFF); - this->start_timer_(thermostat::TIMER_HEATING_OFF); + this->start_timer_(thermostat::THERMOSTAT_TIMER_COOLING_OFF); + this->start_timer_(thermostat::THERMOSTAT_TIMER_FANNING_OFF); + this->start_timer_(thermostat::THERMOSTAT_TIMER_HEATING_OFF); if (this->supports_fan_only_action_uses_fan_mode_timer_) - this->start_timer_(thermostat::TIMER_FAN_MODE); + this->start_timer_(thermostat::THERMOSTAT_TIMER_FAN_MODE); } // add a callback so that whenever the sensor state changes we can take action this->sensor_->add_on_state_callback([this](float state) { @@ -307,9 +307,10 @@ climate::ClimateAction ThermostatClimate::compute_action_(const bool ignore_time return climate::CLIMATE_ACTION_OFF; } // do not change the action if an "ON" timer is running - if ((!ignore_timers) && - (this->timer_active_(thermostat::TIMER_IDLE_ON) || this->timer_active_(thermostat::TIMER_COOLING_ON) || - this->timer_active_(thermostat::TIMER_FANNING_ON) || this->timer_active_(thermostat::TIMER_HEATING_ON))) { + if ((!ignore_timers) && (this->timer_active_(thermostat::THERMOSTAT_TIMER_IDLE_ON) || + this->timer_active_(thermostat::THERMOSTAT_TIMER_COOLING_ON) || + this->timer_active_(thermostat::THERMOSTAT_TIMER_FANNING_ON) || + this->timer_active_(thermostat::THERMOSTAT_TIMER_HEATING_ON))) { return this->action; } @@ -444,18 +445,18 @@ void ThermostatClimate::switch_to_action_(climate::ClimateAction action, bool pu case climate::CLIMATE_ACTION_OFF: case climate::CLIMATE_ACTION_IDLE: if (this->idle_action_ready_()) { - this->start_timer_(thermostat::TIMER_IDLE_ON); + this->start_timer_(thermostat::THERMOSTAT_TIMER_IDLE_ON); if (this->action == climate::CLIMATE_ACTION_COOLING) - this->start_timer_(thermostat::TIMER_COOLING_OFF); + this->start_timer_(thermostat::THERMOSTAT_TIMER_COOLING_OFF); if (this->action == climate::CLIMATE_ACTION_FAN) { if (this->supports_fan_only_action_uses_fan_mode_timer_) { - this->start_timer_(thermostat::TIMER_FAN_MODE); + this->start_timer_(thermostat::THERMOSTAT_TIMER_FAN_MODE); } else { - this->start_timer_(thermostat::TIMER_FANNING_OFF); + this->start_timer_(thermostat::THERMOSTAT_TIMER_FANNING_OFF); } } if (this->action == climate::CLIMATE_ACTION_HEATING) - this->start_timer_(thermostat::TIMER_HEATING_OFF); + this->start_timer_(thermostat::THERMOSTAT_TIMER_HEATING_OFF); // trig = this->idle_action_trigger_; ESP_LOGVV(TAG, "Switching to IDLE/OFF action"); this->cooling_max_runtime_exceeded_ = false; @@ -465,10 +466,10 @@ void ThermostatClimate::switch_to_action_(climate::ClimateAction action, bool pu break; case climate::CLIMATE_ACTION_COOLING: if (this->cooling_action_ready_()) { - this->start_timer_(thermostat::TIMER_COOLING_ON); - this->start_timer_(thermostat::TIMER_COOLING_MAX_RUN_TIME); + this->start_timer_(thermostat::THERMOSTAT_TIMER_COOLING_ON); + this->start_timer_(thermostat::THERMOSTAT_TIMER_COOLING_MAX_RUN_TIME); if (this->supports_fan_with_cooling_) { - this->start_timer_(thermostat::TIMER_FANNING_ON); + this->start_timer_(thermostat::THERMOSTAT_TIMER_FANNING_ON); trig_fan = this->fan_only_action_trigger_; } this->cooling_max_runtime_exceeded_ = false; @@ -479,10 +480,10 @@ void ThermostatClimate::switch_to_action_(climate::ClimateAction action, bool pu break; case climate::CLIMATE_ACTION_HEATING: if (this->heating_action_ready_()) { - this->start_timer_(thermostat::TIMER_HEATING_ON); - this->start_timer_(thermostat::TIMER_HEATING_MAX_RUN_TIME); + this->start_timer_(thermostat::THERMOSTAT_TIMER_HEATING_ON); + this->start_timer_(thermostat::THERMOSTAT_TIMER_HEATING_MAX_RUN_TIME); if (this->supports_fan_with_heating_) { - this->start_timer_(thermostat::TIMER_FANNING_ON); + this->start_timer_(thermostat::THERMOSTAT_TIMER_FANNING_ON); trig_fan = this->fan_only_action_trigger_; } this->heating_max_runtime_exceeded_ = false; @@ -494,9 +495,9 @@ void ThermostatClimate::switch_to_action_(climate::ClimateAction action, bool pu case climate::CLIMATE_ACTION_FAN: if (this->fanning_action_ready_()) { if (this->supports_fan_only_action_uses_fan_mode_timer_) { - this->start_timer_(thermostat::TIMER_FAN_MODE); + this->start_timer_(thermostat::THERMOSTAT_TIMER_FAN_MODE); } else { - this->start_timer_(thermostat::TIMER_FANNING_ON); + this->start_timer_(thermostat::THERMOSTAT_TIMER_FANNING_ON); } trig = this->fan_only_action_trigger_; ESP_LOGVV(TAG, "Switching to FAN_ONLY action"); @@ -505,8 +506,8 @@ void ThermostatClimate::switch_to_action_(climate::ClimateAction action, bool pu break; case climate::CLIMATE_ACTION_DRYING: if (this->drying_action_ready_()) { - this->start_timer_(thermostat::TIMER_COOLING_ON); - this->start_timer_(thermostat::TIMER_FANNING_ON); + this->start_timer_(thermostat::THERMOSTAT_TIMER_COOLING_ON); + this->start_timer_(thermostat::THERMOSTAT_TIMER_FANNING_ON); trig = this->dry_action_trigger_; ESP_LOGVV(TAG, "Switching to DRYING action"); action_ready = true; @@ -549,14 +550,14 @@ void ThermostatClimate::switch_to_supplemental_action_(climate::ClimateAction ac switch (action) { case climate::CLIMATE_ACTION_OFF: case climate::CLIMATE_ACTION_IDLE: - this->cancel_timer_(thermostat::TIMER_COOLING_MAX_RUN_TIME); - this->cancel_timer_(thermostat::TIMER_HEATING_MAX_RUN_TIME); + this->cancel_timer_(thermostat::THERMOSTAT_TIMER_COOLING_MAX_RUN_TIME); + this->cancel_timer_(thermostat::THERMOSTAT_TIMER_HEATING_MAX_RUN_TIME); break; case climate::CLIMATE_ACTION_COOLING: - this->cancel_timer_(thermostat::TIMER_COOLING_MAX_RUN_TIME); + this->cancel_timer_(thermostat::THERMOSTAT_TIMER_COOLING_MAX_RUN_TIME); break; case climate::CLIMATE_ACTION_HEATING: - this->cancel_timer_(thermostat::TIMER_HEATING_MAX_RUN_TIME); + this->cancel_timer_(thermostat::THERMOSTAT_TIMER_HEATING_MAX_RUN_TIME); break; default: return; @@ -571,15 +572,15 @@ void ThermostatClimate::trigger_supplemental_action_() { switch (this->supplemental_action_) { case climate::CLIMATE_ACTION_COOLING: - if (!this->timer_active_(thermostat::TIMER_COOLING_MAX_RUN_TIME)) { - this->start_timer_(thermostat::TIMER_COOLING_MAX_RUN_TIME); + if (!this->timer_active_(thermostat::THERMOSTAT_TIMER_COOLING_MAX_RUN_TIME)) { + this->start_timer_(thermostat::THERMOSTAT_TIMER_COOLING_MAX_RUN_TIME); } trig = this->supplemental_cool_action_trigger_; ESP_LOGVV(TAG, "Calling supplemental COOLING action"); break; case climate::CLIMATE_ACTION_HEATING: - if (!this->timer_active_(thermostat::TIMER_HEATING_MAX_RUN_TIME)) { - this->start_timer_(thermostat::TIMER_HEATING_MAX_RUN_TIME); + if (!this->timer_active_(thermostat::THERMOSTAT_TIMER_HEATING_MAX_RUN_TIME)) { + this->start_timer_(thermostat::THERMOSTAT_TIMER_HEATING_MAX_RUN_TIME); } trig = this->supplemental_heat_action_trigger_; ESP_LOGVV(TAG, "Calling supplemental HEATING action"); @@ -657,7 +658,7 @@ void ThermostatClimate::switch_to_fan_mode_(climate::ClimateFanMode fan_mode, bo this->prev_fan_mode_trigger_->stop_action(); this->prev_fan_mode_trigger_ = nullptr; } - this->start_timer_(thermostat::TIMER_FAN_MODE); + this->start_timer_(thermostat::THERMOSTAT_TIMER_FAN_MODE); if (trig != nullptr) { trig->trigger(); } @@ -758,35 +759,44 @@ void ThermostatClimate::switch_to_swing_mode_(climate::ClimateSwingMode swing_mo bool ThermostatClimate::idle_action_ready_() { if (this->supports_fan_only_action_uses_fan_mode_timer_) { - return !(this->timer_active_(thermostat::TIMER_COOLING_ON) || this->timer_active_(thermostat::TIMER_FAN_MODE) || - this->timer_active_(thermostat::TIMER_HEATING_ON)); + return !(this->timer_active_(thermostat::THERMOSTAT_TIMER_COOLING_ON) || + this->timer_active_(thermostat::THERMOSTAT_TIMER_FAN_MODE) || + this->timer_active_(thermostat::THERMOSTAT_TIMER_HEATING_ON)); } - return !(this->timer_active_(thermostat::TIMER_COOLING_ON) || this->timer_active_(thermostat::TIMER_FANNING_ON) || - this->timer_active_(thermostat::TIMER_HEATING_ON)); + return !(this->timer_active_(thermostat::THERMOSTAT_TIMER_COOLING_ON) || + this->timer_active_(thermostat::THERMOSTAT_TIMER_FANNING_ON) || + this->timer_active_(thermostat::THERMOSTAT_TIMER_HEATING_ON)); } bool ThermostatClimate::cooling_action_ready_() { - return !(this->timer_active_(thermostat::TIMER_IDLE_ON) || this->timer_active_(thermostat::TIMER_FANNING_OFF) || - this->timer_active_(thermostat::TIMER_COOLING_OFF) || this->timer_active_(thermostat::TIMER_HEATING_ON)); + return !(this->timer_active_(thermostat::THERMOSTAT_TIMER_IDLE_ON) || + this->timer_active_(thermostat::THERMOSTAT_TIMER_FANNING_OFF) || + this->timer_active_(thermostat::THERMOSTAT_TIMER_COOLING_OFF) || + this->timer_active_(thermostat::THERMOSTAT_TIMER_HEATING_ON)); } bool ThermostatClimate::drying_action_ready_() { - return !(this->timer_active_(thermostat::TIMER_IDLE_ON) || this->timer_active_(thermostat::TIMER_FANNING_OFF) || - this->timer_active_(thermostat::TIMER_COOLING_OFF) || this->timer_active_(thermostat::TIMER_HEATING_ON)); + return !(this->timer_active_(thermostat::THERMOSTAT_TIMER_IDLE_ON) || + this->timer_active_(thermostat::THERMOSTAT_TIMER_FANNING_OFF) || + this->timer_active_(thermostat::THERMOSTAT_TIMER_COOLING_OFF) || + this->timer_active_(thermostat::THERMOSTAT_TIMER_HEATING_ON)); } -bool ThermostatClimate::fan_mode_ready_() { return !(this->timer_active_(thermostat::TIMER_FAN_MODE)); } +bool ThermostatClimate::fan_mode_ready_() { return !(this->timer_active_(thermostat::THERMOSTAT_TIMER_FAN_MODE)); } bool ThermostatClimate::fanning_action_ready_() { if (this->supports_fan_only_action_uses_fan_mode_timer_) { - return !(this->timer_active_(thermostat::TIMER_FAN_MODE)); + return !(this->timer_active_(thermostat::THERMOSTAT_TIMER_FAN_MODE)); } - return !(this->timer_active_(thermostat::TIMER_IDLE_ON) || this->timer_active_(thermostat::TIMER_FANNING_OFF)); + return !(this->timer_active_(thermostat::THERMOSTAT_TIMER_IDLE_ON) || + this->timer_active_(thermostat::THERMOSTAT_TIMER_FANNING_OFF)); } bool ThermostatClimate::heating_action_ready_() { - return !(this->timer_active_(thermostat::TIMER_IDLE_ON) || this->timer_active_(thermostat::TIMER_COOLING_ON) || - this->timer_active_(thermostat::TIMER_FANNING_OFF) || this->timer_active_(thermostat::TIMER_HEATING_OFF)); + return !(this->timer_active_(thermostat::THERMOSTAT_TIMER_IDLE_ON) || + this->timer_active_(thermostat::THERMOSTAT_TIMER_COOLING_ON) || + this->timer_active_(thermostat::THERMOSTAT_TIMER_FANNING_OFF) || + this->timer_active_(thermostat::THERMOSTAT_TIMER_HEATING_OFF)); } void ThermostatClimate::start_timer_(const ThermostatClimateTimerIndex timer_index) { @@ -1162,43 +1172,43 @@ void ThermostatClimate::set_heat_overrun(float overrun) { this->heating_overrun_ void ThermostatClimate::set_supplemental_cool_delta(float delta) { this->supplemental_cool_delta_ = delta; } void ThermostatClimate::set_supplemental_heat_delta(float delta) { this->supplemental_heat_delta_ = delta; } void ThermostatClimate::set_cooling_maximum_run_time_in_sec(uint32_t time) { - this->timer_[thermostat::TIMER_COOLING_MAX_RUN_TIME].time = + this->timer_[thermostat::THERMOSTAT_TIMER_COOLING_MAX_RUN_TIME].time = 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); } void ThermostatClimate::set_cooling_minimum_off_time_in_sec(uint32_t time) { - this->timer_[thermostat::TIMER_COOLING_OFF].time = + this->timer_[thermostat::THERMOSTAT_TIMER_COOLING_OFF].time = 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); } void ThermostatClimate::set_cooling_minimum_run_time_in_sec(uint32_t time) { - this->timer_[thermostat::TIMER_COOLING_ON].time = + this->timer_[thermostat::THERMOSTAT_TIMER_COOLING_ON].time = 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); } void ThermostatClimate::set_fan_mode_minimum_switching_time_in_sec(uint32_t time) { - this->timer_[thermostat::TIMER_FAN_MODE].time = + this->timer_[thermostat::THERMOSTAT_TIMER_FAN_MODE].time = 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); } void ThermostatClimate::set_fanning_minimum_off_time_in_sec(uint32_t time) { - this->timer_[thermostat::TIMER_FANNING_OFF].time = + this->timer_[thermostat::THERMOSTAT_TIMER_FANNING_OFF].time = 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); } void ThermostatClimate::set_fanning_minimum_run_time_in_sec(uint32_t time) { - this->timer_[thermostat::TIMER_FANNING_ON].time = + this->timer_[thermostat::THERMOSTAT_TIMER_FANNING_ON].time = 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); } void ThermostatClimate::set_heating_maximum_run_time_in_sec(uint32_t time) { - this->timer_[thermostat::TIMER_HEATING_MAX_RUN_TIME].time = + this->timer_[thermostat::THERMOSTAT_TIMER_HEATING_MAX_RUN_TIME].time = 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); } void ThermostatClimate::set_heating_minimum_off_time_in_sec(uint32_t time) { - this->timer_[thermostat::TIMER_HEATING_OFF].time = + this->timer_[thermostat::THERMOSTAT_TIMER_HEATING_OFF].time = 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); } void ThermostatClimate::set_heating_minimum_run_time_in_sec(uint32_t time) { - this->timer_[thermostat::TIMER_HEATING_ON].time = + this->timer_[thermostat::THERMOSTAT_TIMER_HEATING_ON].time = 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); } void ThermostatClimate::set_idle_minimum_time_in_sec(uint32_t time) { - this->timer_[thermostat::TIMER_IDLE_ON].time = + this->timer_[thermostat::THERMOSTAT_TIMER_IDLE_ON].time = 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); } void ThermostatClimate::set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; } @@ -1327,13 +1337,14 @@ void ThermostatClimate::dump_config() { " Minimum Off Time: %" PRIu32 "s\n" " Minimum Run Time: %" PRIu32 "s", this->cooling_deadband_, this->cooling_overrun_, - this->timer_duration_(thermostat::TIMER_COOLING_OFF) / 1000, - this->timer_duration_(thermostat::TIMER_COOLING_ON) / 1000); - if ((this->supplemental_cool_delta_ > 0) || (this->timer_duration_(thermostat::TIMER_COOLING_MAX_RUN_TIME) > 0)) { + this->timer_duration_(thermostat::THERMOSTAT_TIMER_COOLING_OFF) / 1000, + this->timer_duration_(thermostat::THERMOSTAT_TIMER_COOLING_ON) / 1000); + if ((this->supplemental_cool_delta_ > 0) || + (this->timer_duration_(thermostat::THERMOSTAT_TIMER_COOLING_MAX_RUN_TIME) > 0)) { ESP_LOGCONFIG(TAG, " Maximum Run Time: %" PRIu32 "s\n" " Supplemental Delta: %.1f°C", - this->timer_duration_(thermostat::TIMER_COOLING_MAX_RUN_TIME) / 1000, + this->timer_duration_(thermostat::THERMOSTAT_TIMER_COOLING_MAX_RUN_TIME) / 1000, this->supplemental_cool_delta_); } } @@ -1345,13 +1356,14 @@ void ThermostatClimate::dump_config() { " Minimum Off Time: %" PRIu32 "s\n" " Minimum Run Time: %" PRIu32 "s", this->heating_deadband_, this->heating_overrun_, - this->timer_duration_(thermostat::TIMER_HEATING_OFF) / 1000, - this->timer_duration_(thermostat::TIMER_HEATING_ON) / 1000); - if ((this->supplemental_heat_delta_ > 0) || (this->timer_duration_(thermostat::TIMER_HEATING_MAX_RUN_TIME) > 0)) { + this->timer_duration_(thermostat::THERMOSTAT_TIMER_HEATING_OFF) / 1000, + this->timer_duration_(thermostat::THERMOSTAT_TIMER_HEATING_ON) / 1000); + if ((this->supplemental_heat_delta_ > 0) || + (this->timer_duration_(thermostat::THERMOSTAT_TIMER_HEATING_MAX_RUN_TIME) > 0)) { ESP_LOGCONFIG(TAG, " Maximum Run Time: %" PRIu32 "s\n" " Supplemental Delta: %.1f°C", - this->timer_duration_(thermostat::TIMER_HEATING_MAX_RUN_TIME) / 1000, + this->timer_duration_(thermostat::THERMOSTAT_TIMER_HEATING_MAX_RUN_TIME) / 1000, this->supplemental_heat_delta_); } } @@ -1360,15 +1372,15 @@ void ThermostatClimate::dump_config() { " Fan Parameters:\n" " Minimum Off Time: %" PRIu32 "s\n" " Minimum Run Time: %" PRIu32 "s", - this->timer_duration_(thermostat::TIMER_FANNING_OFF) / 1000, - this->timer_duration_(thermostat::TIMER_FANNING_ON) / 1000); + this->timer_duration_(thermostat::THERMOSTAT_TIMER_FANNING_OFF) / 1000, + this->timer_duration_(thermostat::THERMOSTAT_TIMER_FANNING_ON) / 1000); } if (this->supports_fan_mode_on_ || this->supports_fan_mode_off_ || this->supports_fan_mode_auto_ || this->supports_fan_mode_low_ || this->supports_fan_mode_medium_ || this->supports_fan_mode_high_ || this->supports_fan_mode_middle_ || this->supports_fan_mode_focus_ || this->supports_fan_mode_diffuse_ || this->supports_fan_mode_quiet_) { ESP_LOGCONFIG(TAG, " Minimum Fan Mode Switching Time: %" PRIu32 "s", - this->timer_duration_(thermostat::TIMER_FAN_MODE) / 1000); + this->timer_duration_(thermostat::THERMOSTAT_TIMER_FAN_MODE) / 1000); } ESP_LOGCONFIG(TAG, " Minimum Idle Time: %" PRIu32 "s\n" @@ -1381,7 +1393,7 @@ void ThermostatClimate::dump_config() { " FAN_ONLY: %s\n" " FAN_ONLY_ACTION_USES_FAN_MODE_TIMER: %s\n" " FAN_ONLY_COOLING: %s", - this->timer_[thermostat::TIMER_IDLE_ON].time / 1000, YESNO(this->supports_auto_), + this->timer_[thermostat::THERMOSTAT_TIMER_IDLE_ON].time / 1000, YESNO(this->supports_auto_), YESNO(this->supports_heat_cool_), YESNO(this->supports_heat_), YESNO(this->supports_cool_), YESNO(this->supports_dry_), YESNO(this->supports_fan_only_), YESNO(this->supports_fan_only_action_uses_fan_mode_timer_), YESNO(this->supports_fan_only_cooling_)); diff --git a/esphome/components/thermostat/thermostat_climate.h b/esphome/components/thermostat/thermostat_climate.h index e2c7b00266..526f07116e 100644 --- a/esphome/components/thermostat/thermostat_climate.h +++ b/esphome/components/thermostat/thermostat_climate.h @@ -14,17 +14,17 @@ namespace esphome { namespace thermostat { enum ThermostatClimateTimerIndex : uint8_t { - TIMER_COOLING_MAX_RUN_TIME = 0, - TIMER_COOLING_OFF = 1, - TIMER_COOLING_ON = 2, - TIMER_FAN_MODE = 3, - TIMER_FANNING_OFF = 4, - TIMER_FANNING_ON = 5, - TIMER_HEATING_MAX_RUN_TIME = 6, - TIMER_HEATING_OFF = 7, - TIMER_HEATING_ON = 8, - TIMER_IDLE_ON = 9, - TIMER_COUNT = 10, + THERMOSTAT_TIMER_COOLING_MAX_RUN_TIME = 0, + THERMOSTAT_TIMER_COOLING_OFF = 1, + THERMOSTAT_TIMER_COOLING_ON = 2, + THERMOSTAT_TIMER_FAN_MODE = 3, + THERMOSTAT_TIMER_FANNING_OFF = 4, + THERMOSTAT_TIMER_FANNING_ON = 5, + THERMOSTAT_TIMER_HEATING_MAX_RUN_TIME = 6, + THERMOSTAT_TIMER_HEATING_OFF = 7, + THERMOSTAT_TIMER_HEATING_ON = 8, + THERMOSTAT_TIMER_IDLE_ON = 9, + THERMOSTAT_TIMER_COUNT = 10, }; enum OnBootRestoreFrom : uint8_t { @@ -467,7 +467,7 @@ class ThermostatClimate : public climate::Climate, public Component { std::string default_custom_preset_{}; /// Climate action timers - std::array timer_{ + std::array timer_{ ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::cooling_max_run_time_timer_callback_, this)), ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::cooling_off_timer_callback_, this)), ThermostatClimateTimer(false, 0, 0, std::bind(&ThermostatClimate::cooling_on_timer_callback_, this)),