diff --git a/esphome/components/zigbee/__init__.py b/esphome/components/zigbee/__init__.py index 0dc3b4b6f71..d01eddb9ff7 100644 --- a/esphome/components/zigbee/__init__.py +++ b/esphome/components/zigbee/__init__.py @@ -13,7 +13,14 @@ from esphome.components.esp32.const import ( VARIANT_ESP32S31, ) import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_INTERNAL, CONF_MODEL, CONF_NAME, CONF_ON_START +from esphome.const import ( + CONF_ID, + CONF_INTERNAL, + CONF_MODEL, + CONF_NAME, + CONF_ON_START, + CONF_POLLING_INTERVAL, +) from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.types import ConfigType @@ -25,6 +32,7 @@ from .const import ( CONF_POWER_SOURCE, CONF_REPORT, CONF_ROUTER, + CONF_SLEEPY, CONF_USE_DEVICE_TYPE, CONF_WIPE_ON_BOOT, KEY_ZIGBEE, @@ -35,7 +43,6 @@ from .const import ( from .const_zephyr import ( CONF_IEEE802154_VENDOR_OUI, CONF_MAX_EP_NUMBER_ZEPHYR, - CONF_SLEEPY, CONF_ZIGBEE_ID, KEY_EP_NUMBER, ) @@ -128,8 +135,11 @@ NUMBER_SCHEMA = ( def _validate_router_sleepy(config: ConfigType) -> ConfigType: - if config.get(CONF_ROUTER) and config.get(CONF_SLEEPY): - raise cv.Invalid("router and sleepy are mutually exclusive") + if config.get(CONF_ROUTER): + if config.get(CONF_SLEEPY): + raise cv.Invalid("router and sleepy are mutually exclusive") + if config.get(CONF_POLLING_INTERVAL): + raise cv.Invalid(f"{CONF_POLLING_INTERVAL} is only valid for end devices.") return config @@ -160,9 +170,17 @@ CONFIG_SCHEMA = cv.All( ), cv.requires_component("nrf52"), ), - cv.OnlyWith(CONF_SLEEPY, "nrf52", default=False): cv.All( + cv.Optional(CONF_SLEEPY, default=False): cv.All( cv.boolean, ), + cv.Optional(CONF_POLLING_INTERVAL): cv.All( + cv.requires_component(ESP32_DOMAIN), + cv.positive_time_period_milliseconds, + cv.Range( + min=cv.TimePeriodMilliseconds(milliseconds=200), + max=cv.TimePeriodMinutes(minutes=60), # 1 hour + ), + ), } ).extend(cv.COMPONENT_SCHEMA), _validate_router_sleepy, diff --git a/esphome/components/zigbee/const.py b/esphome/components/zigbee/const.py index 86cac665b43..aad2b5c0e39 100644 --- a/esphome/components/zigbee/const.py +++ b/esphome/components/zigbee/const.py @@ -72,6 +72,7 @@ CONF_WIPE_ON_BOOT = "wipe_on_boot" CONF_REPORT = "report" CONF_ROUTER = "router" CONF_POWER_SOURCE = "power_source" +CONF_SLEEPY = "sleepy" CONF_USE_DEVICE_TYPE = "use_device_type" POWER_SOURCE = { "UNKNOWN": 0x00, # ZB_ZCL_BASIC_POWER_SOURCE_UNKNOWN diff --git a/esphome/components/zigbee/const_zephyr.py b/esphome/components/zigbee/const_zephyr.py index bf8e8287c4b..7177b9fc798 100644 --- a/esphome/components/zigbee/const_zephyr.py +++ b/esphome/components/zigbee/const_zephyr.py @@ -4,7 +4,6 @@ CONF_ZIGBEE_BINARY_SENSOR = "zigbee_binary_sensor" CONF_ZIGBEE_SENSOR = "zigbee_sensor" CONF_ZIGBEE_SWITCH = "zigbee_switch" CONF_ZIGBEE_NUMBER = "zigbee_number" -CONF_SLEEPY = "sleepy" CONF_IEEE802154_VENDOR_OUI = "ieee802154_vendor_oui" # Keys for CORE.data storage diff --git a/esphome/components/zigbee/zigbee_esp32.cpp b/esphome/components/zigbee/zigbee_esp32.cpp index 89417e748d2..7c197133c2b 100644 --- a/esphome/components/zigbee/zigbee_esp32.cpp +++ b/esphome/components/zigbee/zigbee_esp32.cpp @@ -319,7 +319,7 @@ void ZigbeeComponent::setup() { #else esp_zigbee_zed_config_s zb_zed_cfg = { .ed_timeout = EZB_NWK_ED_TIMEOUT_64MIN, - .keep_alive = ED_KEEP_ALIVE, + .keep_alive = this->keep_alive_, }; device_config.zed_config = zb_zed_cfg; #endif @@ -339,6 +339,8 @@ void ZigbeeComponent::setup() { #ifdef CONFIG_ZB_ZCZR ezb_bdb_set_router_rejoin_required(true); +#else + ezb_nwk_set_rx_on_when_idle(!this->sleepy_); // if sleepy, disable RX when idle #endif ezb_aps_secur_enable_distributed_security(false); @@ -419,18 +421,27 @@ void ZigbeeComponent::loop() { void ZigbeeComponent::dump_config() { if (esp_zigbee_lock_acquire(10 / portTICK_PERIOD_MS)) { - ESP_LOGCONFIG(TAG, - "Zigbee\n" - " Model: %.*s\n" - " Router: %s\n" - " Device is joined to the network: %s\n" - " Current channel: %d\n" - " Short addr: 0x%04X\n" - " Short pan id: 0x%04X", - this->basic_cluster_data_.model[0], - reinterpret_cast(this->basic_cluster_data_.model + 1), - YESNO(this->device_role_ == EZB_NWK_DEVICE_TYPE_ROUTER), YESNO(ezb_bdb_dev_joined()), - ezb_nwk_get_current_channel(), ezb_nwk_get_short_address(), ezb_nwk_get_panid()); + ESP_LOGCONFIG( + TAG, + "Zigbee\n" + " Model: %.*s\n" +#ifdef CONFIG_ZB_ZCZR + " Router: %s\n" +#else + " Sleepy end device: %s\n" + " Poll interval: %" PRIu32 " ms\n" +#endif + " Device is joined to the network: %s\n" + " Current channel: %d\n" + " Short addr: 0x%04X\n" + " Short pan id: 0x%04X", + this->basic_cluster_data_.model[0], reinterpret_cast(this->basic_cluster_data_.model + 1), +#ifdef CONFIG_ZB_ZCZR + YESNO(this->device_role_ == EZB_NWK_DEVICE_TYPE_ROUTER), +#else + YESNO(this->sleepy_), this->keep_alive_, +#endif + YESNO(ezb_bdb_dev_joined()), ezb_nwk_get_current_channel(), ezb_nwk_get_short_address(), ezb_nwk_get_panid()); esp_zigbee_lock_release(); } else { ESP_LOGCONFIG(TAG, diff --git a/esphome/components/zigbee/zigbee_esp32.h b/esphome/components/zigbee/zigbee_esp32.h index 093ae959390..28ea64c8cea 100644 --- a/esphome/components/zigbee/zigbee_esp32.h +++ b/esphome/components/zigbee/zigbee_esp32.h @@ -20,7 +20,6 @@ namespace esphome::zigbee { /* Zigbee configuration */ -static const uint16_t ED_KEEP_ALIVE = 3000; /* 3000 millisecond */ static const uint8_t MAX_CHILDREN = 10; static const uint32_t EZB_PRIMARY_CHANNEL_MASK = 0x07FFF800U; /* channels 11-26 */ static constexpr uint8_t MAX_ZB_QUEUE_SIZE = 32; @@ -43,6 +42,8 @@ class ZigbeeComponent final : public Component { void loop() override; void dump_config() override; + void set_sleepy(bool sleepy) { this->sleepy_ = sleepy; } + void set_keep_alive(uint32_t keep_alive) { this->keep_alive_ = keep_alive; } void set_basic_cluster(const char *model, const char *manufacturer, uint8_t power_source); void add_cluster(uint8_t endpoint_id, uint16_t cluster_id, uint8_t role); void create_default_cluster(uint8_t endpoint_id, uint16_t device_id); @@ -73,6 +74,8 @@ class ZigbeeComponent final : public Component { bool is_joined() { return this->joined_; } protected: + bool sleepy_{false}; + uint32_t keep_alive_{3000}; struct { uint8_t *model; uint8_t *manufacturer; diff --git a/esphome/components/zigbee/zigbee_esp32.py b/esphome/components/zigbee/zigbee_esp32.py index 103e901b13b..68136d57192 100644 --- a/esphome/components/zigbee/zigbee_esp32.py +++ b/esphome/components/zigbee/zigbee_esp32.py @@ -24,6 +24,7 @@ from esphome.const import ( CONF_MAX_LENGTH, CONF_MODEL, CONF_NAME, + CONF_POLLING_INTERVAL, CONF_TYPE, CONF_UNIT_OF_MEASUREMENT, CONF_VALUE, @@ -45,6 +46,7 @@ from .const import ( CONF_POWER_SOURCE, CONF_REPORT, CONF_ROUTER, + CONF_SLEEPY, CONF_USE_DEVICE_TYPE, KEY_ZIGBEE, POWER_SOURCE, @@ -412,6 +414,11 @@ async def esp32_to_code(config: ConfigType) -> "MockObj": # setup zigbee components var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) + if config.get(CONF_SLEEPY): + cg.add(var.set_sleepy(True)) + if (keep_alive := config.get(CONF_POLLING_INTERVAL, 3000)) != 3000: + cg.add(var.set_keep_alive(keep_alive)) + cg.add( var.set_basic_cluster( config[CONF_MODEL], diff --git a/esphome/components/zigbee/zigbee_zephyr.py b/esphome/components/zigbee/zigbee_zephyr.py index 3f863140e69..8197796b6db 100644 --- a/esphome/components/zigbee/zigbee_zephyr.py +++ b/esphome/components/zigbee/zigbee_zephyr.py @@ -24,6 +24,7 @@ from .const import ( BACNET_UNITS, CONF_POWER_SOURCE, CONF_ROUTER, + CONF_SLEEPY, CONF_WIPE_ON_BOOT, KEY_ZIGBEE, POWER_SOURCE, @@ -35,7 +36,6 @@ from .const import ( ) from .const_zephyr import ( CONF_IEEE802154_VENDOR_OUI, - CONF_SLEEPY, CONF_ZIGBEE_BINARY_SENSOR, CONF_ZIGBEE_ID, CONF_ZIGBEE_NUMBER, diff --git a/tests/components/zigbee/common_esp32.yaml b/tests/components/zigbee/common_esp32.yaml index 3d19180c064..84b79bb84f0 100644 --- a/tests/components/zigbee/common_esp32.yaml +++ b/tests/components/zigbee/common_esp32.yaml @@ -58,8 +58,9 @@ switch: zigbee: model: zigbee_test - router: false - power_source: MAINS_SINGLE_PHASE + sleepy: true + polling_interval: 2s + power_source: BATTERY on_join: then: - logger.log: "Joined network"