Compare commits

...
9 changed files with 127 additions and 12 deletions
+11 -2
View File
@@ -3,6 +3,7 @@
#include "esphome/components/esp32/crash_handler.h"
#include <esp_log.h>
#include <esp_idf_version.h>
#include <driver/uart.h>
#include <soc/soc_caps.h>
@@ -16,8 +17,10 @@
#include <driver/usb_serial_jtag_vfs.h>
#endif
#endif
#include "esp_idf_version.h"
#if defined(CONFIG_PM_ENABLE) && defined(CONFIG_FREERTOS_USE_TICKLESS_IDLE) && \
(ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0))
#include "esp_sleep.h"
#endif
#include "freertos/FreeRTOS.h"
#include <fcntl.h>
@@ -87,6 +90,12 @@ void init_uart(uart_port_t uart_num, uint32_t baud_rate, int tx_buffer_size) {
// ESP-IDF requires rx_buffer_size > UART_HW_FIFO_LEN (128 bytes).
const int min_rx_buffer_size = UART_HW_FIFO_LEN(uart_num) + 1;
uart_driver_install(uart_num, min_rx_buffer_size, tx_buffer_size, 0, nullptr, 0);
#if defined(CONFIG_PM_ENABLE) && defined(CONFIG_FREERTOS_USE_TICKLESS_IDLE) && \
(ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0))
// Always flush before going to light sleep. Could be disabled for devices
// without TOP_PD or if source_clk = UART_SCLK_RTC
esp_sleep_set_console_uart_handling_mode(ESP_SLEEP_ALWAYS_FLUSH_UART);
#endif
}
void Logger::pre_setup() {
@@ -1,6 +1,7 @@
#include "zigbee_time_zephyr.h"
#if defined(USE_ZIGBEE) && defined(USE_NRF52) && defined(USE_TIME)
#include "esphome/core/log.h"
#include "esphome/core/application.h"
namespace esphome::zigbee {
@@ -47,6 +48,7 @@ void ZigbeeTime::set_epoch_time(uint32_t epoch) {
this->synchronize_epoch_(epoch);
this->has_time_ = true;
});
App.wake_loop_threadsafe();
}
void ZigbeeTime::zcl_device_cb_(zb_bufid_t bufid) {
+4 -1
View File
@@ -49,7 +49,8 @@ void ZigbeeComponent::factory_reset() {
void ZigbeeComponent::esp_zigbee_alarm_bdb_commissioning(ezb_bdb_comm_mode_mask_t mode) {
if (!esp_zigbee_lock_acquire(10 / portTICK_PERIOD_MS)) {
global_zigbee->set_timeout("zb_init", 10, [mode]() { ZigbeeComponent::esp_zigbee_alarm_bdb_commissioning(mode); });
global_zigbee->set_timeout("zb_init", 100, [mode]() { ZigbeeComponent::esp_zigbee_alarm_bdb_commissioning(mode); });
App.wake_loop_threadsafe();
return;
}
if (ezb_bdb_start_top_level_commissioning(mode) != EZB_ERR_NONE) {
@@ -88,6 +89,7 @@ bool ZigbeeComponent::app_signal_handler(const ezb_app_signal_t *app_signal) {
global_zigbee->set_timeout("zb_init", 1000, []() {
ZigbeeComponent::esp_zigbee_alarm_bdb_commissioning(EZB_BDB_MODE_INITIALIZATION);
});
App.wake_loop_threadsafe();
}
} break;
case EZB_BDB_SIGNAL_STEERING: {
@@ -113,6 +115,7 @@ bool ZigbeeComponent::app_signal_handler(const ezb_app_signal_t *app_signal) {
ZigbeeComponent::esp_zigbee_alarm_bdb_commissioning(EZB_BDB_MODE_NETWORK_STEERING);
});
}
App.wake_loop_threadsafe();
}
} break;
case EZB_ZDO_SIGNAL_LEAVE: {
+4 -2
View File
@@ -1,10 +1,10 @@
#include "zigbee_zephyr.h"
#if defined(USE_ZIGBEE) && defined(USE_NRF52)
#include "esphome/core/log.h"
#include "esphome/core/application.h"
#include <zephyr/settings/settings.h>
#include <zephyr/storage/flash_map.h>
#include "esphome/core/hal.h"
#include "esphome/core/wake.h"
extern "C" {
#include <zboss_api.h>
@@ -120,7 +120,7 @@ void ZigbeeComponent::zcl_device_cb(zb_bufid_t bufid) {
/* Set default response value. */
p_device_cb_param->status = RET_OK;
esphome::wake_loop_threadsafe();
App.wake_loop_threadsafe();
// endpoints are enumerated from 1
if (global_zigbee->callbacks_.size() >= endpoint) {
@@ -138,6 +138,7 @@ void ZigbeeComponent::on_join_(bool factory_new) {
ESP_LOGD(TAG, "Joined the network");
this->join_cb_.call(factory_new);
});
App.wake_loop_threadsafe();
}
void ZigbeeComponent::on_start_() {
@@ -145,6 +146,7 @@ void ZigbeeComponent::on_start_() {
ESP_LOGD(TAG, "Started zigbee stack");
this->start_cb_.call();
});
App.wake_loop_threadsafe();
}
#ifdef USE_ZIGBEE_WIPE_ON_BOOT
+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
+2
View File
@@ -17,6 +17,8 @@ CONFIG_ESP_TASK_WDT_INIT=y
CONFIG_ESP_TASK_WDT_PANIC=y
CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0=n
CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1=n
CONFIG_FREERTOS_USE_TICKLESS_IDLE=y
CONFIG_PM_ENABLE=y
# esp32_ble
CONFIG_BT_ENABLED=y
@@ -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}