Fix TimeSnapshot command and add TC-DGGEN-2.4 (#31179)

* Implement TimeSnapshot command properly

* Add TC-DGGEN-2.4

* Restyled by clang-format

* Restyled by isort

* Fix lint

* Try to fix build

* Fix lint some more

* Restyled by gn

* Address review comments

* Restyled by clang-format

* Restyled by gn

---------

Co-authored-by: tennessee.carmelveilleux@gmail.com <tennessee@google.com>
Co-authored-by: Restyled.io <commits@restyled.io>
diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml
index e0f6020..e02b23c 100644
--- a/.github/workflows/tests.yaml
+++ b/.github/workflows/tests.yaml
@@ -28,7 +28,7 @@
 
 env:
     CHIP_NO_LOG_TIMESTAMPS: true
-    
+
 jobs:
     test_suites_linux:
         name: Test Suites - Linux
@@ -477,6 +477,7 @@
                   scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --script "src/python_testing/TC_TIMESYNC_2_12.py" --app out/linux-x64-all-clusters-ipv6only-no-ble-no-wifi-tsan-clang-test/chip-all-clusters-app  --factoryreset --app-args "--discriminator 1234 --KVS kvs1 --trace-to json:out/trace_data/app-{SCRIPT_BASE_NAME}.json" --script-args "--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:out/trace_data/test-{SCRIPT_BASE_NAME}.json --trace-to perfetto:out/trace_data/test-{SCRIPT_BASE_NAME}.perfetto"'
                   scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --script "src/python_testing/TC_TIMESYNC_2_13.py" --app out/linux-x64-all-clusters-ipv6only-no-ble-no-wifi-tsan-clang-test/chip-all-clusters-app  --factoryreset --app-args "--discriminator 1234 --KVS kvs1 --trace-to json:out/trace_data/app-{SCRIPT_BASE_NAME}.json" --script-args "--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:out/trace_data/test-{SCRIPT_BASE_NAME}.json --trace-to perfetto:out/trace_data/test-{SCRIPT_BASE_NAME}.perfetto"'
                   scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --script "src/python_testing/TC_TIMESYNC_3_1.py" --app out/linux-x64-all-clusters-ipv6only-no-ble-no-wifi-tsan-clang-test/chip-all-clusters-app  --factoryreset --app-args "--discriminator 1234 --KVS kvs1 --trace-to json:out/trace_data/app-{SCRIPT_BASE_NAME}.json" --script-args "--storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --trace-to json:out/trace_data/test-{SCRIPT_BASE_NAME}.json --trace-to perfetto:out/trace_data/test-{SCRIPT_BASE_NAME}.perfetto"'
+                  scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --script "src/python_testing/TC_DGGEN_2_4.py" --app out/linux-x64-all-clusters-ipv6only-no-ble-no-wifi-tsan-clang-test/chip-all-clusters-app  --factoryreset --app-args "--discriminator 1234 --KVS kvs1 --trace-to json:out/trace_data/app-{SCRIPT_BASE_NAME}.json" --script-args "--storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --trace-to json:out/trace_data/test-{SCRIPT_BASE_NAME}.json --trace-to perfetto:out/trace_data/test-{SCRIPT_BASE_NAME}.perfetto"'
                   scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --app out/linux-x64-lit-icd-ipv6only-no-ble-no-wifi-tsan-clang-test/lit-icd-app  --factoryreset --app-args "--discriminator 1234 --KVS kvs1 --trace-to json:out/trace_data/app-{SCRIPT_BASE_NAME}.json" --script "src/python_testing/TC_ICDM_2_1.py" --script-args "--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:out/trace_data/test-{SCRIPT_BASE_NAME}.json --trace-to perfetto:out/trace_data/test-{SCRIPT_BASE_NAME}.perfetto"'
                   scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --app out/linux-x64-all-clusters-ipv6only-no-ble-no-wifi-tsan-clang-test/chip-all-clusters-app  --factoryreset --app-args "--discriminator 1234 --KVS kvs1 --trace-to json:out/trace_data/app-{SCRIPT_BASE_NAME}.json" --script "src/python_testing/TC_DA_1_5.py" --script-args "--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:out/trace_data/test-{SCRIPT_BASE_NAME}.json --trace-to perfetto:out/trace_data/test-{SCRIPT_BASE_NAME}.perfetto"'
                   scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --app out/linux-x64-all-clusters-ipv6only-no-ble-no-wifi-tsan-clang-test/chip-all-clusters-app  --factoryreset --app-args "--discriminator 1234 --KVS kvs1 --trace-to json:out/trace_data/app-{SCRIPT_BASE_NAME}.json" --script "src/python_testing/TC_IDM_1_2.py" --script-args "--storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --trace-to json:out/trace_data/test-{SCRIPT_BASE_NAME}.json --trace-to perfetto:out/trace_data/test-{SCRIPT_BASE_NAME}.perfetto"'
diff --git a/src/app/clusters/general-diagnostics-server/general-diagnostics-server.cpp b/src/app/clusters/general-diagnostics-server/general-diagnostics-server.cpp
index 108cc0b..ba71b04 100644
--- a/src/app/clusters/general-diagnostics-server/general-diagnostics-server.cpp
+++ b/src/app/clusters/general-diagnostics-server/general-diagnostics-server.cpp
@@ -16,6 +16,13 @@
  */
 
 #include "general-diagnostics-server.h"
+
+#ifdef ZCL_USING_TIME_SYNCHRONIZATION_CLUSTER_SERVER
+// Need the `nogncheck` because it's inter-cluster dependency and this
+// breaks GN deps checks since that doesn't know how to deal with #ifdef'd includes :(.
+#include "app/clusters/time-synchronization-server/time-synchronization-server.h" // nogncheck
+#endif                                                                            // ZCL_USING_TIME_SYNCHRONIZATION_CLUSTER_SERVER
+
 #include "app/server/Server.h"
 #include <app-common/zap-generated/attributes/Accessors.h>
 #include <app-common/zap-generated/cluster-objects.h>
@@ -192,7 +199,9 @@
         return ReadIfSupported(&DiagnosticDataProvider::GetRebootCount, aEncoder);
     }
     case UpTime::Id: {
-        return ReadIfSupported(&DiagnosticDataProvider::GetUpTime, aEncoder);
+        System::Clock::Seconds64 system_time_seconds =
+            std::chrono::duration_cast<System::Clock::Seconds64>(Server::GetInstance().TimeSinceInit());
+        return aEncoder.Encode(static_cast<uint64_t>(system_time_seconds.count()));
     }
     case TotalOperationalHours::Id: {
         return ReadIfSupported(&DiagnosticDataProvider::GetTotalOperationalHours, aEncoder);
@@ -385,9 +394,54 @@
 bool emberAfGeneralDiagnosticsClusterTimeSnapshotCallback(CommandHandler * commandObj, ConcreteCommandPath const & commandPath,
                                                           Commands::TimeSnapshot::DecodableType const & commandData)
 {
-    // TODO(#30096): Command needs to be implemented.
-    ChipLogError(Zcl, "TimeSnapshot not yet supported!");
-    commandObj->AddStatus(commandPath, Status::InvalidCommand);
+    ChipLogError(Zcl, "Received TimeSnapshot command!");
+
+    Commands::TimeSnapshotResponse::Type response;
+
+    System::Clock::Microseconds64 posix_time_us{ 0 };
+
+#ifdef ZCL_USING_TIME_SYNCHRONIZATION_CLUSTER_SERVER
+    bool time_is_synced = false;
+    using Clusters::TimeSynchronization::GranularityEnum;
+    GranularityEnum granularity = Clusters::TimeSynchronization::TimeSynchronizationServer::Instance().GetGranularity();
+    switch (granularity)
+    {
+    case GranularityEnum::kUnknownEnumValue:
+    case GranularityEnum::kNoTimeGranularity:
+        time_is_synced = false;
+        break;
+    case GranularityEnum::kMinutesGranularity:
+        // Minute granularity is not deemed good enough for TimeSnapshot to report PosixTimeMs, by spec.
+        time_is_synced = false;
+        break;
+    case GranularityEnum::kSecondsGranularity:
+    case GranularityEnum::kMillisecondsGranularity:
+    case GranularityEnum::kMicrosecondsGranularity:
+        time_is_synced = true;
+        break;
+    }
+
+    if (time_is_synced)
+    {
+        CHIP_ERROR posix_time_err = System::SystemClock().GetClock_RealTime(posix_time_us);
+        if (posix_time_err != CHIP_NO_ERROR)
+        {
+            ChipLogError(Zcl, "Failed to get POSIX real time: %" CHIP_ERROR_FORMAT, posix_time_err.Format());
+            posix_time_us = System::Clock::Microseconds64{ 0 };
+        }
+    }
+#endif // ZCL_USING_TIME_SYNCHRONIZATION_CLUSTER_SERVER
+
+    System::Clock::Milliseconds64 system_time_ms =
+        std::chrono::duration_cast<System::Clock::Milliseconds64>(Server::GetInstance().TimeSinceInit());
+
+    response.systemTimeMs = static_cast<uint64_t>(system_time_ms.count());
+    if (posix_time_us.count() != 0)
+    {
+        response.posixTimeMs.SetNonNull(
+            static_cast<uint64_t>(std::chrono::duration_cast<System::Clock::Milliseconds64>(posix_time_us).count()));
+    }
+    commandObj->AddResponse(commandPath, response);
     return true;
 }
 
diff --git a/src/python_testing/TC_DGGEN_2_4.py b/src/python_testing/TC_DGGEN_2_4.py
new file mode 100644
index 0000000..b2f5993
--- /dev/null
+++ b/src/python_testing/TC_DGGEN_2_4.py
@@ -0,0 +1,192 @@
+#
+#    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 asyncio
+import logging
+
+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, default_matter_test_main, matter_epoch_us_from_utc_datetime,
+                                    utc_datetime_from_matter_epoch_us, utc_datetime_from_posix_time_ms)
+from mobly import asserts
+
+logger = logging.getLogger(__name__)
+
+
+class TC_DGGEN_2_4(MatterBaseTest):
+    async def read_diags_attribute_expect_success(self, attribute):
+        cluster = Clusters.Objects.GeneralDiagnostics
+        return await self.read_single_attribute_check_success(endpoint=0, cluster=cluster, attribute=attribute)
+
+    async def read_timesync_attribute_expect_success(self, attribute):
+        cluster = Clusters.Objects.TimeSynchronization
+        return await self.read_single_attribute_check_success(endpoint=0, cluster=cluster, attribute=attribute)
+
+    async def set_time_in_timesync(self, current_time: int):
+        endpoint = 0
+        time_cluster = Clusters.Objects.TimeSynchronization
+        code = 0
+
+        try:
+            await self.send_single_cmd(cmd=time_cluster.Commands.SetUTCTime(UTCTime=current_time, granularity=time_cluster.Enums.GranularityEnum.kSecondsGranularity), endpoint=endpoint)
+        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
+
+        asserts.assert_true(code in [0, 1], "Unexpected error while trying to set the UTCTime")
+
+    async def send_time_snapshot_expect_success(self):
+        endpoint = 0
+        diags_cluster = Clusters.Objects.GeneralDiagnostics
+        code = 0
+
+        try:
+            response = await self.send_single_cmd(cmd=diags_cluster.Commands.TimeSnapshot(), endpoint=endpoint)
+        except InteractionModelError as e:
+            code = e.status
+
+        asserts.assert_equal(code, 0, "Expected success of TimeSnapshot.")
+        return response
+
+    @async_test_body
+    async def test_TC_GEN_2_4(self):
+        self.print_step(1, "Detect Time Synchronization UTCTime attribute presence")
+
+        root_descriptor = await self.default_controller.ReadAttribute(self.dut_node_id, [(0, Clusters.Descriptor)])
+        root_server_list = root_descriptor[0][Clusters.Descriptor][Clusters.Descriptor.Attributes.ServerList]
+        has_timesync = (Clusters.TimeSynchronization.id in root_server_list)
+
+        testvar_TimeSyncSupported = False
+
+        if has_timesync:
+            ts_attributes = await self.read_single_attribute(self.default_controller, node_id=self.dut_node_id, endpoint=0, attribute=Clusters.TimeSynchronization.Attributes.AttributeList)
+            has_utc_time = (Clusters.TimeSynchronization.Attributes.UTCTime.attribute_id in ts_attributes)
+
+            if has_utc_time:
+                testvar_TimeSyncSupported = True
+
+        self.print_step(2, "Read UpTime attribute, save as UpTime1")
+        testvar_UpTime1 = await self.read_diags_attribute_expect_success(Clusters.GeneralDiagnostics.Attributes.UpTime)
+        asserts.assert_greater(testvar_UpTime1, 0, "UpTime1 must be > 0")
+
+        # Step 3 (Time Sync supported)
+        if testvar_TimeSyncSupported:
+            self.print_step(3, "Functional verifications when Time Synchronization is supported")
+
+            self.print_step("3a", "Write current time to DUT")
+            # Get current time in the correct format to set via command.
+            th_utc = matter_epoch_us_from_utc_datetime(desired_datetime=None)
+
+            await self.set_time_in_timesync(th_utc)
+
+            self.print_step("3b", "Read current time from DUT")
+            testvar_UTCTime1 = await self.read_timesync_attribute_expect_success(Clusters.TimeSynchronization.Attributes.UTCTime)
+            asserts.assert_true(testvar_UTCTime1 != NullValue, "UTCTime1 readback must not be null")
+
+            self.print_step("3c", "Wait for 1 second")
+            await asyncio.sleep(1)
+
+            self.print_step("3d", "Send a first TimeSnapshot command and verify")
+            response = await self.send_time_snapshot_expect_success()
+            logging.info(f"Step 3d: {response}")
+
+            # Verify that the DUT sends a TimeSnapshotResponse with the following conditions met:
+            #   - Value of PosixTimeMs field is not null.
+            #   - Value of (SystemTimeMs field / 1000) is greater than or equal to UpTime1
+            #   - PosixTimeMs field converted to a UTC timestamp is greater than or equal to UTCTime1 converted to a UTC timestamp.
+            #
+            # On success of prior verifications:
+            #   - Save the value of the SystemTimeMs field as SystemTimeMs1.
+            #   - Save the value of the PosixTimeMs field as PosixTimeMs1.
+            asserts.assert_true(response.posixTimeMs != NullValue, "PosixTimeMs field of TimeSnapshotResponse must not be null")
+            asserts.assert_greater_equal(response.systemTimeMs // 1000, testvar_UpTime1,
+                                         "System time in milliseconds must be >= UpTime1")
+
+            utc_from_posix = utc_datetime_from_posix_time_ms(posix_time_ms=response.posixTimeMs)
+            utc_from_utctime1 = utc_datetime_from_matter_epoch_us(testvar_UTCTime1)
+
+            asserts.assert_greater_equal(
+                utc_from_posix, utc_from_utctime1, "PosixTimeMs field converted to a UTC timestamp must be >= than UTCTime1 converted to a UTC timestamp")
+
+            testvar_SystemTimeMs1 = response.systemTimeMs
+            testvar_PosixTimeMs1 = response.posixTimeMs
+
+            self.print_step("3e", "Wait for 1 second")
+            await asyncio.sleep(1)
+
+            self.print_step("3f", "Send a second TimeSnapshot command and verify")
+
+            response = await self.send_time_snapshot_expect_success()
+            logging.info(f"Step 3f: {response}")
+
+            # Verify that the DUT sends a TimeSnapshotResponse with the following fields:
+            #   - Value of PosixTimeMs field is not null and greater than PosixTimeMs1.
+            #   - Value of SystemTimeMs field is greater than SystemTimeMs1.
+
+            asserts.assert_true(response.posixTimeMs != NullValue, "PosixTimeMs field of TimeSnapshotResponse must not be null")
+            asserts.assert_greater(response.posixTimeMs, testvar_PosixTimeMs1,
+                                   "POSIX time in milliseconds must be > PosixTimeMs1")
+            asserts.assert_greater(response.systemTimeMs, testvar_SystemTimeMs1,
+                                   "System time in milliseconds must be > SystemTimeMs1")
+
+            self.print_step(4, "Skipped: Functional verifications when Time Synchronization is NOT supported")
+
+        # Step 4 (Time Sync not supported)
+        else:  # if not testvar_TimeSyncSupported:
+            self.print_step(3, "Skipped: Functional verifications when Time Synchronization is supported")
+
+            self.print_step(4, "Functional verifications when Time Synchronization is NOT supported")
+
+            self.print_step("4a", "Send a first TimeSnapshot command and verify")
+            response = await self.send_time_snapshot_expect_success()
+            logging.info(f"Step 4a: {response}")
+
+            # Verify that the DUT sends a TimeSnapshotResponse with the following fields:
+            #   - Value of PosixTimeMs field is null.
+            #   - Value of (SystemTimeMs field / 1000) is greater than UpTime1.
+            #
+            # On success of prior verifications, save the value of SystemTimeMs field as SystemTimeMs1.
+
+            asserts.assert_true(response.posixTimeMs == NullValue, "PosixTimeMs field of TimeSnapshotResponse must be null")
+            asserts.assert_greater_equal(response.systemTimeMs // 1000, testvar_UpTime1,
+                                         "System time in milliseconds must be >= UpTime1")
+
+            testvar_SystemTimeMs1 = response.systemTimeMs
+
+            self.print_step("4b", "Wait for 1 second")
+            await asyncio.sleep(1)
+
+            self.print_step("4c", "Send a second TimeSnapshot command and verify")
+
+            response = await self.send_time_snapshot_expect_success()
+            logging.info(f"Step 4c: {response}")
+
+            # Verify that the DUT sends a TimeSnapshotResponse with the following fields:
+            #   - Value of PosixTimeMs field is null.
+            #   - Value of SystemTimeMs field is greater than SystemTimeMs1.
+            #
+            # On success of prior verifications, save the value of SystemTimeMs field as SystemTimeMs1.
+
+            asserts.assert_true(response.posixTimeMs == NullValue, "PosixTimeMs field of TimeSnapshotResponse must be null")
+            asserts.assert_greater(response.systemTimeMs, testvar_SystemTimeMs1,
+                                   "System time in milliseconds must be > SystemTimeMs1")
+
+
+if __name__ == "__main__":
+    default_matter_test_main()
diff --git a/src/python_testing/TC_TIMESYNC_2_2.py b/src/python_testing/TC_TIMESYNC_2_2.py
index 7e63708..f364c44 100644
--- a/src/python_testing/TC_TIMESYNC_2_2.py
+++ b/src/python_testing/TC_TIMESYNC_2_2.py
@@ -43,9 +43,10 @@
         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()
