diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 57b49a686..5003571da 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -522,6 +522,9 @@ pytest_esp32c3_esp_matter_dut: - cd ${ESP_MATTER_PATH} - rm -rf connectedhomeip/connectedhomeip - ln -s ${CHIP_SUBMODULE_PATH} connectedhomeip/connectedhomeip + - cd connectedhomeip/connectedhomeip + - source out/py-env/bin/activate + - cd ${ESP_MATTER_PATH} - pip install -r tools/ci/requirements-pytest.txt - pytest examples/ --target esp32c3 -m esp_matter_dut --junitxml=XUNIT_RESULT.xml tags: ["esp32c3", "esp_matter_dut"] diff --git a/components/esp_matter/CMakeLists.txt b/components/esp_matter/CMakeLists.txt index 3a5520e77..19b37ffbd 100644 --- a/components/esp_matter/CMakeLists.txt +++ b/components/esp_matter/CMakeLists.txt @@ -45,6 +45,9 @@ if (CONFIG_ESP_MATTER_ENABLE_MATTER_SERVER) list(APPEND SRC_DIRS_LIST "data_model" "data_model_provider" "data_model/private") + if (NOT CONFIG_ESP_MATTER_ENABLE_OPTIONAL_ATTRIBUTES) + list(APPEND EXCLUDE_SRCS_LIST "data_model/esp_matter_optional_attribute.cpp") + endif() list(APPEND INCLUDE_DIRS_LIST "zap_common" "data_model") list(APPEND PRIV_INCLUDE_DIRS_LIST "data_model/private") diff --git a/components/esp_matter/Kconfig b/components/esp_matter/Kconfig index ff2c4b90c..ed8f563ee 100644 --- a/components/esp_matter/Kconfig +++ b/components/esp_matter/Kconfig @@ -263,6 +263,14 @@ menu "ESP Matter" cluster will NOT be added to the root node endpoint, per the Matter spec CustomNetworkConfig condition. + config ESP_MATTER_ENABLE_OPTIONAL_ATTRIBUTES + bool "Enable optional attributes helper APIs" + depends on ESP_MATTER_ENABLE_DATA_MODEL + default n + help + Enable helper APIs to create optional attributes for various Matter clusters. + This is intended for testing purposes only. + menu "Select Supported Matter Clusters" visible if ESP_MATTER_ENABLE_DATA_MODEL diff --git a/components/esp_matter/data_model/esp_matter_optional_attribute.cpp b/components/esp_matter/data_model/esp_matter_optional_attribute.cpp new file mode 100644 index 000000000..e39b4da82 --- /dev/null +++ b/components/esp_matter/data_model/esp_matter_optional_attribute.cpp @@ -0,0 +1,184 @@ +// Copyright 2026 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// This file provides helper APIs to create optional attributes for various Matter clusters. +// It is intended for testing purposes only and is compiled only when CONFIG_ESP_MATTER_ENABLE_OPTIONAL_ATTRIBUTES is enabled. + +#include "esp_matter_optional_attribute.h" +#include +#include + +#ifdef CONFIG_ESP_MATTER_ENABLE_OPTIONAL_ATTRIBUTES +static const char *TAG = "optional_attr"; + +namespace esp_matter { +namespace cluster { + +namespace basic_information { +esp_err_t create_optional_attributes(cluster_t *cluster) +{ + ESP_RETURN_ON_FALSE(cluster, ESP_ERR_INVALID_ARG, TAG, "Cluster cannot be NULL"); + + ESP_RETURN_ON_FALSE(basic_information::attribute::create_manufacturing_date(cluster, NULL, 0), ESP_ERR_NO_MEM, TAG, "Failed to create manufacturing_date"); + ESP_RETURN_ON_FALSE(basic_information::attribute::create_part_number(cluster, NULL, 0), ESP_ERR_NO_MEM, TAG, "Failed to create part_number"); + ESP_RETURN_ON_FALSE(basic_information::attribute::create_product_url(cluster, NULL, 0), ESP_ERR_NO_MEM, TAG, "Failed to create product_url"); + ESP_RETURN_ON_FALSE(basic_information::attribute::create_product_label(cluster, NULL, 0), ESP_ERR_NO_MEM, TAG, "Failed to create product_label"); + ESP_RETURN_ON_FALSE(basic_information::attribute::create_serial_number(cluster, NULL, 0), ESP_ERR_NO_MEM, TAG, "Failed to create serial_number"); + ESP_RETURN_ON_FALSE(basic_information::attribute::create_local_config_disabled(cluster, false), ESP_ERR_NO_MEM, TAG, "Failed to create local_config_disabled"); + ESP_RETURN_ON_FALSE(basic_information::attribute::create_reachable(cluster, true), ESP_ERR_NO_MEM, TAG, "Failed to create reachable"); + ESP_RETURN_ON_FALSE(basic_information::attribute::create_product_appearance(cluster, NULL, 0, 0), ESP_ERR_NO_MEM, TAG, "Failed to create product_appearance"); + + return ESP_OK; +} +} /* basic_information */ + +namespace boolean_state_configuration { +esp_err_t create_optional_attributes(cluster_t *cluster) +{ + ESP_RETURN_ON_FALSE(cluster, ESP_ERR_INVALID_ARG, TAG, "Cluster cannot be NULL"); + + ESP_RETURN_ON_FALSE(boolean_state_configuration::attribute::create_default_sensitivity_level(cluster, 0), ESP_ERR_NO_MEM, TAG, "Failed to create default_sensitivity_level"); + ESP_RETURN_ON_FALSE(boolean_state_configuration::attribute::create_alarms_enabled(cluster, 0), ESP_ERR_NO_MEM, TAG, "Failed to create alarms_enabled"); + ESP_RETURN_ON_FALSE(boolean_state_configuration::attribute::create_sensor_fault(cluster, 0), ESP_ERR_NO_MEM, TAG, "Failed to create sensor_fault"); + + return ESP_OK; +} +} /* boolean_state_configuration */ + +namespace electrical_energy_measurement { +esp_err_t create_optional_attributes(cluster_t *cluster) +{ + ESP_RETURN_ON_FALSE(cluster, ESP_ERR_INVALID_ARG, TAG, "Cluster cannot be NULL"); + + ESP_RETURN_ON_FALSE(electrical_energy_measurement::attribute::create_cumulative_energy_reset(cluster, NULL, 0, 0), ESP_ERR_NO_MEM, TAG, "Failed to create cumulative_energy_reset"); + + return ESP_OK; +} +} /* electrical_energy_measurement */ + +namespace electrical_power_measurement { +esp_err_t create_optional_attributes(cluster_t *cluster) +{ + ESP_RETURN_ON_FALSE(cluster, ESP_ERR_INVALID_ARG, TAG, "Cluster cannot be NULL"); + + ESP_RETURN_ON_FALSE(electrical_power_measurement::attribute::create_ranges(cluster, NULL, 0, 0), ESP_ERR_NO_MEM, TAG, "Failed to create ranges"); + ESP_RETURN_ON_FALSE(electrical_power_measurement::attribute::create_voltage(cluster, nullable()), ESP_ERR_NO_MEM, TAG, "Failed to create voltage"); + ESP_RETURN_ON_FALSE(electrical_power_measurement::attribute::create_active_current(cluster, nullable()), ESP_ERR_NO_MEM, TAG, "Failed to create active_current"); + ESP_RETURN_ON_FALSE(electrical_power_measurement::attribute::create_reactive_current(cluster, nullable()), ESP_ERR_NO_MEM, TAG, "Failed to create reactive_current"); + ESP_RETURN_ON_FALSE(electrical_power_measurement::attribute::create_apparent_current(cluster, nullable()), ESP_ERR_NO_MEM, TAG, "Failed to create apparent_current"); + ESP_RETURN_ON_FALSE(electrical_power_measurement::attribute::create_reactive_power(cluster, nullable()), ESP_ERR_NO_MEM, TAG, "Failed to create reactive_power"); + ESP_RETURN_ON_FALSE(electrical_power_measurement::attribute::create_apparent_power(cluster, nullable()), ESP_ERR_NO_MEM, TAG, "Failed to create apparent_power"); + ESP_RETURN_ON_FALSE(electrical_power_measurement::attribute::create_rms_voltage(cluster, nullable()), ESP_ERR_NO_MEM, TAG, "Failed to create rms_voltage"); + ESP_RETURN_ON_FALSE(electrical_power_measurement::attribute::create_rms_current(cluster, nullable()), ESP_ERR_NO_MEM, TAG, "Failed to create rms_current"); + ESP_RETURN_ON_FALSE(electrical_power_measurement::attribute::create_rms_power(cluster, nullable()), ESP_ERR_NO_MEM, TAG, "Failed to create rms_power"); + ESP_RETURN_ON_FALSE(electrical_power_measurement::attribute::create_frequency(cluster, nullable()), ESP_ERR_NO_MEM, TAG, "Failed to create frequency"); + ESP_RETURN_ON_FALSE(electrical_power_measurement::attribute::create_power_factor(cluster, nullable()), ESP_ERR_NO_MEM, TAG, "Failed to create power_factor"); + ESP_RETURN_ON_FALSE(electrical_power_measurement::attribute::create_neutral_current(cluster, nullable()), ESP_ERR_NO_MEM, TAG, "Failed to create neutral_current"); + + return ESP_OK; +} +} /* electrical_power_measurement */ + +namespace ethernet_network_diagnostics { +esp_err_t create_optional_attributes(cluster_t *cluster) +{ + ESP_RETURN_ON_FALSE(cluster, ESP_ERR_INVALID_ARG, TAG, "Cluster cannot be NULL"); + + ESP_RETURN_ON_FALSE(ethernet_network_diagnostics::attribute::create_phy_rate(cluster, nullable()), ESP_ERR_NO_MEM, TAG, "Failed to create phy_rate"); + ESP_RETURN_ON_FALSE(ethernet_network_diagnostics::attribute::create_full_duplex(cluster, nullable()), ESP_ERR_NO_MEM, TAG, "Failed to create full_duplex"); + ESP_RETURN_ON_FALSE(ethernet_network_diagnostics::attribute::create_carrier_detect(cluster, nullable()), ESP_ERR_NO_MEM, TAG, "Failed to create carrier_detect"); + ESP_RETURN_ON_FALSE(ethernet_network_diagnostics::attribute::create_time_since_reset(cluster, 0), ESP_ERR_NO_MEM, TAG, "Failed to create time_since_reset"); + + return ESP_OK; +} +} /* ethernet_network_diagnostics */ + +namespace general_diagnostics { +esp_err_t create_optional_attributes(cluster_t *cluster) +{ + ESP_RETURN_ON_FALSE(cluster, ESP_ERR_INVALID_ARG, TAG, "Cluster cannot be NULL"); + + ESP_RETURN_ON_FALSE(general_diagnostics::attribute::create_total_operational_hours(cluster, 0), ESP_ERR_NO_MEM, TAG, "Failed to create total_operational_hours"); + ESP_RETURN_ON_FALSE(general_diagnostics::attribute::create_boot_reason(cluster, 0), ESP_ERR_NO_MEM, TAG, "Failed to create boot_reason"); + ESP_RETURN_ON_FALSE(general_diagnostics::attribute::create_active_hardware_faults(cluster, NULL, 0, 0), ESP_ERR_NO_MEM, TAG, "Failed to create active_hardware_faults"); + ESP_RETURN_ON_FALSE(general_diagnostics::attribute::create_active_radio_faults(cluster, NULL, 0, 0), ESP_ERR_NO_MEM, TAG, "Failed to create active_radio_faults"); + ESP_RETURN_ON_FALSE(general_diagnostics::attribute::create_active_network_faults(cluster, NULL, 0, 0), ESP_ERR_NO_MEM, TAG, "Failed to create active_network_faults"); + + return ESP_OK; +} +} /* general_diagnostics */ + +namespace occupancy_sensing { +esp_err_t create_optional_attributes(cluster_t *cluster) +{ + ESP_RETURN_ON_FALSE(cluster, ESP_ERR_INVALID_ARG, TAG, "Cluster cannot be NULL"); + + ESP_RETURN_ON_FALSE(occupancy_sensing::attribute::create_hold_time(cluster, 0), ESP_ERR_NO_MEM, TAG, "Failed to create hold_time"); + ESP_RETURN_ON_FALSE(occupancy_sensing::attribute::create_hold_time_limits(cluster, NULL, 0, 0), ESP_ERR_NO_MEM, TAG, "Failed to create hold_time_limits"); + + return ESP_OK; +} +} /* occupancy_sensing */ + +namespace resource_monitoring { +esp_err_t create_optional_attributes(cluster_t *cluster) +{ + ESP_RETURN_ON_FALSE(cluster, ESP_ERR_INVALID_ARG, TAG, "Cluster cannot be NULL"); + + ESP_RETURN_ON_FALSE(resource_monitoring::attribute::create_in_place_indicator(cluster, false), ESP_ERR_NO_MEM, TAG, "Failed to create in_place_indicator"); + ESP_RETURN_ON_FALSE(resource_monitoring::attribute::create_last_changed_time(cluster, nullable()), ESP_ERR_NO_MEM, TAG, "Failed to create last_changed_time"); + + return ESP_OK; +} +} /* resource_monitoring */ + +namespace software_diagnostics { +esp_err_t create_optional_attributes(cluster_t *cluster) +{ + ESP_RETURN_ON_FALSE(cluster, ESP_ERR_INVALID_ARG, TAG, "Cluster cannot be NULL"); + + ESP_RETURN_ON_FALSE(software_diagnostics::attribute::create_thread_metrics(cluster, NULL, 0, 0), ESP_ERR_NO_MEM, TAG, "Failed to create thread_metrics"); + ESP_RETURN_ON_FALSE(software_diagnostics::attribute::create_current_heap_free(cluster, 0), ESP_ERR_NO_MEM, TAG, "Failed to create current_heap_free"); + ESP_RETURN_ON_FALSE(software_diagnostics::attribute::create_current_heap_used(cluster, 0), ESP_ERR_NO_MEM, TAG, "Failed to create current_heap_used"); + + return ESP_OK; +} +} /* software_diagnostics */ + +namespace time_synchronization { +esp_err_t create_optional_attributes(cluster_t *cluster) +{ + ESP_RETURN_ON_FALSE(cluster, ESP_ERR_INVALID_ARG, TAG, "Cluster cannot be NULL"); + + ESP_RETURN_ON_FALSE(time_synchronization::attribute::create_time_source(cluster, 0), ESP_ERR_NO_MEM, TAG, "Failed to create time_source"); + ESP_RETURN_ON_FALSE(time_synchronization::attribute::create_trusted_time_source(cluster, NULL, 0, 0), ESP_ERR_NO_MEM, TAG, "Failed to create trusted_time_source"); + + return ESP_OK; +} +} /* time_synchronization */ + +namespace wifi_network_diagnostics { +esp_err_t create_optional_attributes(cluster_t *cluster) +{ + ESP_RETURN_ON_FALSE(cluster, ESP_ERR_INVALID_ARG, TAG, "Cluster cannot be NULL"); + + ESP_RETURN_ON_FALSE(wifi_network_diagnostics::attribute::create_current_max_rate(cluster, nullable()), ESP_ERR_NO_MEM, TAG, "Failed to create current_max_rate"); + + return ESP_OK; +} +} /* wifi_network_diagnostics */ + +} /* cluster */ +} /* esp_matter */ +#endif /* CONFIG_ESP_MATTER_ENABLE_OPTIONAL_ATTRIBUTES */ \ No newline at end of file diff --git a/components/esp_matter/data_model/esp_matter_optional_attribute.h b/components/esp_matter/data_model/esp_matter_optional_attribute.h new file mode 100644 index 000000000..3108ad481 --- /dev/null +++ b/components/esp_matter/data_model/esp_matter_optional_attribute.h @@ -0,0 +1,72 @@ +// Copyright 2026 Espressif Systems (Shanghai) PTE LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include "sdkconfig.h" +#ifdef CONFIG_ESP_MATTER_ENABLE_OPTIONAL_ATTRIBUTES + +#include + +namespace esp_matter { +namespace cluster { + +namespace basic_information { +esp_err_t create_optional_attributes(cluster_t *cluster); +} /* basic_information */ + +namespace boolean_state_configuration { +esp_err_t create_optional_attributes(cluster_t *cluster); +} /* boolean_state_configuration */ + +namespace electrical_energy_measurement { +esp_err_t create_optional_attributes(cluster_t *cluster); +} /* electrical_energy_measurement */ + +namespace electrical_power_measurement { +esp_err_t create_optional_attributes(cluster_t *cluster); +} /* electrical_power_measurement */ + +namespace ethernet_network_diagnostics { +esp_err_t create_optional_attributes(cluster_t *cluster); +} /* ethernet_network_diagnostics */ + +namespace general_diagnostics { +esp_err_t create_optional_attributes(cluster_t *cluster); +} /* general_diagnostics */ + +namespace occupancy_sensing { +esp_err_t create_optional_attributes(cluster_t *cluster); +} /* occupancy_sensing */ + +namespace resource_monitoring { +esp_err_t create_optional_attributes(cluster_t *cluster); +} /* resource_monitoring */ + +namespace software_diagnostics { +esp_err_t create_optional_attributes(cluster_t *cluster); +} /* software_diagnostics */ + +namespace time_synchronization { +esp_err_t create_optional_attributes(cluster_t *cluster); +} /* time_synchronization */ + +namespace wifi_network_diagnostics { +esp_err_t create_optional_attributes(cluster_t *cluster); +} /* wifi_network_diagnostics */ + +} /* cluster */ +} /* esp_matter */ + +#endif /* CONFIG_ESP_MATTER_ENABLE_OPTIONAL_ATTRIBUTES */ diff --git a/components/esp_matter/data_model_provider/esp_matter_plugin_server_init_callbacks.cpp b/components/esp_matter/data_model_provider/esp_matter_plugin_server_init_callbacks.cpp index 798efdb79..7b6ec6b3d 100644 --- a/components/esp_matter/data_model_provider/esp_matter_plugin_server_init_callbacks.cpp +++ b/components/esp_matter/data_model_provider/esp_matter_plugin_server_init_callbacks.cpp @@ -70,3 +70,4 @@ void MatterCommodityTariffPluginServerInitCallback() {} void MatterCommodityPricePluginServerInitCallback() {} void MatterElectricalGridConditionsPluginServerInitCallback() {} void MatterSoilMeasurementPluginServerInitCallback() {} +void MatterBooleanStateConfigurationPluginServerInitCallback() {} diff --git a/examples/.build-rules.yml b/examples/.build-rules.yml index 879355005..ea58f8806 100644 --- a/examples/.build-rules.yml +++ b/examples/.build-rules.yml @@ -146,3 +146,9 @@ examples/unit_test_app: - if: IDF_TARGET in ["esp32c3"] temporary: true reason: the other targets are not tested yet + +examples/test_apps/test_optional_attributes: + enable: + - if: IDF_TARGET in ["esp32c3"] + temporary: true + reason: the other targets are not tested yet diff --git a/examples/test_apps/pytest_esp_matter_optional_test.py b/examples/test_apps/pytest_esp_matter_optional_test.py new file mode 100644 index 000000000..344a91837 --- /dev/null +++ b/examples/test_apps/pytest_esp_matter_optional_test.py @@ -0,0 +1,60 @@ +# SPDX-License-Identifier: CC0-1.0 + +import pathlib +import pytest +import time +import re +import subprocess +from pytest_embedded import Dut +import os +import sys + +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '../../tools/ci'))) +from gitlab_api import GitLabAPI + +CURRENT_DIR = str(pathlib.Path(__file__).parent) + '/test_optional_attributes' +ESP_MATTER_PATH = str(pathlib.Path(__file__).parent.parent.parent) +TEST_SCRIPT_PATH = str(pathlib.Path(__file__).parent.parent.parent / 'tools' / 'test_optional_attributes' / 'test_optional_attributes_framework.py') +PAA_CERTS_PATH = str(pathlib.Path(__file__).parent.parent.parent / 'connectedhomeip' / 'connectedhomeip' / 'credentials' / 'development' / 'paa-root-certs') + +pytest_build_dir = CURRENT_DIR + +gitlab_api = GitLabAPI() +PYTEST_SSID = gitlab_api.ci_gitlab_pytest_ssid +PYTEST_PASSPHRASE = gitlab_api.ci_gitlab_pytest_passphrase + + +@pytest.mark.esp32c3 +@pytest.mark.esp_matter_dut +@pytest.mark.parametrize( + 'count, app_path, target, erase_all', [ + (1, pytest_build_dir, 'esp32c3', 'y'), + ], + indirect=True, +) +def test_optional_attributes_c3(dut: Dut) -> None: + dut.expect(r'Configuring CHIPoBLE advertising', timeout=20) + time.sleep(5) + + command = ( + f"python3 {TEST_SCRIPT_PATH}" + f" -n 1" + f" --commissioning-method ble-wifi" + f" --wifi-ssid {PYTEST_SSID}" + f" --wifi-passphrase {PYTEST_PASSPHRASE}" + f" --passcode 20202021" + f" --discriminator 3840" + f" --paa-trust-store-path {PAA_CERTS_PATH}" + ) + + out_str = subprocess.getoutput(command) + print(out_str) + + passed = re.search(r'Passed:\s+(\d+)', out_str) + failed = re.search(r'Failed:\s+(\d+)', out_str) + + if failed and int(failed.group(1)) > 0: + assert False, f"Optional attributes test failed. {failed.group(1)} failures detected." + + if not passed or int(passed.group(1)) == 0: + assert False, "Optional attributes test did not report any passed results." diff --git a/examples/test_apps/test_optional_attributes/CMakeLists.txt b/examples/test_apps/test_optional_attributes/CMakeLists.txt new file mode 100644 index 000000000..7f4416588 --- /dev/null +++ b/examples/test_apps/test_optional_attributes/CMakeLists.txt @@ -0,0 +1,35 @@ +# The following lines of boilerplate have to be in your project's +# CMakeLists in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.5) + +if(NOT DEFINED ENV{ESP_MATTER_PATH}) + message(FATAL_ERROR "Please set ESP_MATTER_PATH to the path of esp-matter repo") +endif(NOT DEFINED ENV{ESP_MATTER_PATH}) + +if(NOT DEFINED CLI_PROJECT_VER) + set(PROJECT_VER "1.0") +else() + set(PROJECT_VER "${CLI_PROJECT_VER}") +endif() + +if(NOT DEFINED CLI_PROJECT_VER_NUMBER) + set(PROJECT_VER_NUMBER 1) +else() + set(PROJECT_VER_NUMBER "${CLI_PROJECT_VER_NUMBER}") +endif() + +set(ESP_MATTER_PATH $ENV{ESP_MATTER_PATH}) +set(MATTER_SDK_PATH ${ESP_MATTER_PATH}/connectedhomeip/connectedhomeip) + +# This should be done before using the IDF_TARGET variable. +include($ENV{IDF_PATH}/tools/cmake/project.cmake) + +set(EXTRA_COMPONENT_DIRS + "${MATTER_SDK_PATH}/config/esp32/components" + "${ESP_MATTER_PATH}/components" + ${extra_components_dirs_append}) + +project(test_optional_attributes) + +idf_build_set_property(CXX_COMPILE_OPTIONS "-std=gnu++17;-Os;-DCHIP_HAVE_CONFIG_H;-Wno-overloaded-virtual" APPEND) +idf_build_set_property(C_COMPILE_OPTIONS "-Os" APPEND) diff --git a/examples/test_apps/test_optional_attributes/README.md b/examples/test_apps/test_optional_attributes/README.md new file mode 100644 index 000000000..1e22e8663 --- /dev/null +++ b/examples/test_apps/test_optional_attributes/README.md @@ -0,0 +1,32 @@ +# Optional Attributes Test Application + +This example application creates a Matter node with multiple clusters on the root endpoint, each with all their optional attributes enabled. It serves as the device-side companion to the optional attributes test framework. + +## Overview + +The application: +- Creates a Matter Root Node on endpoint 0 +- Adds optional attributes to existing clusters (Basic Information, General Diagnostics) +- Creates additional clusters (Boolean State Configuration, Electrical Energy Measurement, Electrical Power Measurement, Ethernet Network Diagnostics, Occupancy Sensing, HEPA Filter Monitoring, Software Diagnostics, Time Synchronization, WiFi Network Diagnostics) with their optional attributes enabled +- Enables the Matter console shell for diagnostics and attribute inspection + +## Hardware Required + +ESP32 board. + +## Build and Flash + +```bash +cd examples/test_apps/test_optional_attributes +idf.py set-target +idf.py build flash monitor +``` + +## Configuration + +The key sdkconfig option for this example is: +- `CONFIG_ESP_MATTER_ENABLE_OPTIONAL_ATTRIBUTES=y` — enables the optional attribute helper APIs + +## Running Tests + +This application is meant to be used with the optional attributes test framework. See [Optional Attributes Test Suite](../../../tools/test_optional_attributes/README.md) for instructions on commissioning the device and running the test scripts. diff --git a/examples/test_apps/test_optional_attributes/main/CMakeLists.txt b/examples/test_apps/test_optional_attributes/main/CMakeLists.txt new file mode 100644 index 000000000..7612c1877 --- /dev/null +++ b/examples/test_apps/test_optional_attributes/main/CMakeLists.txt @@ -0,0 +1,10 @@ +set(SRC_DIRS_LIST "." ) +set(INCLUDE_DIRS_LIST ".") +list(APPEND PRIV_INCLUDE_DIRS_LIST "${ESP_MATTER_PATH}/examples/common/utils") + + +idf_component_register(SRC_DIRS ${SRC_DIRS_LIST} + PRIV_INCLUDE_DIRS "." ${PRIV_INCLUDE_DIRS_LIST} + INCLUDE_DIRS ${INCLUDE_DIRS_LIST}) + +target_compile_options(${COMPONENT_LIB} PRIVATE "-DCHIP_HAVE_CONFIG_H") diff --git a/examples/test_apps/test_optional_attributes/main/app_main.cpp b/examples/test_apps/test_optional_attributes/main/app_main.cpp new file mode 100644 index 000000000..9c02bbd31 --- /dev/null +++ b/examples/test_apps/test_optional_attributes/main/app_main.cpp @@ -0,0 +1,216 @@ +/* + This example code is in the Public Domain (or CC0 licensed, at your option.) + + Unless required by applicable law or agreed to in writing, this + software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR + CONDITIONS OF ANY KIND, either express or implied. +*/ + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include + +static const char *TAG = "app_main"; +uint16_t test_endpoint_id = 0; + +using namespace esp_matter; +using namespace esp_matter::attribute; +using namespace esp_matter::endpoint; +using namespace chip::app::Clusters; + +constexpr auto k_timeout_seconds = 300; + +static void app_event_cb(const ChipDeviceEvent *event, intptr_t arg) +{ + switch (event->Type) { + case chip::DeviceLayer::DeviceEventType::kInterfaceIpAddressChanged: + ESP_LOGI(TAG, "Interface IP Address changed"); + break; + + case chip::DeviceLayer::DeviceEventType::kCommissioningComplete: + ESP_LOGI(TAG, "Commissioning complete"); + break; + + case chip::DeviceLayer::DeviceEventType::kFailSafeTimerExpired: + ESP_LOGI(TAG, "Commissioning failed, fail safe timer expired"); + break; + + case chip::DeviceLayer::DeviceEventType::kCommissioningSessionStarted: + ESP_LOGI(TAG, "Commissioning session started"); + break; + + case chip::DeviceLayer::DeviceEventType::kCommissioningSessionStopped: + ESP_LOGI(TAG, "Commissioning session stopped"); + break; + + case chip::DeviceLayer::DeviceEventType::kCommissioningWindowOpened: + ESP_LOGI(TAG, "Commissioning window opened"); + break; + + case chip::DeviceLayer::DeviceEventType::kCommissioningWindowClosed: + ESP_LOGI(TAG, "Commissioning window closed"); + break; + + case chip::DeviceLayer::DeviceEventType::kFabricRemoved: { + ESP_LOGI(TAG, "Fabric removed successfully"); + if (chip::Server::GetInstance().GetFabricTable().FabricCount() == 0) { + chip::CommissioningWindowManager &commissionMgr = chip::Server::GetInstance().GetCommissioningWindowManager(); + constexpr auto kTimeoutSeconds = chip::System::Clock::Seconds16(k_timeout_seconds); + if (!commissionMgr.IsCommissioningWindowOpen()) { + /* After removing last fabric, this example does not remove the Wi-Fi credentials + * and still has IP connectivity so, only advertising on DNS-SD. + */ + CHIP_ERROR err = commissionMgr.OpenBasicCommissioningWindow(kTimeoutSeconds, + chip::CommissioningWindowAdvertisement::kDnssdOnly); + if (err != CHIP_NO_ERROR) { + ESP_LOGE(TAG, "Failed to open commissioning window, err:%" CHIP_ERROR_FORMAT, err.Format()); + } + } + } + break; + } + + case chip::DeviceLayer::DeviceEventType::kFabricWillBeRemoved: + ESP_LOGI(TAG, "Fabric will be removed"); + break; + + case chip::DeviceLayer::DeviceEventType::kFabricUpdated: + ESP_LOGI(TAG, "Fabric is updated"); + break; + + case chip::DeviceLayer::DeviceEventType::kFabricCommitted: + ESP_LOGI(TAG, "Fabric is committed"); + break; + + case chip::DeviceLayer::DeviceEventType::kBLEDeinitialized: + ESP_LOGI(TAG, "BLE deinitialized and memory reclaimed"); + break; + + default: + break; + } +} + +// This callback is invoked when clients interact with the Identify Cluster. +// In the callback implementation, an endpoint can identify itself. (e.g., by flashing an LED or light). +static esp_err_t app_identification_cb(identification::callback_type_t type, uint16_t endpoint_id, uint8_t effect_id, + uint8_t effect_variant, void *priv_data) +{ + ESP_LOGI(TAG, "Identification callback: type: %u, effect: %u, variant: %u", type, effect_id, effect_variant); + return ESP_OK; +} + +// This callback is called for every attribute update. The callback implementation shall +// handle the desired attributes and return an appropriate error code. If the attribute +// is not of your interest, please do not return an error code and strictly return ESP_OK. +static esp_err_t app_attribute_update_cb(attribute::callback_type_t type, uint16_t endpoint_id, uint32_t cluster_id, + uint32_t attribute_id, esp_matter_attr_val_t *val, void *priv_data) +{ + esp_err_t err = ESP_OK; + + if (type == POST_UPDATE) { + ESP_LOGI(TAG, "Attribute update for Endpoint 0x%04" PRIX16 "'s Cluster 0x%08" PRIX32 "'s Attribute 0x%08" PRIX32, endpoint_id, cluster_id, attribute_id); + } + + return err; +} + +extern "C" void app_main() +{ + esp_err_t err = ESP_OK; + + /* Initialize the ESP NVS layer */ + nvs_flash_init(); + + /* Create a Matter node and add the mandatory Root Node device type on endpoint 0 */ + node::config_t node_config; + + // node handle can be used to add/modify other endpoints. + node_t *node = node::create(&node_config, app_attribute_update_cb, app_identification_cb); + ABORT_APP_ON_FAILURE(node != nullptr, ESP_LOGE(TAG, "Failed to create Matter node")); + + // Add optional attributes for clusters on Root Node (Endpoint 0) + endpoint_t *root_endpoint = endpoint::get(node, 0); + + // Existing clusters on Root Node - just add optional attributes + cluster::basic_information::create_optional_attributes(cluster::get(root_endpoint, BasicInformation::Id)); + cluster::general_diagnostics::create_optional_attributes(cluster::get(root_endpoint, GeneralDiagnostics::Id)); + + // Create new clusters and their optional attributes + // 1. Boolean State Configuration + cluster::boolean_state_configuration::config_t bool_config; + cluster::boolean_state_configuration::create(root_endpoint, &bool_config, CLUSTER_FLAG_SERVER); + cluster::boolean_state_configuration::create_optional_attributes(cluster::get(root_endpoint, BooleanStateConfiguration::Id)); + + // 2. Electrical Energy Measurement + cluster::electrical_energy_measurement::config_t energy_config; + energy_config.feature_flags = cluster::electrical_energy_measurement::feature::imported_energy::get_id() | cluster::electrical_energy_measurement::feature::cumulative_energy::get_id(); + cluster::electrical_energy_measurement::create(root_endpoint, &energy_config, CLUSTER_FLAG_SERVER); + cluster::electrical_energy_measurement::create_optional_attributes(cluster::get(root_endpoint, ElectricalEnergyMeasurement::Id)); + + // 3. Electrical Power Measurement + cluster::electrical_power_measurement::config_t power_config; + power_config.feature_flags = cluster::electrical_power_measurement::feature::direct_current::get_id(); + cluster::electrical_power_measurement::create(root_endpoint, &power_config, CLUSTER_FLAG_SERVER); + cluster::electrical_power_measurement::create_optional_attributes(cluster::get(root_endpoint, ElectricalPowerMeasurement::Id)); + + // 4. Ethernet Network Diagnostics + // Note: Usually only one of Ethernet or WiFi diagnostics should be present, but for test purpose we try adding. + cluster::ethernet_network_diagnostics::create(root_endpoint, nullptr, CLUSTER_FLAG_SERVER); + cluster::ethernet_network_diagnostics::create_optional_attributes(cluster::get(root_endpoint, EthernetNetworkDiagnostics::Id)); + + // 5. Occupancy Sensing + cluster::occupancy_sensing::config_t occupancy_config; + occupancy_config.feature_flags = cluster::occupancy_sensing::feature::other::get_id(); + cluster::occupancy_sensing::create(root_endpoint, &occupancy_config, CLUSTER_FLAG_SERVER); + cluster::occupancy_sensing::create_optional_attributes(cluster::get(root_endpoint, OccupancySensing::Id)); + + // 6. Resource Monitoring + cluster::resource_monitoring::config_t resource_config; // Assuming generic config or specific like HepaFilter + // Resource Monitoring is usually a base for specific clusters like HEPA Filter Monitoring. + // We'll create HepaFilterMonitoring as an example of ResourceMonitoring + cluster::hepa_filter_monitoring::create(root_endpoint, &resource_config, CLUSTER_FLAG_SERVER); + cluster::resource_monitoring::create_optional_attributes(cluster::get(root_endpoint, HepaFilterMonitoring::Id)); + + // 7. Software Diagnostics + cluster::software_diagnostics::config_t sw_diag_config; + cluster::software_diagnostics::create(root_endpoint, &sw_diag_config, CLUSTER_FLAG_SERVER); + cluster::software_diagnostics::create_optional_attributes(cluster::get(root_endpoint, SoftwareDiagnostics::Id)); + + // 8. Time Synchronization + cluster::time_synchronization::config_t time_sync_config; + cluster::time_synchronization::create(root_endpoint, &time_sync_config, CLUSTER_FLAG_SERVER); + cluster::time_synchronization::create_optional_attributes(cluster::get(root_endpoint, TimeSynchronization::Id)); + + // 9. Wifi Network Diagnostics + // Usually created by root_node::create if config enabled, but we ensure it's here and add optionals + if (!cluster::get(root_endpoint, WiFiNetworkDiagnostics::Id)) { + cluster::wifi_network_diagnostics::create(root_endpoint, nullptr, CLUSTER_FLAG_SERVER); + } + cluster::wifi_network_diagnostics::create_optional_attributes(cluster::get(root_endpoint, WiFiNetworkDiagnostics::Id)); + + /* Matter start */ + err = esp_matter::start(app_event_cb); + ABORT_APP_ON_FAILURE(err == ESP_OK, ESP_LOGE(TAG, "Failed to start Matter, err:%d", err)); + +#if CONFIG_ENABLE_CHIP_SHELL + esp_matter::console::diagnostics_register_commands(); + esp_matter::console::wifi_register_commands(); + esp_matter::console::factoryreset_register_commands(); + esp_matter::console::attribute_register_commands(); +#if CONFIG_OPENTHREAD_CLI + esp_matter::console::otcli_register_commands(); +#endif + esp_matter::console::init(); +#endif + +} diff --git a/examples/test_apps/test_optional_attributes/partitions.csv b/examples/test_apps/test_optional_attributes/partitions.csv new file mode 100644 index 000000000..f4638c82c --- /dev/null +++ b/examples/test_apps/test_optional_attributes/partitions.csv @@ -0,0 +1,11 @@ +# Name, Type, SubType, Offset, Size, Flags +# Note: Firmware partition offset needs to be 64K aligned, initial 36K (9 sectors) are reserved for bootloader and partition table +esp_secure_cert, 0x3F, ,0xd000, 0x2000, encrypted +nvs, data, nvs, 0x10000, 0xC000, +nvs_keys, data, nvs_keys,, 0x1000, encrypted +otadata, data, ota, , 0x2000 +phy_init, data, phy, , 0x1000, +ota_0, app, ota_0, 0x20000, 0x1E0000, +ota_1, app, ota_1, 0x200000, 0x1E0000, +fctry, data, nvs, 0x3E0000, 0x6000 +coredump, data, coredump,0x3F0000, 0x10000 diff --git a/examples/test_apps/test_optional_attributes/sdkconfig.defaults b/examples/test_apps/test_optional_attributes/sdkconfig.defaults new file mode 100644 index 000000000..cb3347b9c --- /dev/null +++ b/examples/test_apps/test_optional_attributes/sdkconfig.defaults @@ -0,0 +1,46 @@ +CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y + +#enable BT +CONFIG_BT_ENABLED=y +CONFIG_BT_NIMBLE_ENABLED=y + +#disable BT connection reattempt +CONFIG_BT_NIMBLE_ENABLE_CONN_REATTEMPT=n + +#enable lwip ipv6 autoconfig +CONFIG_LWIP_IPV6_AUTOCONFIG=y + +# Use a custom partition table +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_FILENAME="partitions.csv" +CONFIG_PARTITION_TABLE_OFFSET=0xC000 + +# Enable chip shell +CONFIG_ENABLE_CHIP_SHELL=y + +#enable lwIP route hooks +CONFIG_LWIP_HOOK_IP6_ROUTE_DEFAULT=y +CONFIG_LWIP_HOOK_ND6_GET_GW_DEFAULT=y + +# Button +CONFIG_BUTTON_PERIOD_TIME_MS=20 +CONFIG_BUTTON_LONG_PRESS_TIME_MS=5000 + +# disable softap by default +CONFIG_ESP_WIFI_SOFTAP_SUPPORT=n + +# Remove this after `ENABLE_WIFI_AP` config has been deleted from chip component +CONFIG_ENABLE_WIFI_AP=n + +# Enable OTA Requestor +CONFIG_ENABLE_OTA_REQUESTOR=y + +# Enable optional attributes helper APIs +CONFIG_ESP_MATTER_ENABLE_OPTIONAL_ATTRIBUTES=y + +# Enable HKDF in mbedtls +CONFIG_MBEDTLS_HKDF_C=y + +# Increase LwIP IPv6 address number to 6 (MAX_FABRIC + 1) +# unique local addresses for fabrics(MAX_FABRIC), a link local address(1) +CONFIG_LWIP_IPV6_NUM_ADDRESSES=6 \ No newline at end of file diff --git a/tools/ci/build_apps.py b/tools/ci/build_apps.py index 53ff37a3a..8e8862b82 100644 --- a/tools/ci/build_apps.py +++ b/tools/ci/build_apps.py @@ -22,7 +22,7 @@ APPS_BUILD_PER_JOB = 30 PYTEST_C6_APPS = [ {"target": "esp32c6", "name": "light"}, ] -MAINFEST_FILES = [ +MANIFEST_FILES = [ str(PROJECT_ROOT / 'examples' / '.build-rules.yml'), ] @@ -30,21 +30,22 @@ PYTEST_H2_APPS = [ {"target": "esp32h2", "name": "light"}, {"target": "esp32s3", "name": "thread_border_router"}, ] -MAINFEST_FILES = [ +MANIFEST_FILES = [ str(PROJECT_ROOT / 'examples' / '.build-rules.yml'), ] PYTEST_C3_APPS = [ {"target": "esp32c3", "name": "light"}, + {"target": "esp32c3", "name": "test_optional_attributes"}, ] -MAINFEST_FILES = [ +MANIFEST_FILES = [ str(PROJECT_ROOT / 'examples' / '.build-rules.yml'), ] PYTEST_C2_APPS = [ {"target": "esp32c2", "name": "light"}, ] -MAINFEST_FILES = [ +MANIFEST_FILES = [ str(PROJECT_ROOT / 'examples' / '.build-rules.yml'), ] @@ -81,7 +82,7 @@ NO_PYTEST_REMAINING_APPS = [ {"target": "esp32h2", "name": "refrigerator"}, {"target": "esp32" , "name": "demo/badge"}, ] -MAINFEST_FILES = [ +MANIFEST_FILES = [ str(PROJECT_ROOT / 'examples' / '.build-rules.yml'), ] @@ -132,7 +133,7 @@ def get_cmake_apps( build_log_filename='build_log.txt', size_json_filename='size.json', check_warnings=False, - manifest_files=MAINFEST_FILES, + manifest_files=MANIFEST_FILES, ) return apps @@ -211,7 +212,7 @@ if __name__ == '__main__': parser.add_argument( '--no_pytest', action="store_true", - help='Exclude pytest apps definded in PYTEST_H2_APPS and PYTEST_C6_APPS and some optional no-pytest apps', + help='Exclude pytest apps defined in PYTEST_H2_APPS and PYTEST_C6_APPS and some optional no-pytest apps', ) parser.add_argument( '--no_pytest_remaining', @@ -221,22 +222,22 @@ if __name__ == '__main__': parser.add_argument( '--pytest_c6', action="store_true", - help='Only build pytest apps, definded in PYTEST_C6_APPS', + help='Only build pytest apps, defined in PYTEST_C6_APPS', ) parser.add_argument( '--pytest_h2', action="store_true", - help='Only build pytest apps, definded in PYTEST_H2_APPS', + help='Only build pytest apps, defined in PYTEST_H2_APPS', ) parser.add_argument( '--pytest_c3', action="store_true", - help='Only build pytest apps, definded in PYTEST_C3_APPS', + help='Only build pytest apps, defined in PYTEST_C3_APPS', ) parser.add_argument( '--pytest_c2', action="store_true", - help='Only build pytest apps, definded in PYTEST_C2_APPS', + help='Only build pytest apps, defined in PYTEST_C2_APPS', ) parser.add_argument( '--collect-size-info', diff --git a/tools/test_optional_attributes/README.md b/tools/test_optional_attributes/README.md new file mode 100644 index 000000000..b09e28297 --- /dev/null +++ b/tools/test_optional_attributes/README.md @@ -0,0 +1,68 @@ +# Test Optional Attributes Suite + +This directory contains test scripts to verify the presence and readability of optional attributes in various Matter clusters which are migrated to be code driven. + +## Prerequisites + +1. **Environment**: You need to be in a Matter build environment (e.g., `connectedhomeip` SDK environment) where the python controller and testing libraries are built and available. + ```bash + # Example setup in connectedhomeip + source ./scripts/activate.sh + ``` +2. **Device**: A Matter device must be commissioned and reachable. + +## Usage + +1. **Build the Example App**: + + The example application with all clusters enabled is located at `examples/test_apps/test_optional_attributes`. + + ```bash + cd examples/test_apps/test_optional_attributes + idf.py build + idf.py flash monitor + ``` + +2. **Build Python Environment**: + + The test requires the Matter Python controller and test framework. You can build the environment using the installation script: + + ```bash + ./install.sh --build-python + ``` + + This will create a virtual python environment at `connectedhomeip/connectedhomeip/out/py_env`. + +3. **Run the Test**: + + Activate the Python environment and run the test. Below is an example command for BLE-WiFi commissioning: + + ```bash + source connectedhomeip/connectedhomeip/out/py_env/bin/activate + python3 tools/test_optional_attributes/test_optional_attributes_framework.py \ + -n 1 \ + --commissioning-method ble-wifi \ + --wifi-ssid \ + --wifi-passphrase \ + --passcode 20202021 \ + --discriminator 3840 \ + --paa-trust-store-path connectedhomeip/connectedhomeip/credentials/development/paa-root-certs + ``` + + Note: Adjust passcode/discriminator as per your device configuration (default for `test_optional_attributes` is usually `20202021`/`3840`). + +## Tested Clusters + +The scripts check for optional attributes in: +- Basic Information +- Boolean State Configuration +- Descriptor +- Electrical Energy Measurement +- Electrical Power Measurement +- Ethernet Network Diagnostics +- General Diagnostics +- Occupancy Sensing +- Resource Monitoring +- Software Diagnostics +- Time Synchronization +- WiFi Network Diagnostics diff --git a/tools/test_optional_attributes/test_optional_attributes_framework.py b/tools/test_optional_attributes/test_optional_attributes_framework.py new file mode 100644 index 000000000..af21ff2b3 --- /dev/null +++ b/tools/test_optional_attributes/test_optional_attributes_framework.py @@ -0,0 +1,197 @@ +# Copyright 2026 Espressif Systems (Shanghai) PTE LTD +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +from mobly import asserts +import matter.clusters as Clusters +from matter.testing.decorators import async_test_body +from matter.testing.matter_testing import MatterBaseTest +from matter.testing.runner import default_matter_test_main + +class TestOptionalAttributes(MatterBaseTest): + + # Mapping of Cluster Object to list of Attribute Objects to check + # We use the actual Cluster objects from matter.clusters + OPTIONAL_ATTRIBUTES = { + Clusters.BasicInformation: [ + Clusters.BasicInformation.Attributes.ManufacturingDate, + Clusters.BasicInformation.Attributes.PartNumber, + Clusters.BasicInformation.Attributes.ProductURL, + Clusters.BasicInformation.Attributes.ProductLabel, + Clusters.BasicInformation.Attributes.SerialNumber, + Clusters.BasicInformation.Attributes.LocalConfigDisabled, + Clusters.BasicInformation.Attributes.Reachable, + # TODO:Intentionally skip ProductAppearance for now, it requires to set nvs storage. + # Clusters.BasicInformation.Attributes.ProductAppearance, + ], + Clusters.BooleanStateConfiguration: [ + # TODO:Intentionally skip DefaultSensitivityLevel, AlarmsEnabled, optional features conformance. + # Clusters.BooleanStateConfiguration.Attributes.DefaultSensitivityLevel, + #Clusters.BooleanStateConfiguration.Attributes.AlarmsEnabled, + Clusters.BooleanStateConfiguration.Attributes.SensorFault, + ], + Clusters.Descriptor: [ + # Clusters.Descriptor.Attributes.EndpointUniqueId + # Note: EndpointUniqueId might not be available in all older versions of the controller definitions + # We will check dynamically if possible, or skip if attribute object doesn't exist + ], + Clusters.ElectricalEnergyMeasurement: [ + Clusters.ElectricalEnergyMeasurement.Attributes.CumulativeEnergyReset, + ], + Clusters.ElectricalPowerMeasurement: [ + # TODO:Intentionally skip Ranges for now, requires initialization setup. + # Clusters.ElectricalPowerMeasurement.Attributes.Ranges, + # Clusters.ElectricalPowerMeasurement.Attributes.Voltage, + # TODO:Intentionally skip other attributes for now, has features conformance. + # Clusters.ElectricalPowerMeasurement.Attributes.ActiveCurrent, + # Clusters.ElectricalPowerMeasurement.Attributes.ReactiveCurrent, + # Clusters.ElectricalPowerMeasurement.Attributes.ApparentCurrent, + # Clusters.ElectricalPowerMeasurement.Attributes.ReactivePower, + # Clusters.ElectricalPowerMeasurement.Attributes.ApparentPower, + # Clusters.ElectricalPowerMeasurement.Attributes.RMSVoltage, + # Clusters.ElectricalPowerMeasurement.Attributes.RMSCurrent, + # Clusters.ElectricalPowerMeasurement.Attributes.RMSPower, + # Clusters.ElectricalPowerMeasurement.Attributes.Frequency, + # Clusters.ElectricalPowerMeasurement.Attributes.PowerFactor, + # Clusters.ElectricalPowerMeasurement.Attributes.NeutralCurrent, + ], + Clusters.EthernetNetworkDiagnostics: [ + Clusters.EthernetNetworkDiagnostics.Attributes.PHYRate, + Clusters.EthernetNetworkDiagnostics.Attributes.FullDuplex, + Clusters.EthernetNetworkDiagnostics.Attributes.CarrierDetect, + Clusters.EthernetNetworkDiagnostics.Attributes.TimeSinceReset, + ], + Clusters.GeneralDiagnostics: [ + Clusters.GeneralDiagnostics.Attributes.TotalOperationalHours, + Clusters.GeneralDiagnostics.Attributes.BootReason, + Clusters.GeneralDiagnostics.Attributes.ActiveHardwareFaults, + Clusters.GeneralDiagnostics.Attributes.ActiveRadioFaults, + Clusters.GeneralDiagnostics.Attributes.ActiveNetworkFaults, + ], + Clusters.OccupancySensing: [ + Clusters.OccupancySensing.Attributes.HoldTime, + Clusters.OccupancySensing.Attributes.HoldTimeLimits, + ], + Clusters.HepaFilterMonitoring: [ + Clusters.HepaFilterMonitoring.Attributes.InPlaceIndicator, + Clusters.HepaFilterMonitoring.Attributes.LastChangedTime, + ], + Clusters.SoftwareDiagnostics: [ + Clusters.SoftwareDiagnostics.Attributes.ThreadMetrics, + Clusters.SoftwareDiagnostics.Attributes.CurrentHeapFree, + Clusters.SoftwareDiagnostics.Attributes.CurrentHeapUsed, + ], + Clusters.TimeSynchronization: [ + Clusters.TimeSynchronization.Attributes.TimeSource, + ], + Clusters.WiFiNetworkDiagnostics: [ + Clusters.WiFiNetworkDiagnostics.Attributes.CurrentMaxRate, + ] + } + + # Add EndpointUniqueId dynamically if it exists + if hasattr(Clusters.Descriptor.Attributes, 'EndpointUniqueId'): + OPTIONAL_ATTRIBUTES[Clusters.Descriptor].append(Clusters.Descriptor.Attributes.EndpointUniqueId) + + @async_test_body + async def test_optional_attributes(self): + dev_ctrl = self.default_controller + node_id = self.dut_node_id + # We assume endpoint 1 for most application clusters, or we can probe the endpoint. + # For simplicity in this test, we might iterate endpoints or just default to 1. + # Better: Read the descriptor to find endpoints. + + logging.info(f"Reading from Node ID: {node_id}") + + # 1. Get List of Endpoints + endpoint_list = await self.read_single_attribute_check_success( + cluster=Clusters.Descriptor, + attribute=Clusters.Descriptor.Attributes.PartsList, + dev_ctrl=dev_ctrl, + node_id=node_id, + endpoint=0 + ) + endpoints = [0] + list(endpoint_list) + logging.info(f"Found Endpoints: {endpoints}") + + failures = [] + successes = [] + + for cluster_class, attributes in self.OPTIONAL_ATTRIBUTES.items(): + cluster_name = cluster_class.__name__ + + # Find which endpoint has this cluster + target_endpoint = None + for ep in endpoints: + # Read ServerList + server_list = await self.read_single_attribute_check_success( + cluster=Clusters.Descriptor, + attribute=Clusters.Descriptor.Attributes.ServerList, + dev_ctrl=dev_ctrl, + node_id=node_id, + endpoint=ep + ) + if cluster_class.id in server_list: + target_endpoint = ep + break + + if target_endpoint is None: + logging.warning(f"Cluster {cluster_name} not found on any endpoint. Skipping attributes.") + continue + + logging.info(f"Checking {cluster_name} on Endpoint {target_endpoint}") + + # Read AttributeList to verify presence + # AttributeList is global attribute 0xFFFB + # We can use the generated cluster object for this if available, or just standard read + try: + # Using the standard read to get the list of supported attributes + # We can't always rely on the high-level object for 'AttributeList' if it's not generated + # typically it is in Globals? + # Actually, standard read returns the decoded structure. + # Let's try reading the specific attribute and catch errors. + pass + except Exception as e: + logging.error(f"Failed to prepare check for {cluster_name}: {e}") + continue + + for attribute_def in attributes: + attr_name = attribute_def.__name__ + try: + val = await self.read_single_attribute_check_success( + cluster=cluster_class, + attribute=attribute_def, + dev_ctrl=dev_ctrl, + node_id=node_id, + endpoint=target_endpoint + ) + successes.append(f"{cluster_name}::{attr_name} = {val}") + logging.info(f" [PASS] {attr_name}: {val}") + except Exception as e: + # If it fails, it might be UnsupportedAttribute if not implemented + failures.append(f"{cluster_name}::{attr_name} - {str(e)}") + logging.error(f" [FAIL] {attr_name}: {e}") + + logging.info("-" * 40) + logging.info("Test Results:") + logging.info(f"Passed: {len(successes)}") + logging.info(f"Failed: {len(failures)}") + + if failures: + for f in failures: + logging.error(f" {f}") + asserts.fail("Some optional attributes failed to read.") + +if __name__ == "__main__": + default_matter_test_main()