From 4152918939e8a5c41682461f3d4c7092efc2e2f7 Mon Sep 17 00:00:00 2001 From: C Freeman Date: Tue, 23 May 2023 13:21:31 -0400 Subject: [PATCH] TC-TIMESYNC-2.<2-8> (#26580) * Add PICS * Add a check-time function to python tests * TC-TIMESYNC-2.2 - initial * TC-TIMESYNC-2.4 * TC-TIMESYNC-2.5 * TC-TIMESYNC-2.6 * TC-TIMESYNC-2.7 * TC-TIMESYNC-2.8 * TC-TIMESYNC-2.9 * Restyled by isort --------- Co-authored-by: Restyled.io --- src/app/tests/suites/certification/PICS.yaml | 93 +++++++++ .../tests/suites/certification/ci-pics-values | 27 +++ src/python_testing/TC_TIMESYNC_2_2.py | 76 +++++++ src/python_testing/TC_TIMESYNC_2_4.py | 145 ++++++++++++++ src/python_testing/TC_TIMESYNC_2_5.py | 169 ++++++++++++++++ src/python_testing/TC_TIMESYNC_2_6.py | 119 +++++++++++ src/python_testing/TC_TIMESYNC_2_7.py | 171 ++++++++++++++++ src/python_testing/TC_TIMESYNC_2_8.py | 185 ++++++++++++++++++ src/python_testing/TC_TIMESYNC_2_9.py | 138 +++++++++++++ src/python_testing/matter_testing_support.py | 12 +- 10 files changed, 1134 insertions(+), 1 deletion(-) create mode 100644 src/python_testing/TC_TIMESYNC_2_2.py create mode 100644 src/python_testing/TC_TIMESYNC_2_4.py create mode 100644 src/python_testing/TC_TIMESYNC_2_5.py create mode 100644 src/python_testing/TC_TIMESYNC_2_6.py create mode 100644 src/python_testing/TC_TIMESYNC_2_7.py create mode 100644 src/python_testing/TC_TIMESYNC_2_8.py create mode 100644 src/python_testing/TC_TIMESYNC_2_9.py diff --git a/src/app/tests/suites/certification/PICS.yaml b/src/app/tests/suites/certification/PICS.yaml index 42e6ecb323af16..6e13ca476beb56 100644 --- a/src/app/tests/suites/certification/PICS.yaml +++ b/src/app/tests/suites/certification/PICS.yaml @@ -7607,3 +7607,96 @@ PICS: - label: "Does the device implement sending the GoToTiltPercentage command ?" id: WNCV.C.C08.Tx + + # Time Synchronization Cluster Test Plan + - label: + "Does the device implement the Time Synchronization cluster as a + server?" + id: TIMESYNC.S + + - label: + "Does the device implement the Time Synchronization cluster as a + client?" + id: TIMESYNC.C + + # + # server / features + # + - label: "Does the DUT(server) support the TimeZone feature?" + id: TIMESYNC.S.F00 + + - label: "Does the DUT(server) support the NTPClient feature?" + id: TIMESYNC.S.F01 + + - label: "Does the DUT(server) support the NTPServer feature?" + id: TIMESYNC.S.F02 + + - label: "Does the DUT(server) support the TimeSyncClient feature?" + id: TIMESYNC.S.F03 + + # + # server / attributes + # + - label: "Does the device implement the UTCTime attribute ?" + id: TIMESYNC.S.A0000 + + - label: "Does the device implement the Granularity attribute ?" + id: TIMESYNC.S.A0001 + + - label: "Does the device implement the TimeSource attribute ?" + id: TIMESYNC.S.A0002 + + - label: "Does the device implement the TrustedTimeSource attribute ?" + id: TIMESYNC.S.A0003 + + - label: "Does the device implement the DefaultNTP attribute ?" + id: TIMESYNC.S.A0004 + + - label: "Does the device implement the TimeZone attribute ?" + id: TIMESYNC.S.A0005 + + - label: "Does the device implement the DSTOffset attribute ?" + id: TIMESYNC.S.A0006 + + - label: "Does the device implement the LocalTime attribute ?" + id: TIMESYNC.S.A0007 + + - label: "Does the device implement the TimeZoneDatabase attribute ?" + id: TIMESYNC.S.A0008 + + - label: "Does the device implement the NTPServerAvailable attribute ?" + id: TIMESYNC.S.A0009 + + - label: "Does the device implement the TimeZoneListMaxSize attribute ?" + id: TIMESYNC.S.A000a + + - label: "Does the device implement the DSTOffsetListMaxSize attribute ?" + id: TIMESYNC.S.A000b + + - label: "Does the device implement the SupportsDNSResolve attribute ?" + id: TIMESYNC.S.A000c + + # + # server / commandsReceived + # + - label: "Does the device implement receiving the SetUTCTime command?" + id: TIMESYNC.S.C00.Rsp + + - label: + "Does the device implement receiving the SetTrustedTimeSource command?" + id: TIMESYNC.S.C01.Rsp + + - label: "Does the device implement receiving the SetTimeZone command?" + id: TIMESYNC.S.C02.Rsp + + - label: "Does the device implement receiving the SetDSTOffset command?" + id: TIMESYNC.S.C04.Rsp + + - label: "Does the device implement receiving the SetDefaultNTP command?" + id: TIMESYNC.S.C05.Rsp + + # + # server / commandsGenerated + # + - label: "Does the DUT(server) support the SetTimeZoneResponse command?" + id: TIMESYNC.S.C03.Tx diff --git a/src/app/tests/suites/certification/ci-pics-values b/src/app/tests/suites/certification/ci-pics-values index 88a6cdcf05e1a1..5b5fc568fb1847 100644 --- a/src/app/tests/suites/certification/ci-pics-values +++ b/src/app/tests/suites/certification/ci-pics-values @@ -2066,3 +2066,30 @@ MCORE.IDM.C.SubscribeRequest.Attribute.DataType_Bool=1 MCORE.IDM.C.SubscribeRequest.MultipleAttributes=1 MCORE.IDM.S.LargeData=1 MCORE.IDM.S.PersistentSubscription=1 + +# Time Synchronization +TIMESYNC.S=1 +TIMESYNC.S.F00=1 +TIMESYNC.S.F01=1 +TIMESYNC.S.F02=0 +TIMESYNC.S.F03=1 +TIMESYNC.S.A0000=1 +TIMESYNC.S.A0001=1 +TIMESYNC.S.A0002=1 +TIMESYNC.S.A0003=1 +TIMESYNC.S.A0004=1 +TIMESYNC.S.A0005=1 +TIMESYNC.S.A0006=1 +TIMESYNC.S.A0007=1 +TIMESYNC.S.A0008=1 +TIMESYNC.S.A0009=0 +TIMESYNC.S.A000a=1 +TIMESYNC.S.A000b=1 +TIMESYNC.S.A000c=1 +TIMESYNC.S.C00.Rsp=1 +TIMESYNC.S.C01.Rsp=1 +TIMESYNC.S.C02.Rsp=1 +TIMESYNC.S.C04.Rsp=1 +TIMESYNC.S.C05.Rsp=1 +TIMESYNC.S.C03.Tx=1 +TIMESYNC.C=0 diff --git a/src/python_testing/TC_TIMESYNC_2_2.py b/src/python_testing/TC_TIMESYNC_2_2.py new file mode 100644 index 00000000000000..0a92f1c47fcf05 --- /dev/null +++ b/src/python_testing/TC_TIMESYNC_2_2.py @@ -0,0 +1,76 @@ +# +# Copyright (c) 2023 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. +# + +from datetime import timedelta + +import chip.clusters as Clusters +from chip.clusters.Types import NullValue +from chip.interaction_model import InteractionModelError +from matter_testing_support import MatterBaseTest, async_test_body, compare_time, default_matter_test_main, utc_time_in_matter_epoch +from mobly import asserts + + +class TC_TIMESYNC_2_2(MatterBaseTest): + async def read_ts_attribute_expect_success(self, endpoint, attribute): + cluster = Clusters.Objects.TimeSynchronization + return await self.read_single_attribute_check_success(endpoint=endpoint, cluster=cluster, attribute=attribute) + + @async_test_body + async def test_TC_TIMESYNC_2_2(self): + + endpoint = self.user_params.get("endpoint", 0) + + time_cluster = Clusters.Objects.TimeSynchronization + + self.print_step(1, "Commissioning, already done") + attributes = Clusters.TimeSynchronization.Attributes + + self.print_step(2, "Read UTCTime attribute") + utc_dut_initial = await self.read_ts_attribute_expect_success(endpoint=endpoint, attribute=attributes.UTCTime) + th_utc = utc_time_in_matter_epoch() + try: + await self.send_single_cmd(cmd=time_cluster.Commands.SetUTCTime(UTCTime=th_utc, granularity=time_cluster.Enums.GranularityEnum.kMillisecondsGranularity), endpoint=endpoint) + code = 0 + except InteractionModelError as e: + # The python layer discards the cluster specific portion of the status IB, so for now we just expect a generic FAILURE error + # see #26521 + code = e.status + pass + + if utc_dut_initial is NullValue: + asserts.assert_equal(code, 0, "Unexpected error returned for null UTCTime") + else: + asserts.assert_true(code in [0, 1], "Unexpected error returned for non-null UTCTime") + + self.print_step(3, "Read Granulatiry attribute") + granularity_dut = await self.read_ts_attribute_expect_success(endpoint=endpoint, attribute=attributes.Granularity) + asserts.assert_less(granularity_dut, time_cluster.Enums.GranularityEnum.kUnknownEnumValue, + "Granularity out of expected range") + asserts.assert_not_equal(granularity_dut, time_cluster.Enums.GranularityEnum.kNoTimeGranularity) + + th_utc = utc_time_in_matter_epoch() + utc_dut = await self.read_ts_attribute_expect_success(endpoint=endpoint, attribute=attributes.UTCTime) + asserts.assert_is_not(utc_dut, NullValue, "Received null value for UTCTime after set") + if granularity_dut == time_cluster.Enums.GranularityEnum.kMinutesGranularity: + tolerance = timedelta(minutes=10) + else: + tolerance = timedelta(minutes=1) + compare_time(received=utc_dut, utc=th_utc, tolerance=tolerance) + + +if __name__ == "__main__": + default_matter_test_main() diff --git a/src/python_testing/TC_TIMESYNC_2_4.py b/src/python_testing/TC_TIMESYNC_2_4.py new file mode 100644 index 00000000000000..69cde75a35f500 --- /dev/null +++ b/src/python_testing/TC_TIMESYNC_2_4.py @@ -0,0 +1,145 @@ +# +# Copyright (c) 2023 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. +# + +import typing +from datetime import timedelta + +import chip.clusters as Clusters +from chip.interaction_model import InteractionModelError, Status +from matter_testing_support import MatterBaseTest, async_test_body, default_matter_test_main, type_matches, utc_time_in_matter_epoch +from mobly import asserts + + +class TC_TIMESYNC_2_4(MatterBaseTest): + async def read_ts_attribute_expect_success(self, attribute): + cluster = Clusters.Objects.TimeSynchronization + return await self.read_single_attribute_check_success(endpoint=self.endpoint, cluster=cluster, attribute=attribute) + + async def send_set_time_zone_cmd(self, tz: typing.List[Clusters.Objects.TimeSynchronization.Structs.TimeZoneStruct]) -> Clusters.Objects.TimeSynchronization.Commands.SetTimeZoneResponse: + ret = await self.send_single_cmd(cmd=Clusters.Objects.TimeSynchronization.Commands.SetTimeZone(timeZone=tz), endpoint=self.endpoint) + asserts.assert_true(type_matches(ret, Clusters.Objects.TimeSynchronization.Commands.SetTimeZoneResponse), + "Unexpected return type for SetTimeZone") + return ret + + async def send_set_time_zone_cmd_expect_error(self, tz: typing.List[Clusters.Objects.TimeSynchronization.Structs.TimeZoneStruct], error: Status) -> None: + try: + await self.send_single_cmd(cmd=Clusters.Objects.TimeSynchronization.Commands.SetTimeZone(timeZone=tz), endpoint=self.endpoint) + asserts.assert_true(False, "Unexpected SetTimeZone command success") + except InteractionModelError as e: + asserts.assert_equal(e.status, error, "Unexpected error returned") + pass + + @async_test_body + async def test_TC_TIMESYNC_2_4(self): + + self.endpoint = self.user_params.get("endpoint", 0) + + time_cluster = Clusters.Objects.TimeSynchronization + tz_struct = time_cluster.Structs.TimeZoneStruct + + self.print_step(0, "Commissioning, already done") + attributes = Clusters.TimeSynchronization.Attributes + + self.print_step(1, "Read TimeZoneDatabase attribute") + tz_database_dut = await self.read_ts_attribute_expect_success(attribute=attributes.TimeZoneDatabase) + + self.print_step(2, "Read TimeZoneListMaxSize attribute") + tz_max_size_dut = await self.read_ts_attribute_expect_success(attribute=attributes.TimeZoneListMaxSize) + + self.print_step(3, "Send SetTimeZone command") + tz = [tz_struct(offset=0, validAt=0)] + ret = await self.send_set_time_zone_cmd(tz=tz) + asserts.assert_true(ret.DSTOffsetRequired, "DSTOffsetRequired not set to true") + + self.print_step(4, "Send SetTimeZone command") + tz = [tz_struct(offset=0, validAt=0, name="")] + ret = await self.send_set_time_zone_cmd(tz=tz) + asserts.assert_true(ret.DSTOffsetRequired, "DSTOffsetRequired not set to true") + + self.print_step(5, "Send SetTimeZone command") + tz = [tz_struct(offset=-43200, validAt=0)] + ret = await self.send_set_time_zone_cmd(tz=tz) + asserts.assert_true(ret.DSTOffsetRequired, "DSTOffsetRequired not set to true") + + self.print_step(6, "Send SetTimeZone command") + tz = [tz_struct(offset=50400, validAt=0)] + ret = await self.send_set_time_zone_cmd(tz=tz) + asserts.assert_true(ret.DSTOffsetRequired, "DSTOffsetRequired not set to true") + + self.print_step(7, "Send SetTimeZone command") + tz = [tz_struct(offset=3600, validAt=0, name="FakeCountry/FakeCity")] + ret = await self.send_set_time_zone_cmd(tz=tz) + asserts.assert_true(ret.DSTOffsetRequired, "DSTOffsetRequired not set to true") + + self.print_step(8, "Send SetTimeZone command") + tz = [tz_struct(offset=3600, validAt=0, name="Europe/Dublin")] + ret = await self.send_set_time_zone_cmd(tz=tz) + if tz_database_dut is time_cluster.Enums.TimeZoneDatabaseEnum.kNone: + asserts.assert_true(ret.DSTOffsetRequired, "DSTOffsetRequired not set to true") + + self.print_step(9, "Send SetTimeZone command") + if tz_max_size_dut == 2: + tz = [tz_struct(offset=3600, validAt=0, name="Europe/Dublin"), + tz_struct(offset=7200, validAt=utc_time_in_matter_epoch() + timedelta(minutes=2).microseconds, name="Europe/Athens")] + ret = await self.send_set_time_zone_cmd(tz=tz) + + self.print_step(10, "Send SetTimeZone command - bad validAt time") + tz = [tz_struct(offset=3600, validAt=0, name="Europe/Dublin")] + await self.send_set_time_zone_cmd_expect_error(tz=tz, error=Status.ConstraintError) + + self.print_step(11, "Send SetTimeZone command - bad second entry") + if tz_max_size_dut == 2: + tz = [tz_struct(offset=3600, validAt=0, name="Europe/Dublin"), tz_struct(offset=0, validAt=0, name="Europe/Athens")] + await self.send_set_time_zone_cmd_expect_error(tz=tz, error=Status.ConstraintError) + + self.print_step(12, "Send SetTimeZone command - bad offset (low)") + tz = [tz_struct(offset=-43201, validAt=0)] + await self.send_set_time_zone_cmd_expect_error(tz=tz, error=Status.ConstraintError) + + self.print_step(13, "Send SetTimeZone command - bad offset (high)") + tz = [tz_struct(offset=50401, validAt=0)] + await self.send_set_time_zone_cmd_expect_error(tz=tz, error=Status.ConstraintError) + + self.print_step(14, "Send SetTimeZone command - too long name") + tz = [tz_struct(offset=50401, validAt=0, name="AVeryLongStringWithSixtyFiveChars/ThisIsSomeExtraPaddingForTheStr")] + await self.send_set_time_zone_cmd_expect_error(tz=tz, error=Status.ConstraintError) + + self.print_step(15, "Send SetTimeZone command - too many entries") + if tz_max_size_dut == 2: + tz = [tz_struct(offset=3600, validAt=0, name="Europe/Dublin"), + tz_struct(offset=7200, validAt=utc_time_in_matter_epoch() + + timedelta(minutes=2).microseconds, name="Europe/Athens"), + tz_struct(offset=10800, validAt=utc_time_in_matter_epoch() + + timedelta(minutes=4).microseconds, name="Europe/Istanbul") + ] + await self.send_set_time_zone_cmd_expect_error(tz=tz, error=Status.ResourceExhausted) + + self.print_step(16, "Send SetTimeZone command - too many entries") + if tz_max_size_dut == 1: + tz = [tz_struct(offset=3600, validAt=0, name="Europe/Dublin"), + tz_struct(offset=7200, validAt=utc_time_in_matter_epoch() + + timedelta(minutes=2).microseconds, name="Europe/Athens") + ] + await self.send_set_time_zone_cmd_expect_error(tz=tz, error=Status.ResourceExhausted) + + self.print_step(17, "Reset time zone") + tz = [tz_struct(offset=0, validAt=0)] + await self.send_set_time_zone_cmd(tz=tz) + + +if __name__ == "__main__": + default_matter_test_main() diff --git a/src/python_testing/TC_TIMESYNC_2_5.py b/src/python_testing/TC_TIMESYNC_2_5.py new file mode 100644 index 00000000000000..4717bed506c296 --- /dev/null +++ b/src/python_testing/TC_TIMESYNC_2_5.py @@ -0,0 +1,169 @@ +# +# Copyright (c) 2023 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. +# + +import typing + +import chip.clusters as Clusters +from chip.clusters.Types import NullValue +from chip.interaction_model import InteractionModelError, Status +from matter_testing_support import MatterBaseTest, async_test_body, default_matter_test_main, utc_time_in_matter_epoch +from mobly import asserts + + +class TC_TIMESYNC_2_5(MatterBaseTest): + async def read_ts_attribute_expect_success(self, attribute): + cluster = Clusters.Objects.TimeSynchronization + return await self.read_single_attribute_check_success(endpoint=self.endpoint, cluster=cluster, attribute=attribute) + + async def send_set_dst_cmd(self, dst: typing.List[Clusters.Objects.TimeSynchronization.Structs.DSTOffsetStruct]) -> None: + await self.send_single_cmd(cmd=Clusters.Objects.TimeSynchronization.Commands.SetDSTOffset(DSTOffset=dst)) + + async def send_set_dst_cmd_expect_error(self, dst: typing.List[Clusters.Objects.TimeSynchronization.Structs.DSTOffsetStruct], error: Status) -> None: + try: + await self.send_single_cmd(cmd=Clusters.Objects.TimeSynchronization.Commands.SetDSTOffset(DSTOffset=dst)) + asserts.assert_true(False, "Unexpected SetTimeZone command success") + except InteractionModelError as e: + asserts.assert_equal(e.status, error, "Unexpected error returned") + pass + + @async_test_body + async def test_TC_TIMESYNC_2_5(self): + + self.endpoint = self.user_params.get("endpoint", 0) + + time_cluster = Clusters.Objects.TimeSynchronization + dst_struct = time_cluster.Structs.DSTOffsetStruct + + self.print_step(0, "Commissioning, already done") + attributes = Clusters.TimeSynchronization.Attributes + dst_offset_attr = attributes.DSTOffset + + self.print_step(1, "Read DSTOffsetListMaxSize attribute") + dst_max_size_dut = await self.read_ts_attribute_expect_success(attribute=attributes.DSTOffsetListMaxSize) + asserts.assert_greater_equal(dst_max_size_dut, 1, "DSTOffsetListMaxSize must be at least 1") + + self.print_step(2, "Send SetDSTOffset command") + dst = [] + await self.send_set_dst_cmd(dst=dst) + + self.print_step(3, "Read DSTOffset attribute") + attr = await self.read_ts_attribute_expect_success(dst_offset_attr) + asserts.assert_false(attr, "DSTOffset not set correctly to empty list") + + self.print_step(4, "Test setting unsorted list - expect error") + if dst_max_size_dut > 1: + th_utc = utc_time_in_matter_epoch() + dst = [dst_struct(offset=3600, validStarting=th_utc, valid_until=th_utc+1.577e+13), + dst_struct(offset=3600, validStarting=0, validUntil=th_utc)] + await self.send_set_dst_cmd_expect_error(dst=dst, error=Status.ConstraintError) + + self.print_step(5, "Read DSTOffset attribute - expect empty") + if dst_max_size_dut > 1: + attr = await self.read_ts_attribute_expect_success(dst_offset_attr) + asserts.assert_false(attr, "DSTOffset not set correctly to empty list") + + self.print_step(6, "Test setting list with invalid second entry - expect error") + if dst_max_size_dut > 1: + th_utc = utc_time_in_matter_epoch() + dst = [dst_struct(offset=3600, validStarting=0, valid_until=th_utc+3e+8), + dst_struct(offset=3600, validStarting=th_utc, validUntil=th_utc+1.577e+13)] + await self.send_set_dst_cmd_expect_error(dst=dst, error=Status.ConstraintError) + + self.print_step(7, "Read DSTOffset attribute - expect empty") + if dst_max_size_dut > 1: + attr = await self.read_ts_attribute_expect_success(dst_offset_attr) + asserts.assert_false(attr, "DSTOffset not set correctly to empty list") + + self.print_step(8, "Test setting list with two null values - expect error") + if dst_max_size_dut > 1: + th_utc = utc_time_in_matter_epoch() + dst = [dst_struct(offset=3600, validStarting=0, valid_until=NullValue), + dst_struct(offset=3600, validStarting=th_utc+3e+8, validUntil=NullValue)] + await self.send_set_dst_cmd_expect_error(dst=dst, error=Status.ConstraintError) + + self.print_step(9, "Read DSTOffset attribute - expect empty") + if dst_max_size_dut > 1: + attr = await self.read_ts_attribute_expect_success(dst_offset_attr) + asserts.assert_false(attr, "DSTOffset not set correctly to empty list") + + self.print_step(10, "Test setting list with null value not at end - expect error") + if dst_max_size_dut > 1: + th_utc = utc_time_in_matter_epoch() + dst = [dst_struct(offset=3600, validStarting=0, valid_until=NullValue), + dst_struct(offset=3600, validStarting=th_utc+3e+8, validUntil=th_utc+1.577e+13)] + await self.send_set_dst_cmd_expect_error(dst=dst, error=Status.ConstraintError) + + self.print_step(11, "Read DSTOffset attribute - expect empty") + if dst_max_size_dut > 1: + attr = await self.read_ts_attribute_expect_success(dst_offset_attr) + asserts.assert_false(attr, "DSTOffset not set correctly to empty list") + + self.print_step(12, "Test setting too many entries") + dst = [] + th_utc = utc_time_in_matter_epoch() + for i in range(dst_max_size_dut+1): + year = 3.156e+13 + six_months = 1.577e+13 + vstart = year*i + six_months + vuntil = year * (i+1) + dst.append(dst_struct(offset=3600, validStarting=vstart, validUntil=vuntil)) + await self.send_set_dst_cmd_expect_error(dst=dst, error=Status.ResourceExhausted) + + self.print_step(13, "Read DSTOffset attribute - expect empty") + attr = await self.read_ts_attribute_expect_success(dst_offset_attr) + asserts.assert_false(attr, "DSTOffset not set correctly to empty list") + + self.print_step(14, "Set valid list with null ValidUntil") + th_utc = utc_time_in_matter_epoch() + dst = [dst_struct(offset=3600, validStarting=th_utc+3e+8, validUntil=NullValue)] + await self.send_set_dst_cmd(dst=dst) + + self.print_step(15, "Read back list") + dut_dst = await self.read_ts_attribute_expect_success(dst_offset_attr) + asserts.assert_equal(dut_dst, dst) + + self.print_step(16, "Set valid list with non-null ValidUntil") + th_utc = utc_time_in_matter_epoch() + dst = [dst_struct(offset=3600, validStarting=th_utc+3e+8, validUntil=th_utc+1.577e+13)] + await self.send_set_dst_cmd(dst=dst) + + self.print_step(17, "Read back list") + dut_dst = await self.read_ts_attribute_expect_success(dst_offset_attr) + asserts.assert_equal(dut_dst, dst) + + self.print_step(18, "Test setting max entries") + dst = [] + th_utc = utc_time_in_matter_epoch() + for i in range(dst_max_size_dut): + year = 3.156e+13 + six_months = 1.577e+13 + vstart = year*i + six_months + vuntil = year * (i+1) + dst.append(dst_struct(offset=3600, validStarting=vstart, validUntil=vuntil)) + await self.send_set_dst_cmd(dst=dst) + + self.print_step(19, "Read back list") + dut_dst = await self.read_ts_attribute_expect_success(dst_offset_attr) + asserts.assert_equal(dut_dst, dst) + + self.print_step(20, "Set empty list") + dst = [] + await self.send_set_dst_cmd(dst=dst) + + +if __name__ == "__main__": + default_matter_test_main() diff --git a/src/python_testing/TC_TIMESYNC_2_6.py b/src/python_testing/TC_TIMESYNC_2_6.py new file mode 100644 index 00000000000000..b88b3ff34aaa4a --- /dev/null +++ b/src/python_testing/TC_TIMESYNC_2_6.py @@ -0,0 +1,119 @@ +# +# Copyright (c) 2023 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. +# + +import typing + +import chip.clusters as Clusters +from chip.clusters.Types import Nullable, NullValue +from chip.interaction_model import InteractionModelError, Status +from matter_testing_support import MatterBaseTest, async_test_body, default_matter_test_main +from mobly import asserts + + +class TC_TIMESYNC_2_6(MatterBaseTest): + async def read_ts_attribute_expect_success(self, attribute): + cluster = Clusters.Objects.TimeSynchronization + return await self.read_single_attribute_check_success(endpoint=self.endpoint, cluster=cluster, attribute=attribute) + + async def send_set_default_ntp_cmd(self, ntp: typing.Union[Nullable, str]) -> None: + await self.send_single_cmd(cmd=Clusters.Objects.TimeSynchronization.Commands.SetDefaultNTP(defaultNTP=ntp)) + + async def send_set_default_ntp_cmd_expect_error(self, ntp: typing.Union[Nullable, str], error: Status) -> None: + try: + await self.send_single_cmd(cmd=Clusters.Objects.TimeSynchronization.Commands.SetDefaultNTP(defaultNTP=ntp)) + asserts.assert_true(False, "Unexpected SetTimeZone command success") + except InteractionModelError as e: + asserts.assert_equal(e.status, error, "Unexpected error returned") + pass + + @async_test_body + async def test_TC_TIMESYNC_2_6(self): + + self.endpoint = self.user_params.get("endpoint", 0) + + self.print_step(0, "Commissioning, already done") + attributes = Clusters.TimeSynchronization.Attributes + default_ntp_attr = attributes.DefaultNTP + + self.print_step(1, "Read SupportsDNSResolve attribute") + dns_resolve_supported = await self.read_ts_attribute_expect_success(attributes.SupportsDNSResolve) + + self.print_step(2, "Clear DefaultNTP") + await self.send_set_default_ntp_cmd(ntp=NullValue) + + self.print_step(3, "Read DefaultNTP") + attr = await self.read_ts_attribute_expect_success(default_ntp_attr) + asserts.assert_equal(attr, NullValue, "DefaultNTP not cleared correctly") + + self.print_step(4, "Set DefaultNTP to resolvable host name") + ntp = "time.nist.gov" + if dns_resolve_supported: + await self.send_set_default_ntp_cmd(ntp=ntp) + else: + await self.send_set_default_ntp_cmd_expect_error(ntp=ntp, error=Status.InvalidCommand) + + self.print_step(5, "Read DefaultNTP") + attr = await self.read_ts_attribute_expect_success(default_ntp_attr) + expected = ntp if dns_resolve_supported else NullValue + asserts.assert_equal(attr, expected, "Unexpected DefaultNTP value") + + self.print_step(6, "Clear DefaultNTP") + await self.send_set_default_ntp_cmd(ntp=NullValue) + + self.print_step(7, "Read DefaultNTP") + attr = await self.read_ts_attribute_expect_success(default_ntp_attr) + asserts.assert_equal(attr, NullValue, "DefaultNTP not cleared correctly") + + self.print_step(8, "Set DefaultNTP to RandomString") + await self.send_set_default_ntp_cmd_expect_error(ntp="RandomString", error=Status.InvalidCommand) + + self.print_step(9, "Read DefaultNTP") + attr = await self.read_ts_attribute_expect_success(default_ntp_attr) + asserts.assert_equal(attr, NullValue, "DefaultNTP incorrectly set") + + self.print_step(10, "Set DefaultNTP to malformed IPv6") + await self.send_set_default_ntp_cmd_expect_error(ntp="fe80:1", error=Status.InvalidCommand) + + self.print_step(11, "Read DefaultNTP") + attr = await self.read_ts_attribute_expect_success(default_ntp_attr) + asserts.assert_equal(attr, NullValue, "DefaultNTP incorrectly set") + + self.print_step(12, "Set DefaultNTP to malformed IPv6") + await self.send_set_default_ntp_cmd_expect_error(ntp="fz80::1", error=Status.InvalidCommand) + + self.print_step(13, "Read DefaultNTP") + attr = await self.read_ts_attribute_expect_success(default_ntp_attr) + asserts.assert_equal(attr, NullValue, "DefaultNTP incorrectly set") + + self.print_step(14, "Set DefaultNTP to good IPv6") + ntp = "fe80::1" + await self.send_set_default_ntp(ntp=ntp) + + self.print_step(15, "Read DefaultNTP") + attr = await self.read_ts_attribute_expect_success(default_ntp_attr) + asserts.assert_equal(attr, ntp, "DefaultNTP mismatch") + + self.print_step(16, "Clear DefaultNTP") + await self.send_set_default_ntp_cmd(ntp=NullValue) + + self.print_step(17, "Read DefaultNTP") + attr = await self.read_ts_attribute_expect_success(default_ntp_attr) + asserts.assert_equal(attr, NullValue, "DefaultNTP not cleared correctly") + + +if __name__ == "__main__": + default_matter_test_main() diff --git a/src/python_testing/TC_TIMESYNC_2_7.py b/src/python_testing/TC_TIMESYNC_2_7.py new file mode 100644 index 00000000000000..418b2586f5484d --- /dev/null +++ b/src/python_testing/TC_TIMESYNC_2_7.py @@ -0,0 +1,171 @@ +# +# Copyright (c) 2023 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. +# + +import time +import typing +from datetime import timedelta + +import chip.clusters as Clusters +from chip.clusters.Types import NullValue +from chip.interaction_model import InteractionModelError +from chip.tlv import uint +from matter_testing_support import (MatterBaseTest, async_test_body, compare_time, default_matter_test_main, type_matches, + utc_time_in_matter_epoch) +from mobly import asserts + + +class TC_TIMESYNC_2_7(MatterBaseTest): + + async def read_ts_attribute_expect_success(self, attribute): + cluster = Clusters.Objects.TimeSynchronization + return await self.read_single_attribute_check_success(endpoint=self.endpoint, cluster=cluster, attribute=attribute) + + async def send_set_time_zone_cmd(self, tz: typing.List[Clusters.Objects.TimeSynchronization.Structs.TimeZoneStruct]) -> Clusters.Objects.TimeSynchronization.Commands.SetTimeZoneResponse: + ret = await self.send_single_cmd(cmd=Clusters.Objects.TimeSynchronization.Commands.SetTimeZone(timeZone=tz), endpoint=self.endpoint) + asserts.assert_true(type_matches(ret, Clusters.Objects.TimeSynchronization.Commands.SetTimeZoneResponse), + "Unexpected return type for SetTimeZone") + return ret + + async def send_set_dst_cmd(self, dst: typing.List[Clusters.Objects.TimeSynchronization.Structs.DSTOffsetStruct]) -> None: + await self.send_single_cmd(cmd=Clusters.Objects.TimeSynchronization.Commands.SetDSTOffset(DSTOffset=dst)) + + async def send_set_utc_cmd(self, utc: uint) -> None: + await self.send_single_cmd(cmd=Clusters.Objects.TimeSynchronization.Commands.SetUTCTime(UTCTime=utc, granularity=Clusters.Objects.TimeSynchronization.Enums.GranularityEnum.kMillisecondsGranularity)) + + @async_test_body + async def test_TC_TIMESYNC_2_7(self): + + self.endpoint = self.user_params.get("endpoint", 0) + + self.print_step(0, "Commissioning, already done") + time_cluster = Clusters.Objects.TimeSynchronization + attributes = time_cluster.Attributes + tz_struct = time_cluster.Structs.TimeZoneStruct + dst_struct = time_cluster.Structs.DSTOffsetStruct + utc_attr = attributes.UTCTime + local_attr = attributes.LocalTime + + self.print_step(1, "Send SetTimeZone command with 0 offset") + tz = [tz_struct(offset=0, validAt=0)] + ret = await self.send_set_time_zone_cmd(tz) + asserts.assert_true(ret.DSTOffsetRequired, "DSTOffsetRequired not set to true") + + self.print_step(2, "Send SetDSTOffset command") + dst = [dst_struct(offset=0, validStarting=0, validUntil=NullValue)] + await self.send_set_dst_cmd(dst) + + self.print_step(3, "Send SetUTCCommand") + # It doesn't actually matter if this succeeds. The DUT is free to reject this command and use its own time. + # If the DUT fails to get the time completely, all other tests will fail. + try: + await self.send_set_utc_cmd(utc_time_in_matter_epoch()) + except InteractionModelError: + pass + + self.print_step(4, "Read UTCTime") + utc = await self.read_ts_attribute_expect_success(utc_attr) + compare_time(received=utc, offset=timedelta(), tolerance=timedelta(seconds=5)) + + self.print_step(5, "Read LocalTime") + local = await self.read_ts_attribute_expect_success(local_attr) + compare_time(received=local, offset=timedelta(), tolerance=timedelta(seconds=5)) + + self.print_step(6, "Send SetTimeZone command with 0 offset") + tz = [tz_struct(offset=3600, validAt=0)] + ret = await self.send_set_time_zone_cmd(tz) + asserts.assert_true(ret.DSTOffsetRequired, "DSTOffsetRequired not set to true") + + self.print_step(7, "Send SetDSTOffset command") + dst = [dst_struct(offset=0, validStarting=0, validUntil=NullValue)] + await self.send_set_dst_cmd(dst) + + self.print_step(8, "Read UTCTime") + utc = await self.read_ts_attribute_expect_success(utc_attr) + compare_time(received=utc, offset=timedelta(), tolerance=timedelta(seconds=5)) + + self.print_step(9, "Read LocalTime") + local = await self.read_ts_attribute_expect_success(local_attr) + compare_time(received=local, offset=timedelta(seconds=3600), tolerance=timedelta(seconds=5)) + + self.print_step(10, "Send SetTimeZone") + th_utc = utc_time_in_matter_epoch() + tz = [tz_struct(offset=7200, validAt=th_utc+1e+7)] + ret = await self.send_set_time_zone_cmd(tz) + asserts.assert_true(ret.DSTOffsetRequired, "DSTOffsetRequired not set to true") + + self.print_step(11, "Send SetDSTOffset command") + dst = [dst_struct(offset=0, validStarting=0, validUntil=NullValue)] + await self.send_set_dst_cmd(dst) + + self.print_step(12, "Read LocalTime") + local = await self.read_ts_attribute_expect_success(local_attr) + compare_time(received=local, offset=timedelta(), tolerance=timedelta(seconds=5)) + + self.print_step(13, "Wait 15s then Read LocalTime") + time.sleep(15) + local = await self.read_ts_attribute_expect_success(local_attr) + compare_time(received=local, offset=timedelta(seconds=7200), tolerance=timedelta(seconds=5)) + + self.print_step(14, "Read TZ list size") + tz_list_size = await self.read_ts_attribute_expect_success(attributes.TimeZoneListMaxSize) + + self.print_step(15, "Set time zone with two items") + if tz_list_size > 1: + th_utc = utc_time_in_matter_epoch() + tz = [tz_struct(offset=3600, validAt=0), tz_struct(offset=7200, validAt=th_utc+1e+7)] + + self.print_step(16, "Send SetDSTOffset command") + if tz_list_size > 1: + dst = [dst_struct(offset=0, validStarting=0, validUntil=NullValue)] + await self.send_set_dst_cmd(dst) + + self.print_step(17, "Read LocalTime") + if tz_list_size > 1: + local = await self.read_ts_attribute_expect_success(local_attr) + compare_time(received=local, offset=timedelta(seconds=3600), tolerance=timedelta(seconds=5)) + + self.print_step(18, "Wait 15s and read LocalTime") + if tz_list_size > 1: + time.sleep(15) + local = await self.read_ts_attribute_expect_success(local_attr) + compare_time(received=local, offset=timedelta(seconds=7200), tolerance=timedelta(seconds=5)) + + self.print_step(19, "Send SetTimeZone with negative offset") + tz = [tz_struct(offset=-3600, validAt=0)] + ret = await self.send_set_time_zone_cmd(tz) + asserts.assert_true(ret.DSTOffsetRequired, "DSTOffsetRequired not set to true") + + self.print_step(20, "Send SetDSTOffset command") + dst = [dst_struct(offset=0, validStarting=0, validUntil=NullValue)] + await self.send_set_dst_cmd(dst) + + self.print_step(21, "Read LocalTime") + local = await self.read_ts_attribute_expect_success(local_attr) + compare_time(received=local, offset=timedelta(seconds=-3600), tolerance=timedelta(seconds=5)) + + self.print_step(22, "Send SetTimeZone with 0 offset") + tz = [tz_struct(offset=0, validAt=0)] + ret = await self.send_set_time_zone_cmd(tz) + asserts.assert_true(ret.DSTOffsetRequired, "DSTOffsetRequired not set to true") + + self.print_step(23, "Send SetDSTOffset command") + dst = [dst_struct(offset=0, validStarting=0, validUntil=NullValue)] + await self.send_set_dst_cmd(dst) + + +if __name__ == "__main__": + default_matter_test_main() diff --git a/src/python_testing/TC_TIMESYNC_2_8.py b/src/python_testing/TC_TIMESYNC_2_8.py new file mode 100644 index 00000000000000..589cc61b381752 --- /dev/null +++ b/src/python_testing/TC_TIMESYNC_2_8.py @@ -0,0 +1,185 @@ +# +# Copyright (c) 2023 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. +# + +import time +import typing +from datetime import timedelta + +import chip.clusters as Clusters +from chip.clusters.Types import NullValue +from chip.interaction_model import InteractionModelError +from chip.tlv import uint +from matter_testing_support import (MatterBaseTest, async_test_body, compare_time, default_matter_test_main, type_matches, + utc_time_in_matter_epoch) +from mobly import asserts + + +class TC_TIMESYNC_2_8(MatterBaseTest): + + async def read_ts_attribute_expect_success(self, attribute): + cluster = Clusters.Objects.TimeSynchronization + return await self.read_single_attribute_check_success(endpoint=self.endpoint, cluster=cluster, attribute=attribute) + + async def send_set_time_zone_cmd(self, tz: typing.List[Clusters.Objects.TimeSynchronization.Structs.TimeZoneStruct]) -> Clusters.Objects.TimeSynchronization.Commands.SetTimeZoneResponse: + ret = await self.send_single_cmd(cmd=Clusters.Objects.TimeSynchronization.Commands.SetTimeZone(timeZone=tz), endpoint=self.endpoint) + asserts.assert_true(type_matches(ret, Clusters.Objects.TimeSynchronization.Commands.SetTimeZoneResponse), + "Unexpected return type for SetTimeZone") + return ret + + async def send_set_dst_cmd(self, dst: typing.List[Clusters.Objects.TimeSynchronization.Structs.DSTOffsetStruct]) -> None: + await self.send_single_cmd(cmd=Clusters.Objects.TimeSynchronization.Commands.SetDSTOffset(DSTOffset=dst)) + + async def send_set_utc_cmd(self, utc: uint) -> None: + await self.send_single_cmd(cmd=Clusters.Objects.TimeSynchronization.Commands.SetUTCTime(UTCTime=utc, granularity=Clusters.Objects.TimeSynchronization.Enums.GranularityEnum.kMillisecondsGranularity)) + + @async_test_body + async def test_TC_TIMESYNC_2_8(self): + + self.endpoint = self.user_params.get("endpoint", 0) + + self.print_step(0, "Commissioning, already done") + time_cluster = Clusters.Objects.TimeSynchronization + attributes = time_cluster.Attributes + tz_struct = time_cluster.Structs.TimeZoneStruct + dst_struct = time_cluster.Structs.DSTOffsetStruct + utc_attr = attributes.UTCTime + local_attr = attributes.LocalTime + + self.print_step(1, "Send SetTimeZone command with 0 offset") + tz = [tz_struct(offset=0, validAt=0)] + ret = await self.send_set_time_zone_cmd(tz) + asserts.assert_true(ret.DSTOffsetRequired, "DSTOffsetRequired not set to true") + + self.print_step(2, "Send SetDSTOffset command") + dst = [dst_struct(offset=0, validStarting=0, validUntil=NullValue)] + await self.send_set_dst_cmd(dst) + + self.print_step(3, "Send SetUTCCommand") + # It doesn't actually matter if this succeeds. The DUT is free to reject this command and use its own time. + # If the DUT fails to get the time completely, all other tests will fail. + try: + await self.send_set_utc_cmd(utc_time_in_matter_epoch()) + except InteractionModelError: + pass + + self.print_step(4, "Read UTCTime") + utc = await self.read_ts_attribute_expect_success(utc_attr) + compare_time(received=utc, offset=timedelta(), tolerance=timedelta(seconds=5)) + + self.print_step(5, "Read LocalTime") + local = await self.read_ts_attribute_expect_success(local_attr) + compare_time(received=local, offset=timedelta(), tolerance=timedelta(seconds=5)) + + self.print_step(6, "Send SetDSTOffset command") + th_utc = utc_time_in_matter_epoch() + dst = [dst_struct(offset=3600, validStarting=0, validUntil=th_utc+1e+7)] + await self.send_set_dst_cmd(dst) + + self.print_step(7, "Read UTCTime") + utc = await self.read_ts_attribute_expect_success(utc_attr) + compare_time(received=utc, offset=timedelta(), tolerance=timedelta(seconds=5)) + + self.print_step(8, "Read LocalTime") + local = await self.read_ts_attribute_expect_success(local_attr) + compare_time(received=local, offset=timedelta(seconds=3600), tolerance=timedelta(seconds=5)) + + self.print_step(9, "Wait 15s") + time.sleep(15) + + self.print_step(10, "Read UTCTime") + utc = await self.read_ts_attribute_expect_success(utc_attr) + compare_time(received=utc, offset=timedelta(), tolerance=timedelta(seconds=5)) + + self.print_step(11, "Read LocalTime") + local = await self.read_ts_attribute_expect_success(local_attr) + compare_time(received=local, offset=timedelta(), tolerance=timedelta(seconds=5)) + + self.print_step(12, "Send SetDSTOffset command") + th_utc = utc_time_in_matter_epoch() + dst = [dst_struct(offset=3600, validStarting=0, validUntil=NullValue)] + await self.send_set_dst_cmd(dst) + + self.print_step(13, "Read LocalTime") + local = await self.read_ts_attribute_expect_success(local_attr) + compare_time(received=local, offset=timedelta(seconds=3600), tolerance=timedelta(seconds=5)) + + self.print_step(14, "Wait 15s") + time.sleep(15) + + self.print_step(15, "Read LocalTime") + local = await self.read_ts_attribute_expect_success(local_attr) + compare_time(received=local, offset=timedelta(seconds=3600), tolerance=timedelta(seconds=5)) + + self.print_step(16, "Read DSTOffsetListMaxSize") + dst_list_size = await self.read_ts_attribute_expect_success(attributes.DSTOffsetListMaxSize) + + self.print_step(17, "Send multiple DST offsets") + if dst_list_size > 1: + th_utc = utc_time_in_matter_epoch() + dst = [dst_struct(offset=3600, validStarting=0, validUntil=th_utc+1e+7), + dst_struct(offset=7200, validStarting=th_utc+2.5e+7, validUntil=th_utc+4e+7)] + await self.send_set_dst_cmd(dst) + + self.print_step(18, "Read LocalTime") + if dst_list_size > 1: + local = await self.read_ts_attribute_expect_success(local_attr) + compare_time(received=local, offset=timedelta(seconds=3600), tolerance=timedelta(seconds=5)) + + self.print_step(19, "Wait 15s") + if dst_list_size > 1: + time.sleep(15) + + self.print_step(20, "Read LocalTime") + if dst_list_size > 1: + local = await self.read_ts_attribute_expect_success(local_attr) + compare_time(received=local, offset=timedelta(), tolerance=timedelta(seconds=5)) + + self.print_step(21, "Wait 15s") + if dst_list_size > 1: + time.sleep(15) + + self.print_step(22, "Read LocalTime") + if dst_list_size > 1: + local = await self.read_ts_attribute_expect_success(local_attr) + compare_time(received=local, offset=timedelta(seconds=7200), tolerance=timedelta(seconds=5)) + + self.print_step(23, "Wait 15s") + if dst_list_size > 1: + time.sleep(15) + + self.print_step(24, "Read LocalTime") + if dst_list_size > 1: + local = await self.read_ts_attribute_expect_success(local_attr) + compare_time(received=local, offset=timedelta(), tolerance=timedelta(seconds=5)) + + self.print_step(25, "Send SetDSTOffset command") + th_utc = utc_time_in_matter_epoch() + dst = [dst_struct(offset=3600, validStarting=0, validUntil=NullValue)] + await self.send_set_dst_cmd(dst) + + self.print_step(26, "Read LocalTime") + local = await self.read_ts_attribute_expect_success(local_attr) + compare_time(received=local, offset=timedelta(seconds=-3600), tolerance=timedelta(seconds=5)) + + self.print_step(27, "Send SetDSTOffset command") + th_utc = utc_time_in_matter_epoch() + dst = [dst_struct(offset=0, validStarting=0, validUntil=NullValue)] + await self.send_set_dst_cmd(dst) + + +if __name__ == "__main__": + default_matter_test_main() diff --git a/src/python_testing/TC_TIMESYNC_2_9.py b/src/python_testing/TC_TIMESYNC_2_9.py new file mode 100644 index 00000000000000..77857c195540f0 --- /dev/null +++ b/src/python_testing/TC_TIMESYNC_2_9.py @@ -0,0 +1,138 @@ +# +# Copyright (c) 2023 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. +# + +import typing +from datetime import timedelta + +import chip.clusters as Clusters +from chip.clusters.Types import NullValue +from chip.interaction_model import InteractionModelError +from chip.tlv import uint +from matter_testing_support import (MatterBaseTest, async_test_body, compare_time, default_matter_test_main, type_matches, + utc_time_in_matter_epoch) +from mobly import asserts + + +class TC_TIMESYNC_2_9(MatterBaseTest): + + async def read_ts_attribute_expect_success(self, attribute): + cluster = Clusters.Objects.TimeSynchronization + return await self.read_single_attribute_check_success(endpoint=self.endpoint, cluster=cluster, attribute=attribute) + + async def send_set_time_zone_cmd(self, tz: typing.List[Clusters.Objects.TimeSynchronization.Structs.TimeZoneStruct]) -> Clusters.Objects.TimeSynchronization.Commands.SetTimeZoneResponse: + ret = await self.send_single_cmd(cmd=Clusters.Objects.TimeSynchronization.Commands.SetTimeZone(timeZone=tz), endpoint=self.endpoint) + asserts.assert_true(type_matches(ret, Clusters.Objects.TimeSynchronization.Commands.SetTimeZoneResponse), + "Unexpected return type for SetTimeZone") + return ret + + async def send_set_dst_cmd(self, dst: typing.List[Clusters.Objects.TimeSynchronization.Structs.DSTOffsetStruct]) -> None: + await self.send_single_cmd(cmd=Clusters.Objects.TimeSynchronization.Commands.SetDSTOffset(DSTOffset=dst)) + + async def send_set_utc_cmd(self, utc: uint) -> None: + await self.send_single_cmd(cmd=Clusters.Objects.TimeSynchronization.Commands.SetUTCTime(UTCTime=utc, granularity=Clusters.Objects.TimeSynchronization.Enums.GranularityEnum.kMillisecondsGranularity)) + + @async_test_body + async def test_TC_TIMESYNC_2_9(self): + + self.endpoint = self.user_params.get("endpoint", 0) + + self.print_step(0, "Commissioning, already done") + time_cluster = Clusters.Objects.TimeSynchronization + attributes = time_cluster.Attributes + tz_struct = time_cluster.Structs.TimeZoneStruct + dst_struct = time_cluster.Structs.DSTOffsetStruct + utc_attr = attributes.UTCTime + local_attr = attributes.LocalTime + + self.print_step(1, "Send SetUTCCommand") + # It doesn't actually matter if this succeeds. The DUT is free to reject this command and use its own time. + # If the DUT fails to get the time completely, all other tests will fail. + try: + await self.send_set_utc_cmd(utc_time_in_matter_epoch()) + except InteractionModelError: + pass + + self.print_step(2, "Send SetTimeZone command") + tz = [tz_struct(offset=7200, validAt=0)] + ret = await self.send_set_time_zone_cmd(tz) + asserts.assert_true(ret.DSTOffsetRequired, "DSTOffsetRequired not set to true") + + self.print_step(3, "Send SetDSTOffset command") + dst = [dst_struct(offset=3600, validStarting=0, validUntil=NullValue)] + await self.send_set_dst_cmd(dst) + + self.print_step(4, "Read UTCTime") + utc = await self.read_ts_attribute_expect_success(utc_attr) + compare_time(received=utc, offset=timedelta(), tolerance=timedelta(seconds=5)) + + self.print_step(5, "Read LocalTime") + local = await self.read_ts_attribute_expect_success(local_attr) + compare_time(received=local, offset=timedelta(seconds=7200+3600), tolerance=timedelta(seconds=5)) + + self.print_step(6, "Send SetDSTOffset command") + dst = [dst_struct(offset=-3600, validStarting=0, validUntil=NullValue)] + await self.send_set_dst_cmd(dst) + + self.print_step(7, "Read UTCTime") + utc = await self.read_ts_attribute_expect_success(utc_attr) + compare_time(received=utc, offset=timedelta(), tolerance=timedelta(seconds=5)) + + self.print_step(8, "Read LocalTime") + local = await self.read_ts_attribute_expect_success(local_attr) + compare_time(received=local, offset=timedelta(seconds=7200-3600), tolerance=timedelta(seconds=5)) + + self.print_step(9, "Send SetTimeZone command") + tz = [tz_struct(offset=-7200, validAt=0)] + ret = await self.send_set_time_zone_cmd(tz) + asserts.assert_true(ret.DSTOffsetRequired, "DSTOffsetRequired not set to true") + + self.print_step(10, "Send SetDSTOffset command") + dst = [dst_struct(offset=3600, validStarting=0, validUntil=NullValue)] + await self.send_set_dst_cmd(dst) + + self.print_step(11, "Read UTCTime") + utc = await self.read_ts_attribute_expect_success(utc_attr) + compare_time(received=utc, offset=timedelta(), tolerance=timedelta(seconds=5)) + + self.print_step(12, "Read LocalTime") + local = await self.read_ts_attribute_expect_success(local_attr) + compare_time(received=local, offset=timedelta(seconds=-7200+3600), tolerance=timedelta(seconds=5)) + + self.print_step(13, "Send SetDSTOffset command") + dst = [dst_struct(offset=-3600, validStarting=0, validUntil=NullValue)] + await self.send_set_dst_cmd(dst) + + self.print_step(14, "Read UTCTime") + utc = await self.read_ts_attribute_expect_success(utc_attr) + compare_time(received=utc, offset=timedelta(), tolerance=timedelta(seconds=5)) + + self.print_step(15, "Read LocalTime") + local = await self.read_ts_attribute_expect_success(local_attr) + compare_time(received=local, offset=timedelta(seconds=-7200-3600), tolerance=timedelta(seconds=5)) + + self.print_step(16, "Send SetTimeZone command") + tz = [tz_struct(offset=0, validAt=0)] + ret = await self.send_set_time_zone_cmd(tz) + asserts.assert_true(ret.DSTOffsetRequired, "DSTOffsetRequired not set to true") + + self.print_step(17, "Send SetDSTOffset command") + dst = [dst_struct(offset=0, validStarting=0, validUntil=NullValue)] + await self.send_set_dst_cmd(dst) + + +if __name__ == "__main__": + default_matter_test_main() diff --git a/src/python_testing/matter_testing_support.py b/src/python_testing/matter_testing_support.py index 1e2e40a6b4ab3d..366c71f3ad12c0 100644 --- a/src/python_testing/matter_testing_support.py +++ b/src/python_testing/matter_testing_support.py @@ -29,7 +29,7 @@ from binascii import hexlify, unhexlify from dataclasses import asdict as dataclass_asdict from dataclasses import dataclass, field -from datetime import datetime, timezone +from datetime import datetime, timedelta, timezone from typing import List, Optional, Tuple from chip.tlv import float32, uint @@ -183,6 +183,16 @@ def utc_time_in_matter_epoch(desired_datetime: datetime = None): return utc_th_us +def compare_time(received: int, offset: timedelta = timedelta(), utc: int = None, tolerance: timedelta = timedelta(seconds=5)) -> None: + if utc is None: + utc = utc_time_in_matter_epoch() + + expected = utc + offset.microseconds + delta_us = abs(expected - received) + delta = timedelta(microseconds=delta_us) + asserts.assert_less(delta, tolerance, "Received time is out of tolerance") + + @dataclass class MatterTestConfig: storage_path: pathlib.Path = None