+
+        code = 0
         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
diff --git a/src/python_testing/matter_testing_support.py b/src/python_testing/matter_testing_support.py
index a0109ea..b0a3450 100644
--- a/src/python_testing/matter_testing_support.py
+++ b/src/python_testing/matter_testing_support.py
@@ -21,7 +21,6 @@
 import inspect
 import json
 import logging
-import math
 import os
 import pathlib
 import queue
@@ -183,8 +182,10 @@
     else:
         return isinstance(received_value, desired_type)
 
+# TODO(#31177): Need to add unit tests for all time conversion methods.
 
-def utc_time_in_matter_epoch(desired_datetime: datetime = None):
+
+def utc_time_in_matter_epoch(desired_datetime: Optional[datetime] = None):
     """ Returns the time in matter epoch in us.
 
         If desired_datetime is None, it will return the current time.
@@ -199,19 +200,36 @@
     return utc_th_us
 
 
+matter_epoch_us_from_utc_datetime = utc_time_in_matter_epoch
+
+
+def utc_datetime_from_matter_epoch_us(matter_epoch_us: int) -> datetime:
+    """Returns the given Matter epoch time as a usable Python datetime in UTC."""
+    delta_from_epoch = timedelta(microseconds=matter_epoch_us)
+    matter_epoch = datetime(2000, 1, 1, 0, 0, 0, 0, timezone.utc)
+
+    return matter_epoch + delta_from_epoch
+
+
+def utc_datetime_from_posix_time_ms(posix_time_ms: int) -> datetime:
+    millis = posix_time_ms % 1000
+    seconds = posix_time_ms // 1000
+    return datetime.fromtimestamp(seconds, timezone.utc) + timedelta(milliseconds=millis)
+
+
 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()
 
     # total seconds includes fractional for microseconds
-    expected = utc + offset.total_seconds()*1000000
+    expected = utc + offset.total_seconds() * 1000000
     delta_us = abs(expected - received)
     delta = timedelta(microseconds=delta_us)
     asserts.assert_less_equal(delta, tolerance, "Received time is out of tolerance")
 
 
 def get_wait_seconds_from_set_time(set_time_matter_us: int, wait_seconds: int):
-    seconds_passed = math.floor((utc_time_in_matter_epoch() - set_time_matter_us)/1000000)
+    seconds_passed = (utc_time_in_matter_epoch() - set_time_matter_us) // 1000000
     return wait_seconds - seconds_passed