diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 8284b5a9abd284..5e38c6c341bea6 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -578,6 +578,7 @@ jobs: scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --load-from-env /tmp/test_env.yaml --script src/python_testing/TC_OPSTATE_2_3.py' scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --load-from-env /tmp/test_env.yaml --script src/python_testing/TC_OPSTATE_2_4.py' scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --load-from-env /tmp/test_env.yaml --script src/python_testing/TC_OPSTATE_2_5.py' + scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --load-from-env /tmp/test_env.yaml --script src/python_testing/TC_OPSTATE_2_6.py' scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --load-from-env /tmp/test_env.yaml --script src/python_testing/TC_OVENOPSTATE_2_1.py' scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --load-from-env /tmp/test_env.yaml --script src/python_testing/TC_OVENOPSTATE_2_2.py' scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --load-from-env /tmp/test_env.yaml --script src/python_testing/TC_OVENOPSTATE_2_3.py' diff --git a/examples/all-clusters-app/all-clusters-common/include/operational-state-delegate-impl.h b/examples/all-clusters-app/all-clusters-common/include/operational-state-delegate-impl.h index 60b6b09e9b6511..badadd68cd30a9 100644 --- a/examples/all-clusters-app/all-clusters-common/include/operational-state-delegate-impl.h +++ b/examples/all-clusters-app/all-clusters-common/include/operational-state-delegate-impl.h @@ -138,6 +138,7 @@ class OperationalStateDelegate : public GenericOperationalStateDelegateImpl }; Instance * GetOperationalStateInstance(); +OperationalStateDelegate * GetOperationalStateDelegate(); void Shutdown(); diff --git a/examples/all-clusters-app/all-clusters-common/src/operational-state-delegate-impl.cpp b/examples/all-clusters-app/all-clusters-common/src/operational-state-delegate-impl.cpp index d258b8261a1aed..d4a91cf259a2a7 100644 --- a/examples/all-clusters-app/all-clusters-common/src/operational-state-delegate-impl.cpp +++ b/examples/all-clusters-app/all-clusters-common/src/operational-state-delegate-impl.cpp @@ -59,6 +59,7 @@ void GenericOperationalStateDelegateImpl::HandlePauseStateCallback(GenericOperat auto error = GetInstance()->SetOperationalState(to_underlying(OperationalState::OperationalStateEnum::kPaused)); if (error == CHIP_NO_ERROR) { + GetInstance()->UpdateCountdownTimeFromDelegate(); err.Set(to_underlying(ErrorStateEnum::kNoError)); } else @@ -73,6 +74,7 @@ void GenericOperationalStateDelegateImpl::HandleResumeStateCallback(GenericOpera auto error = GetInstance()->SetOperationalState(to_underlying(OperationalStateEnum::kRunning)); if (error == CHIP_NO_ERROR) { + GetInstance()->UpdateCountdownTimeFromDelegate(); err.Set(to_underlying(ErrorStateEnum::kNoError)); } else @@ -96,6 +98,7 @@ void GenericOperationalStateDelegateImpl::HandleStartStateCallback(GenericOperat auto error = GetInstance()->SetOperationalState(to_underlying(OperationalStateEnum::kRunning)); if (error == CHIP_NO_ERROR) { + GetInstance()->UpdateCountdownTimeFromDelegate(); (void) DeviceLayer::SystemLayer().StartTimer(System::Clock::Seconds16(1), onOperationalStateTimerTick, this); err.Set(to_underlying(ErrorStateEnum::kNoError)); } @@ -113,6 +116,8 @@ void GenericOperationalStateDelegateImpl::HandleStopStateCallback(GenericOperati { (void) DeviceLayer::SystemLayer().CancelTimer(onOperationalStateTimerTick, this); + GetInstance()->UpdateCountdownTimeFromDelegate(); + OperationalState::GenericOperationalError current_err(to_underlying(OperationalState::ErrorStateEnum::kNoError)); GetInstance()->GetCurrentOperationalError(current_err); @@ -152,6 +157,11 @@ static void onOperationalStateTimerTick(System::Layer * systemLayer, void * data delegate->mPausedTime++; } } + else if (!countdown_time.IsNull() && countdown_time.Value() <= 0) + { + OperationalState::GenericOperationalError noError(to_underlying(OperationalState::ErrorStateEnum::kNoError)); + delegate->HandleStopStateCallback(noError); + } if (state == OperationalState::OperationalStateEnum::kRunning || state == OperationalState::OperationalStateEnum::kPaused) { @@ -173,6 +183,11 @@ OperationalState::Instance * OperationalState::GetOperationalStateInstance() return gOperationalStateInstance; } +OperationalStateDelegate * OperationalState::GetOperationalStateDelegate() +{ + return gOperationalStateDelegate; +} + void OperationalState::Shutdown() { if (gOperationalStateInstance != nullptr) diff --git a/examples/all-clusters-app/linux/AllClustersCommandDelegate.cpp b/examples/all-clusters-app/linux/AllClustersCommandDelegate.cpp index d620731e93dc5c..6c1467fc62a145 100644 --- a/examples/all-clusters-app/linux/AllClustersCommandDelegate.cpp +++ b/examples/all-clusters-app/linux/AllClustersCommandDelegate.cpp @@ -667,21 +667,65 @@ void AllClustersAppCommandHandler::OnModeChangeHandler(std::string device, std:: void AllClustersAppCommandHandler::OnOperationalStateChange(std::string device, std::string operation, Json::Value param) { - OperationalState::Instance * operationalStateInstance = nullptr; if (device == "Generic") { - operationalStateInstance = OperationalState::GetOperationalStateInstance(); + OnGenericOperationalStateChange(device, operation, param); } else if (device == "Oven") { - operationalStateInstance = OvenCavityOperationalState::GetOperationalStateInstance(); + OnOvenOperationalStateChange(device, operation, param); } else { ChipLogDetail(NotSpecified, "Invalid device type : %s", device.c_str()); return; } +} + +void AllClustersAppCommandHandler::OnGenericOperationalStateChange(std::string device, std::string operation, Json::Value param) +{ + OperationalState::Instance * operationalStateInstance = OperationalState::GetOperationalStateInstance(); + OperationalState::OperationalStateDelegate * operationalStateDelegate = OperationalState::GetOperationalStateDelegate(); + OperationalState::GenericOperationalError noError(to_underlying(OperationalState::ErrorStateEnum::kNoError)); + OperationalState::OperationalStateEnum state = + static_cast(operationalStateInstance->GetCurrentOperationalState()); + if (operation == "Start") + { + operationalStateDelegate->HandleStartStateCallback(noError); + } + else if (operation == "Resume") + { + operationalStateDelegate->HandleResumeStateCallback(noError); + } + else if (operation == "Pause") + { + operationalStateDelegate->HandlePauseStateCallback(noError); + } + else if (operation == "Stop" && state == OperationalState::OperationalStateEnum::kRunning) + { + operationalStateDelegate->HandleStopStateCallback(noError); + } + else if (operation == "OnFault") + { + uint8_t event_id = to_underlying(OperationalState::ErrorStateEnum::kUnableToCompleteOperation); + if (!param.isNull()) + { + event_id = to_underlying(static_cast(param.asUInt())); + } + OperationalState::GenericOperationalError err(event_id); + operationalStateInstance->OnOperationalErrorDetected(err); + } + else + { + ChipLogDetail(NotSpecified, "Invalid operation : %s", operation.c_str()); + return; + } +} + +void AllClustersAppCommandHandler::OnOvenOperationalStateChange(std::string device, std::string operation, Json::Value param) +{ + OperationalState::Instance * operationalStateInstance = OvenCavityOperationalState::GetOperationalStateInstance(); if (operation == "Start" || operation == "Resume") { operationalStateInstance->SetOperationalState(to_underlying(OperationalState::OperationalStateEnum::kRunning)); diff --git a/examples/all-clusters-app/linux/AllClustersCommandDelegate.h b/examples/all-clusters-app/linux/AllClustersCommandDelegate.h index f097c539b54fb6..f1b873fc0d69c4 100644 --- a/examples/all-clusters-app/linux/AllClustersCommandDelegate.h +++ b/examples/all-clusters-app/linux/AllClustersCommandDelegate.h @@ -105,6 +105,16 @@ class AllClustersAppCommandHandler * Should be called when it is necessary to change the operational state as a manual operation. */ void OnOperationalStateChange(std::string device, std::string operation, Json::Value param); + + /** + * Should be called when it is necessary to change the operational state as a manual operation. + */ + void OnGenericOperationalStateChange(std::string device, std::string operation, Json::Value param); + + /** + * Should be called when it is necessary to change the operational state as a manual operation. + */ + void OnOvenOperationalStateChange(std::string device, std::string operation, Json::Value param); }; class AllClustersCommandDelegate : public NamedPipeCommandDelegate diff --git a/src/python_testing/TC_OPSTATE_2_6.py b/src/python_testing/TC_OPSTATE_2_6.py new file mode 100644 index 00000000000000..8560452b327712 --- /dev/null +++ b/src/python_testing/TC_OPSTATE_2_6.py @@ -0,0 +1,62 @@ +# +# Copyright (c) 2024 Project CHIP Authors +# All rights reserved. +# +# 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. +# + +# See https://github.com/project-chip/connectedhomeip/blob/master/docs/testing/python.md#defining-the-ci-test-arguments +# for details about the block below. +# +# === BEGIN CI TEST ARGUMENTS === +# test-runner-runs: run1 +# test-runner-run/run1/app: ${ALL_CLUSTERS_APP} +# test-runner-run/run1/factoryreset: True +# test-runner-run/run1/quiet: True +# test-runner-run/run1/app-args: --discriminator 1234 --KVS kvs1 --trace-to json:${TRACE_APP}.json +# test-runner-run/run1/script-args: --endpoint 1 --int-arg PIXIT.WAITTIME.REBOOT:5 --storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --PICS src/app/tests/suites/certification/ci-pics-values --trace-to json:${TRACE_TEST_JSON}.json --trace-to perfetto:${TRACE_TEST_PERFETTO}.perfetto +# === END CI TEST ARGUMENTS === + + +import chip.clusters as Clusters +from matter_testing_support import MatterBaseTest, TestStep, async_test_body, default_matter_test_main +from TC_OpstateCommon import TC_OPSTATE_BASE, TestInfo + + +class TC_OPSTATE_2_6(MatterBaseTest, TC_OPSTATE_BASE): + def __init__(self, *args): + super().__init__(*args) + + test_info = TestInfo( + pics_code="OPSTATE", + cluster=Clusters.OperationalState + ) + + super().setup_base(test_info=test_info) + + def steps_TC_OPSTATE_2_6(self) -> list[TestStep]: + return self.steps_TC_OPSTATE_BASE_2_6() + + def pics_TC_OPSTATE_2_6(self) -> list[str]: + return ["OPSTATE.S", "OPSTATE.S.A0002"] + + @async_test_body + async def test_TC_OPSTATE_2_6(self): + # endpoint = self.matter_test_config.endpoint + + # await self.TEST_TC_OPSTATE_BASE_2_6(endpoint=endpoint) + await self.TEST_TC_OPSTATE_BASE_2_6(endpoint=1) + + +if __name__ == "__main__": + default_matter_test_main() diff --git a/src/python_testing/TC_OpstateCommon.py b/src/python_testing/TC_OpstateCommon.py index 09dc73e3344c1f..ece50ac90c65b6 100644 --- a/src/python_testing/TC_OpstateCommon.py +++ b/src/python_testing/TC_OpstateCommon.py @@ -28,7 +28,7 @@ from chip.clusters.Attribute import EventReadResult, SubscriptionTransaction from chip.clusters.Types import NullValue from chip.interaction_model import InteractionModelError, Status -from matter_testing_support import EventChangeCallback, TestStep +from matter_testing_support import ClusterAttributeChangeAccumulator, EventChangeCallback, TestStep from mobly import asserts @@ -1082,6 +1082,7 @@ async def TEST_TC_OPSTATE_BASE_2_5(self, endpoint=1): # STEP 7: TH waits for initial-countdown-time self.step(7) + logging.info(f'Sleeping for {initial_countdown_time:.1f} seconds.') time.sleep(initial_countdown_time) # STEP 8: TH sends Stop command to the DUT @@ -1221,3 +1222,112 @@ async def TEST_TC_OPSTATE_BASE_2_5(self, endpoint=1): self.skip_step(20) self.skip_step(21) self.skip_step(22) + + ############################ + # TEST CASE 2.6 - Optional Reports with DUT as Server + ############################ + def steps_TC_OPSTATE_BASE_2_6(self) -> list[TestStep]: + steps = [TestStep(1, "Commissioning, already done", is_commissioning=True), + TestStep(2, "Subscribe to CountdownTime attribute"), + TestStep(3, "Manually put the DUT into a state where it will use the CountdownTime attribute, " + "the initial value of the CountdownTime is greater than 30, " + "and it will begin counting down the CountdownTime attribute."), + TestStep(4, "Over a period of 30 seconds, TH counts all report transactions with an attribute " + "report for the CountdownTime attribute in numberOfReportsReceived"), + TestStep(5, "Until the current operation finishes, TH counts all report transactions with " + "an attribute report for the CountdownTime attribute in numberOfReportsReceived and saves up to 5 such reports."), + TestStep(6, "Manually put the DUT into a state where it will use the CountdownTime attribute, " + "the initial value of the CountdownTime is greater than 30, and it will begin counting down the CountdownTime attribute."), + TestStep(7, "TH reads from the DUT the OperationalState attribute"), + TestStep(8, "Manually put the device in the Paused(0x02) operational state") + ] + return steps + + async def TEST_TC_OPSTATE_BASE_2_6(self, endpoint=1): + cluster = self.test_info.cluster + attributes = cluster.Attributes + + self.init_test() + + # commission + self.step(1) + + # Note that this does a subscribe-all instead of subscribing only to the CountdownTime attribute. + # To-Do: Update the TP to subscribe-all. + self.step(2) + sub_handler = ClusterAttributeChangeAccumulator(cluster) + await sub_handler.start(self.default_controller, self.dut_node_id, endpoint) + + self.step(3) + if self.pics_guard(self.check_pics(f"{self.test_info.pics_code}.S.M.ST_RUNNING")): + self.send_manual_or_pipe_command(name="OperationalStateChange", + device=self.device, + operation="Start") + time.sleep(1) + await self.read_and_expect_value(endpoint=endpoint, + attribute=attributes.OperationalState, + expected_value=cluster.Enums.OperationalStateEnum.kRunning) + count = sub_handler.attribute_report_counts[attributes.CountdownTime] + asserts.assert_greater(count, 0, "Did not receive any reports for CountdownTime") + else: + self.skip_step(3) + + sub_handler.reset() + self.step(4) + logging.info('Test will now collect data for 30 seconds') + time.sleep(30) + + count = sub_handler.attribute_report_counts[attributes.CountdownTime] + sub_handler.reset() + asserts.assert_less_equal(count, 5, "Received more than 5 reports for CountdownTime") + asserts.assert_greater_equal(count, 0, "Did not receive any reports for CountdownTime") + + attr_value = await self.read_expect_success( + endpoint=endpoint, + attribute=attributes.OperationalState) + if attr_value == cluster.Enums.OperationalStateEnum.kRunning: + self.step(5) + wait_count = 0 + while (attr_value != cluster.Enums.OperationalStateEnum.kStopped) and (wait_count < 20): + time.sleep(1) + wait_count = wait_count + 1 + attr_value = await self.read_expect_success( + endpoint=endpoint, + attribute=attributes.OperationalState) + count = sub_handler.attribute_report_counts[attributes.CountdownTime] + asserts.assert_less_equal(count, 5, "Received more than 5 reports for CountdownTime") + asserts.assert_greater(count, 0, "Did not receive any reports for CountdownTime") + else: + self.skip_step(5) + + sub_handler.reset() + self.step(6) + if self.pics_guard(self.check_pics(f"{self.test_info.pics_code}.S.M.ST_RUNNING")): + self.send_manual_or_pipe_command(name="OperationalStateChange", + device=self.device, + operation="Start") + time.sleep(1) + await self.read_and_expect_value(endpoint=endpoint, + attribute=attributes.OperationalState, + expected_value=cluster.Enums.OperationalStateEnum.kRunning) + count = sub_handler.attribute_report_counts[attributes.CountdownTime] + asserts.assert_greater(count, 0, "Did not receive any reports for CountdownTime") + else: + self.skip_step(6) + + self.step(7) + await self.read_and_expect_value(endpoint=endpoint, + attribute=attributes.OperationalState, + expected_value=cluster.Enums.OperationalStateEnum.kRunning) + + sub_handler.reset() + self.step(8) + if self.pics_guard(self.check_pics(f"{self.test_info.pics_code}.S.M.ST_PAUSED")): + self.send_manual_or_pipe_command(name="OperationalStateChange", + device=self.device, + operation="Pause") + time.sleep(1) + count = sub_handler.attribute_report_counts[attributes.CountdownTime] + asserts.assert_greater(count, 0, "Did not receive any reports for CountdownTime") + else: + self.skip_step(8)