From 7922462bcf9354cb67776b61d1ed4b52c05efcb6 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 1/6] [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 b7afeafda91b25cec3dd3428ea064841747e51b4 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 2/6] [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 95cd224e3ed6e68f0246a2a158505dca0c38fdc4 Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Wed, 13 Aug 2025 15:40:12 -0700 Subject: [PATCH 3/6] [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 93f94751053a00280112d5e763f2bdc125c5d0c0 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 4/6] 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 9c897993bb9555290a625de4e2eba6e20ba0414a 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 5/6] 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 8d61b1e8dfcc0e51577f51c36f7fa9e27456af2b Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 14 Aug 2025 14:00:27 +1200 Subject: [PATCH 6/6] Bump version to 2025.8.0b2 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index 6566b56e4b..e2d892eb44 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.0b1 +PROJECT_NUMBER = 2025.8.0b2 # 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 841630df5c..3b5365854d 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.0b1" +__version__ = "2025.8.0b2" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = (