Compare commits

..
5 changed files with 105 additions and 8 deletions
+9
View File
@@ -56,6 +56,15 @@ void EntityBase::configure_entity_(const char *name, uint32_t object_id_hash, ui
this->flags_.entity_category = (entity_fields >> ENTITY_FIELD_ENTITY_CATEGORY_SHIFT) & 0x3;
}
void EntityBase::set_internal(bool internal) {
// Remove the after-setup path in 2027.3.0 and ignore the call instead.
if (App.is_setup_complete()) {
ESP_LOGE(TAG, "'%s': set_internal() after setup is undefined behavior, stops working in 2027.3.0",
this->get_name().c_str());
}
this->flags_.internal = internal;
}
// Weak default lookup functions — overridden by generated code in main.cpp
__attribute__((weak)) const char *entity_device_class_lookup(uint8_t) { return ""; }
__attribute__((weak)) const char *entity_uom_lookup(uint8_t) { return ""; }
+20 -7
View File
@@ -88,13 +88,26 @@ class EntityBase {
// Get whether this Entity should be hidden outside ESPHome
bool is_internal() const { return this->flags_.internal; }
// Deprecated: Calling set_internal() at runtime is undefined behavior. Components and clients
// are NOT notified of the change, the flag may have already been read during setup, and there
// is NO guarantee any consumer will observe the new value. Use the 'internal:' YAML key instead.
ESPDEPRECATED("set_internal() is undefined behavior at runtime — components and Home Assistant are NOT "
"notified. Use the 'internal:' YAML key instead. Will be removed in 2027.3.0.",
"2026.3.0")
void set_internal(bool internal) { this->flags_.internal = internal; }
// Set whether this Entity should be hidden outside ESPHome. Prefer the 'internal:' YAML key
// whenever possible: it is guaranteed and has none of the limitations below. Use this only when
// the decision can only be made at boot. Must be called before MQTT and the API read the flag:
// from on_boot at the default priority, or a setup() that runs above setup_priority::AFTER_WIFI.
// If the answer comes from a device handshake, hold setup with can_proceed() until it arrives.
// Calls after setup finishes are undefined behavior: the flag is still written and an error is
// logged, and from 2027.3.0 the call will be ignored.
//
// Known limitations. Not bugs, so no issue reports please; a PR that removes one with no RAM
// or performance cost would be considered.
// - No consumer is notified of a change, so the flag can only be decided once per boot.
// - The guard is coarse: a call from a priority below AFTER_WIFI (an on_boot with a low priority,
// or a setup() at LATE) still passes, but the API camera listener is already registered, MQTT
// (AFTER_CONNECTION) has cached the flag, and an API client that connected while setup was
// stalled on a slow component has already listed the entities, so they keep the old value.
// - Un-hiding an entity declared 'internal: true' in YAML skips the duplicate name check that
// codegen runs for exposed entities, so a name collision can surface at runtime. Entities with
// only an 'id:' are forced internal and use the id as their name.
// - Zigbee codegen skips YAML internal entities entirely, so un-hiding cannot add them to Zigbee.
void set_internal(bool internal);
// Check if this object is declared to be disabled by default.
// That means that when the device gets added to Home Assistant (or other clients) it should
+1 -1
View File
@@ -12,7 +12,7 @@ pyserial==3.5
platformio==6.1.19
esptool==5.4.0
click==8.3.3
aioesphomeapi==46.4.0
aioesphomeapi==46.3.0
aiohappyeyeballs==2.7.1 # Happy Eyeballs for requests downloads; already pulled in by aioesphomeapi
zeroconf==0.151.3
puremagic==2.2.0
@@ -0,0 +1,34 @@
esphome:
name: set-internal-at-boot
on_boot:
then:
- lambda: |-
id(hidden_at_boot).set_internal(true);
id(shown_at_boot).set_internal(false);
host:
api:
actions:
- action: set_internal_late
then:
- lambda: id(untouched).set_internal(true);
logger:
sensor:
- platform: template
name: "Hidden At Boot"
id: hidden_at_boot
lambda: return 1.0;
- platform: template
name: "Shown At Boot"
id: shown_at_boot
internal: true
lambda: return 2.0;
- platform: template
name: "Untouched"
id: untouched
lambda: return 3.0;
@@ -0,0 +1,41 @@
"""Integration test for set_internal() called during and after setup."""
from __future__ import annotations
import pytest
from .log_utils import LineWaiter
from .types import APIClientConnectedFactory, RunCompiledFunction
@pytest.mark.asyncio
async def test_set_internal_at_boot(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""set_internal() in on_boot changes API exposure, later calls log an error."""
waiter = LineWaiter()
async with (
run_compiled(yaml_config, line_callback=waiter.callback),
api_client_connected() as client,
):
entities, services = await client.list_entities_services()
names = {entity.name for entity in entities}
assert "Hidden At Boot" not in names
assert "Shown At Boot" in names
assert "Untouched" in names
late = next(s for s in services if s.name == "set_internal_late")
await client.execute_service(late, {})
await waiter.wait_for(
"'Untouched'",
"set_internal() after setup is undefined behavior",
timeout=5.0,
)
# Still written during the deprecation window, ignored from 2027.3.0
entities, _ = await client.list_entities_services()
assert "Untouched" not in {entity.name for entity in entities}