[ICD] Update ICDM 2.1 Certification test to match test plan (#33404)

* WIP

* Update icdm 2.1 cert test

* Update ci PICS

* Fix ICDM test script

* Added unit tests for ICDM 2.1 test script

* Fix python linter

* fix ci

* move staticmethods inside the class

* Fix ci for missing function

* Fix bit count function
diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml
index 0c0290f..58bdd23 100644
--- a/.github/workflows/tests.yaml
+++ b/.github/workflows/tests.yaml
@@ -560,6 +560,7 @@
                   scripts/run_in_python_env.sh out/venv './scripts/tests/run_python_test.py --app out/linux-x64-rvc-ipv6only-no-ble-no-wifi-tsan-clang-test/chip-rvc-app  --factoryreset --app-args "--discriminator 1234 --KVS kvs1 --trace_file json:out/trace_data/app-{SCRIPT_BASE_NAME}.json" --script "src/python_testing/TC_RVCOPSTATE_2_3.py" --script-args "--storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --PICS examples/rvc-app/rvc-common/pics/rvc-app-pics-values --endpoint 1 --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-rvc-ipv6only-no-ble-no-wifi-tsan-clang-test/chip-rvc-app  --factoryreset --app-args "--discriminator 1234 --KVS kvs1 --trace_file json:out/trace_data/app-{SCRIPT_BASE_NAME}.json" --script "src/python_testing/TC_RVCOPSTATE_2_4.py" --script-args "--storage-path admin_storage.json --commissioning-method on-network --discriminator 1234 --passcode 20202021 --PICS examples/rvc-app/rvc-common/pics/rvc-app-pics-values --endpoint 1 --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 './src/python_testing/test_testing/test_TC_DA_1_2.py'
+                  scripts/run_in_python_env.sh out/venv './src/python_testing/test_testing/test_TC_ICDM_2_1.py'
             - name: Uploading core files
               uses: actions/upload-artifact@v4
               if: ${{ failure() && !env.ACT }}
diff --git a/examples/lit-icd-app/lit-icd-common/lit-icd-server-app.matter b/examples/lit-icd-app/lit-icd-common/lit-icd-server-app.matter
index cf3d071..5274f24 100644
--- a/examples/lit-icd-app/lit-icd-common/lit-icd-server-app.matter
+++ b/examples/lit-icd-app/lit-icd-common/lit-icd-server-app.matter
@@ -1783,7 +1783,7 @@
     callback attribute registeredClients;
     callback attribute ICDCounter;
     callback attribute clientsSupportedPerFabric;
-    ram      attribute userActiveModeTriggerHint default = 0x110D;
+    ram      attribute userActiveModeTriggerHint default = 0x111D;
     ram      attribute userActiveModeTriggerInstruction default = "Restart the application";
     ram      attribute operatingMode default = 0;
     callback attribute generatedCommandList;
diff --git a/examples/lit-icd-app/lit-icd-common/lit-icd-server-app.zap b/examples/lit-icd-app/lit-icd-common/lit-icd-server-app.zap
index 7780ccc..6bbfc90 100644
--- a/examples/lit-icd-app/lit-icd-common/lit-icd-server-app.zap
+++ b/examples/lit-icd-app/lit-icd-common/lit-icd-server-app.zap
@@ -1,6 +1,6 @@
 {
   "fileFormat": 2,
-  "featureLevel": 100,
+  "featureLevel": 102,
   "creator": "zap",
   "keyValuePairs": [
     {
@@ -29,6 +29,7 @@
       "pathRelativity": "relativeToZap",
       "path": "../../../src/app/zap-templates/app-templates.json",
       "type": "gen-templates-json",
+      "category": "matter",
       "version": "chip-v1"
     }
   ],
@@ -3497,7 +3498,7 @@
               "storageOption": "RAM",
               "singleton": 0,
               "bounded": 0,
-              "defaultValue": "0x110D",
+              "defaultValue": "0x111D",
               "reportable": 1,
               "minInterval": 1,
               "maxInterval": 65534,
@@ -3699,7 +3700,7 @@
               "singleton": 0,
               "bounded": 0,
               "defaultValue": "0x0",
-              "reportable": 0,
+              "reportable": 1,
               "minInterval": 1,
               "maxInterval": 65534,
               "reportableChange": 0
@@ -3715,7 +3716,7 @@
               "singleton": 0,
               "bounded": 0,
               "defaultValue": "0x00",
-              "reportable": 0,
+              "reportable": 1,
               "minInterval": 1,
               "maxInterval": 65534,
               "reportableChange": 0
@@ -3730,7 +3731,7 @@
               "storageOption": "External",
               "singleton": 0,
               "bounded": 0,
-              "defaultValue": "",
+              "defaultValue": null,
               "reportable": 1,
               "minInterval": 1,
               "maxInterval": 65534,
@@ -3746,7 +3747,7 @@
               "storageOption": "External",
               "singleton": 0,
               "bounded": 0,
-              "defaultValue": "",
+              "defaultValue": null,
               "reportable": 1,
               "minInterval": 1,
               "maxInterval": 65534,
@@ -3762,7 +3763,7 @@
               "storageOption": "External",
               "singleton": 0,
               "bounded": 0,
-              "defaultValue": "",
+              "defaultValue": null,
               "reportable": 1,
               "minInterval": 1,
               "maxInterval": 65534,
@@ -3778,7 +3779,7 @@
               "storageOption": "External",
               "singleton": 0,
               "bounded": 0,
-              "defaultValue": "",
+              "defaultValue": null,
               "reportable": 1,
               "minInterval": 1,
               "maxInterval": 65534,
@@ -3836,7 +3837,7 @@
               "storageOption": "External",
               "singleton": 0,
               "bounded": 0,
-              "defaultValue": "",
+              "defaultValue": null,
               "reportable": 1,
               "minInterval": 1,
               "maxInterval": 65534,
@@ -3852,8 +3853,8 @@
               "storageOption": "External",
               "singleton": 0,
               "bounded": 0,
-              "defaultValue": "",
-              "reportable": 0,
+              "defaultValue": null,
+              "reportable": 1,
               "minInterval": 1,
               "maxInterval": 65534,
               "reportableChange": 0
@@ -3868,8 +3869,8 @@
               "storageOption": "External",
               "singleton": 0,
               "bounded": 0,
-              "defaultValue": "",
-              "reportable": 0,
+              "defaultValue": null,
+              "reportable": 1,
               "minInterval": 1,
               "maxInterval": 65534,
               "reportableChange": 0
@@ -3884,8 +3885,8 @@
               "storageOption": "External",
               "singleton": 0,
               "bounded": 0,
-              "defaultValue": "",
-              "reportable": 0,
+              "defaultValue": null,
+              "reportable": 1,
               "minInterval": 1,
               "maxInterval": 65534,
               "reportableChange": 0
@@ -3900,7 +3901,7 @@
               "storageOption": "External",
               "singleton": 0,
               "bounded": 0,
-              "defaultValue": "",
+              "defaultValue": null,
               "reportable": 1,
               "minInterval": 1,
               "maxInterval": 65534,
@@ -3916,7 +3917,7 @@
               "storageOption": "External",
               "singleton": 0,
               "bounded": 0,
-              "defaultValue": "",
+              "defaultValue": null,
               "reportable": 1,
               "minInterval": 1,
               "maxInterval": 65534,
@@ -3932,7 +3933,7 @@
               "storageOption": "External",
               "singleton": 0,
               "bounded": 0,
-              "defaultValue": "",
+              "defaultValue": null,
               "reportable": 1,
               "minInterval": 1,
               "maxInterval": 65534,
@@ -3948,7 +3949,7 @@
               "storageOption": "External",
               "singleton": 0,
               "bounded": 0,
-              "defaultValue": "",
+              "defaultValue": null,
               "reportable": 1,
               "minInterval": 1,
               "maxInterval": 65534,
@@ -3964,7 +3965,7 @@
               "storageOption": "External",
               "singleton": 0,
               "bounded": 0,
-              "defaultValue": "0",
+              "defaultValue": null,
               "reportable": 1,
               "minInterval": 1,
               "maxInterval": 65534,
@@ -3980,7 +3981,7 @@
               "storageOption": "External",
               "singleton": 0,
               "bounded": 0,
-              "defaultValue": "2",
+              "defaultValue": null,
               "reportable": 1,
               "minInterval": 1,
               "maxInterval": 65534,
@@ -4022,7 +4023,7 @@
               "storageOption": "External",
               "singleton": 0,
               "bounded": 0,
-              "defaultValue": "",
+              "defaultValue": null,
               "reportable": 1,
               "minInterval": 1,
               "maxInterval": 65534,
@@ -4038,7 +4039,7 @@
               "storageOption": "External",
               "singleton": 0,
               "bounded": 0,
-              "defaultValue": "",
+              "defaultValue": null,
               "reportable": 1,
               "minInterval": 1,
               "maxInterval": 65534,
@@ -4054,7 +4055,7 @@
               "storageOption": "External",
               "singleton": 0,
               "bounded": 0,
-              "defaultValue": "",
+              "defaultValue": null,
               "reportable": 1,
               "minInterval": 1,
               "maxInterval": 65534,
@@ -4070,7 +4071,7 @@
               "storageOption": "External",
               "singleton": 0,
               "bounded": 0,
-              "defaultValue": "",
+              "defaultValue": null,
               "reportable": 1,
               "minInterval": 1,
               "maxInterval": 65534,
diff --git a/src/app/tests/suites/TestIcdManagementCluster.yaml b/src/app/tests/suites/TestIcdManagementCluster.yaml
index f7f207a..a319683 100644
--- a/src/app/tests/suites/TestIcdManagementCluster.yaml
+++ b/src/app/tests/suites/TestIcdManagementCluster.yaml
@@ -141,7 +141,7 @@
       response:
           constraints:
               type: bitmap32
-          value: 0x110D
+          value: 0x111D
 
     - label: "Read UserActiveModeTriggerInstruction"
       command: "readAttribute"
diff --git a/src/app/tests/suites/certification/PICS.yaml b/src/app/tests/suites/certification/PICS.yaml
index d23a1db..cf01598 100644
--- a/src/app/tests/suites/certification/PICS.yaml
+++ b/src/app/tests/suites/certification/PICS.yaml
@@ -8692,10 +8692,13 @@
           "Does the device implement the UserActiveModeTriggerHint attribute?"
       id: ICDM.S.A0006
 
+    - label: "Does the device implement the OperatingMode? attribute?"
+      id: ICDM.S.A0007
+
     - label:
           "Does the device implement the UserActiveModeTriggerInstruction
           attribute?"
-      id: ICDM.S.A0007
+      id: ICDM.S.A0008
 
     #
     # Client Attribute
diff --git a/src/app/tests/suites/certification/ci-pics-values b/src/app/tests/suites/certification/ci-pics-values
index 20f0a33..aa8fb52 100644
--- a/src/app/tests/suites/certification/ci-pics-values
+++ b/src/app/tests/suites/certification/ci-pics-values
@@ -2668,6 +2668,7 @@
 ICDM.S.A0005=1
 ICDM.S.A0006=1
 ICDM.S.A0007=1
+ICDM.S.A0008=1
 
 #Client Attribute
 ICDM.C.A0000=1
diff --git a/src/python_testing/TC_ICDM_2_1.py b/src/python_testing/TC_ICDM_2_1.py
index 21a3082..6af092a 100644
--- a/src/python_testing/TC_ICDM_2_1.py
+++ b/src/python_testing/TC_ICDM_2_1.py
@@ -15,98 +15,250 @@
 #    limitations under the License.
 #
 import logging
+import re
 
 import chip.clusters as Clusters
-from matter_testing_support import MatterBaseTest, async_test_body, default_matter_test_main
+from matter_testing_support import MatterBaseTest, TestStep, async_test_body, default_matter_test_main
 from mobly import asserts
 
-logger = logging.getLogger('PythonMatterControllerTEST')
-logger.setLevel(logging.INFO)
+logger = logging.getLogger(__name__)
+
+kRootEndpointId = 0
+kMaxUserActiveModeBitmap = 0x1FFFF
+kMaxUserActiveModeTriggerInstructionByteLength = 128
+
+cluster = Clusters.Objects.IcdManagement
+uat = cluster.Bitmaps.UserActiveModeTriggerBitmap
+modes = cluster.Enums.OperatingModeEnum
+features = cluster.Bitmaps.Feature
+
+# BitMask for all user active mode trigger hints that are depedent on the UserActiveModeTriggerInstruction
+kUatInstructionDependentBitMask = uat.kCustomInstruction | uat.kActuateSensorSeconds | uat.kActuateSensorTimes | uat.kActuateSensorLightsBlink | uat.kResetButtonLightsBlink | uat.kResetButtonSeconds | uat.kResetButtonTimes | uat.kSetupButtonSeconds | uat.kSetupButtonLightsBlink | uat.kSetupButtonTimes | uat.kAppDefinedButton
+
+# BitMask for UserActiveModeTriggerHint that REQUIRE the prescense of the UserActiveModeTriggerInstruction
+kUatInstructionMandatoryBitMask = uat.kCustomInstruction | uat.kActuateSensorSeconds | uat.kActuateSensorTimes | uat.kResetButtonSeconds | uat.kResetButtonTimes | uat.kSetupButtonSeconds | uat.kSetupButtonTimes | uat.kAppDefinedButton
+
+# BitMask for all user active mode trigger hints that have the UserActiveModeTriggerInstruction as an uint
+kUatNumberInstructionBitMask = uat.kActuateSensorSeconds | uat.kActuateSensorTimes | uat.kResetButtonSeconds | uat.kResetButtonTimes | uat.kSetupButtonSeconds | uat.kSetupButtonTimes
+
+# BitMask for all user active mode trigger hints that provide a color in the UserActiveModeTriggerInstruction
+kUatColorInstructionBitMask = uat.kActuateSensorLightsBlink | uat.kResetButtonLightsBlink | uat.kSetupButtonLightsBlink
 
 
 class TC_ICDM_2_1(MatterBaseTest):
-    async def read_icdm_attribute_expect_success(self, endpoint, attribute):
-        cluster = Clusters.Objects.IcdManagement
-        return await self.read_single_attribute_check_success(endpoint=endpoint, cluster=cluster, attribute=attribute)
+
+    #
+    # Class Helper functions
+    #
+
+    @staticmethod
+    def is_valid_uint32_value(var):
+        return isinstance(var, int) and 0 <= var <= 0xFFFFFFFF
+
+    @staticmethod
+    def is_valid_uint16_value(var):
+        return isinstance(var, int) and 0 <= var <= 0xFFFF
+
+    @staticmethod
+    def is_valid_uint8_value(var):
+        return isinstance(var, int) and 0 <= var <= 0xFF
+
+    @staticmethod
+    def set_bits_count(number):
+        return bin(number).count("1")
+
+    async def _read_icdm_attribute_expect_success(self, attribute):
+        return await self.read_single_attribute_check_success(endpoint=kRootEndpointId, cluster=cluster, attribute=attribute)
+
+    async def _wildcard_cluster_read(self):
+        return await self.default_controller.ReadAttribute(self.dut_node_id, [(kRootEndpointId, cluster)])
+
+    #
+    # Test Harness Helpers
+    #
+
+    def desc_TC_ICDM_2_1(self) -> str:
+        """Returns a description of this test"""
+        return "[TC_ICDM_2_1]  attributes with DUT as Server"
+
+    def steps_TC_ICDM_2_1(self) -> list[TestStep]:
+        steps = [
+            TestStep(1, "Commissioning, already done", is_commissioning=True),
+            TestStep(2, "TH reads from the DUT the ActiveModeThreshold attribute."),
+            TestStep(3, "TH reads from the DUT the ActiveModeDuration attribute."),
+            TestStep(4, "TH reads from the DUT the IdleModeDuration attribute."),
+            TestStep(
+                5, "TH reads from the DUT the ClientsSupportedPerFabric attribute."),
+            TestStep(6, "TH reads from the DUT the RegisteredClients attribute."),
+            TestStep(7, "TH reads from the DUT the ICDCounter attribute."),
+            TestStep(
+                8, "TH reads from the DUT the UserActiveModeTriggerHint attribute."),
+            TestStep(
+                9, "TH reads from the DUT the UserActiveModeTriggerInstruction attribute"),
+            TestStep(10, "TH reads from the DUT the OperatingMode attribute."),
+        ]
+        return steps
 
     def pics_TC_ICDM_2_1(self) -> list[str]:
-        return ["ICDM.S"]
+        """ This function returns a list of PICS for this test case that must be True for the test to be run"""
+        pics = [
+            "ICDM.S",
+        ]
+        return pics
+
+    #
+    # ICDM 2.1 Test Body
+    #
 
     @async_test_body
     async def test_TC_ICDM_2_1(self):
 
-        if not self.check_pics("ICDM.S"):
-            logger.info("Test skipped because PICS ICDM.S is not set")
-            return
+        cluster = Clusters.Objects.IcdManagement
+        attributes = cluster.Attributes
 
-        endpoint = self.user_params.get("endpoint", 0)
+        # Commissioning
+        self.step(1)
+        # Read feature map
+        featureMap = await self._read_icdm_attribute_expect_success(
+            attributes.FeatureMap)
 
-        self.print_step(1, "Commissioning, already done")
-        attributes = Clusters.IcdManagement.Attributes
-        idleModeDuration = 0
+        # Validate ActiveModeThreshold
+        self.step(2)
+        if self.check_pics("ICDM.S.A0002"):
 
-        # Idle Mode Duration attribute test
-        if (self.check_pics("ICDM.S.A0000")):
-            self.print_step(2, "Read IdleModeDuration Attribute")
-
-            idleModeDuration = await self.read_icdm_attribute_expect_success(endpoint=endpoint,
-                                                                             attribute=attributes.IdleModeDuration)
-            asserts.assert_greater_equal(idleModeDuration, 1, "IdleModeDuration attribute is smaller than minimum value (1).")
-            asserts.assert_less_equal(idleModeDuration, 64800, "IdleModeDuration attribute is greater than maximum value (64800).")
-        else:
-            asserts.assert_true(False, "IdleModeDuration is a mandatory attribute and must be present in the PICS file")
-
-        # Active Mode Duration attribute test
-        if (self.check_pics("ICDM.S.A0001")):
-            self.print_step(2, "Read ActiveModeDuration Attribute")
-
-            idleModeDuration *= 1000  # Convert seconds to milliseconds
-            activeModeDuration = await self.read_icdm_attribute_expect_success(endpoint=endpoint,
-                                                                               attribute=attributes.ActiveModeDuration)
-            asserts.assert_true(0 <= activeModeDuration <= 65535,
-                                "ActiveModeDuration attribute does not fit in a uint16.")
-            asserts.assert_less_equal(activeModeDuration, idleModeDuration,
-                                      "ActiveModeDuration attribute is greater than the IdleModeDuration attrbiute.")
-        else:
-            asserts.assert_true(False, "ActiveModeDuration is a mandatory attribute and must be present in the PICS file")
-
-        # Active Mode Threshold attribute test
-        if (self.check_pics("ICDM.S.A0002")):
-            self.print_step(2, "Read ActiveModeThreshold Attribute")
-
-            activeModeThreshold = await self.read_icdm_attribute_expect_success(endpoint=endpoint,
-                                                                                attribute=attributes.ActiveModeThreshold)
-            asserts.assert_true(0 <= activeModeThreshold <= 65535,
+            activeModeThreshold = await self._read_icdm_attribute_expect_success(
+                attributes.ActiveModeThreshold)
+            # Verify ActiveModeThreshold is not bigger than uint16
+            asserts.assert_true(self.is_valid_uint16_value(activeModeThreshold),
                                 "ActiveModeThreshold attribute does not fit in a uint16.")
+
+            if featureMap > 0 and features.kLongIdleTimeSupport in features(featureMap):
+                asserts.assert_greater_equal(
+                    activeModeThreshold, 5000, "Minimum ActiveModeThreshold is 5s for a LIT ICD.")
+
         else:
-            asserts.assert_true(False, "ActiveModeThreshold is a mandatory attribute and must be present in the PICS file")
+            asserts.assert_true(
+                False, "ActiveModeThreshold is a mandatory attribute and must be present in the PICS file")
 
-        # RegisteredClients attribute test
-        if (self.check_pics("ICDM.S.A0003")):
-            self.print_step(2, "Read RegisteredClients Attribute")
+        # Validate ActiveModeDuration
+        self.step(3)
+        if self.check_pics("ICDM.S.A0001"):
+            activeModeDuration = await self._read_icdm_attribute_expect_success(
+                attributes.ActiveModeDuration)
+            # Verify ActiveModeDuration is not bigger than uint32
+            asserts.assert_true(self.is_valid_uint32_value(activeModeDuration),
+                                "ActiveModeDuration attribute does not fit in a uint32")
+        else:
+            asserts.assert_true(
+                False, "ActiveModeDuration is a mandatory attribute and must be present in the PICS file")
 
-            await self.read_icdm_attribute_expect_success(endpoint=endpoint,
-                                                          attribute=attributes.RegisteredClients)
+        # Validate IdleModeDuration
+        self.step(4)
+        if self.check_pics("ICDM.S.A0000"):
+            idleModeDuration = await self._read_icdm_attribute_expect_success(
+                attributes.IdleModeDuration)
+            # Verify IdleModeDuration is not bigger than uint32
+            asserts.assert_greater_equal(
+                idleModeDuration, 1, "IdleModeDuration attribute is smaller than minimum value (1).")
+            asserts.assert_less_equal(
+                idleModeDuration, 64800, "IdleModeDuration attribute is greater than maximum value (64800).")
+            asserts.assert_greater_equal(idleModeDuration * 1000, activeModeDuration,
+                                         "ActiveModeDuration attribute is greater than the IdleModeDuration attrbiute.")
+        else:
+            asserts.assert_true(
+                False, "IdleModeDuration is a mandatory attribute and must be present in the PICS file")
 
-        # ICDCounter attribute test
-        if (self.check_pics("ICDM.S.A0003")):
-            self.print_step(2, "Read ICDCounter Attribute")
+        # Validate ClientsSupportedPerFabric
+        self.step(5)
+        if self.pics_guard(self.check_pics("ICDM.S.A0005")):
+            clientsSupportedPerFabric = await self._read_icdm_attribute_expect_success(
+                attributes.ClientsSupportedPerFabric)
 
-            ICDCounter = await self.read_icdm_attribute_expect_success(endpoint=endpoint,
-                                                                       attribute=attributes.ICDCounter)
-            asserts.assert_true(0 <= ICDCounter <= 4294967295,
-                                "ICDCounter attribute does not fit in a uint32.")
+            # Verify ClientsSupportedPerFabric is not bigger than uint16
+            asserts.assert_true(self.is_valid_uint16_value(clientsSupportedPerFabric),
+                                "ClientsSupportedPerFabric attribute does not fit in a uint16.")
 
-        # ClientsSupportedPerFabric attribute test
-        if (self.check_pics("ICDM.S.A0003")):
-            self.print_step(2, "Read ClientsSupportedPerFabric Attribute")
+            asserts.assert_greater_equal(
+                clientsSupportedPerFabric, 1, "ClientsSupportedPerFabric attribute is smaller than minimum value (1).")
 
-            clientsSupportedPerFabric = await self.read_icdm_attribute_expect_success(endpoint=endpoint,
-                                                                                      attribute=attributes.ClientsSupportedPerFabric)
-            asserts.assert_true(0 <= clientsSupportedPerFabric <= 65535,
-                                "ActiveModeThreshold ClientsSupportedPerFabric does not fit in a uint16.")
-            asserts.assert_greater_equal(clientsSupportedPerFabric, 1,
-                                         "ClientsSupportedPerFabric attribute is smaller than minimum value (1).")
+        # Validate RegisteredClients
+        self.step(6)
+        if self.pics_guard(self.check_pics("ICDM.S.A0003")):
+            registeredClients = await self._read_icdm_attribute_expect_success(
+                attributes.RegisteredClients)
+
+            asserts.assert_true(isinstance(
+                registeredClients, list), "RegisteredClients is not a list.")
+
+        # Validate ICDCounter
+        self.step(7)
+        if self.pics_guard(self.check_pics("ICDM.S.A0004")):
+
+            icdCounter = await self._read_icdm_attribute_expect_success(
+                attributes.ICDCounter)
+            # Verify ICDCounter is not bigger than uint32
+            asserts.assert_true(self.is_valid_uint32_value(icdCounter),
+                                "ActiveModeDuration attribute does not fit in a uint32")
+
+        # Validate UserActiveModeTriggerHint
+        self.step(8)
+        if self.pics_guard(self.check_pics("ICDM.S.A0006")):
+            userActiveModeTriggerHint = await self._read_icdm_attribute_expect_success(
+                attributes.UserActiveModeTriggerHint)
+
+            # Verify that it is a bitmap32 - Only the first 16 bits are used
+            asserts.assert_true(0 <= userActiveModeTriggerHint <= kMaxUserActiveModeBitmap,
+                                "UserActiveModeTriggerHint attribute does not fit in a bitmap32")
+
+            # Verify that only a single UserActiveModeTriggerInstruction dependent bit is set
+            uatHintInstructionDepedentBitmap = uat(
+                userActiveModeTriggerHint) & kUatInstructionDependentBitMask
+
+        asserts.assert_less_equal(
+            self.set_bits_count(uatHintInstructionDepedentBitmap), 1, "UserActiveModeTriggerHint has more than 1 bit that is dependent on the UserActiveModeTriggerInstruction")
+
+        # Valdate UserActiveModeTriggerInstruction
+        self.step(9)
+        if self.check_pics("ICDM.S.A0007"):
+            userActiveModeTriggerInstruction = await self._read_icdm_attribute_expect_success(
+                attributes.UserActiveModeTriggerInstruction)
+
+            # Verify that the UserActiveModeTriggerInstruction has the correct encoding
+            try:
+                encodedUATInstruction = userActiveModeTriggerInstruction.encode(
+                    'utf-8')
+            except Exception:
+                asserts.assert_true(
+                    False, "UserActiveModeTriggerInstruction is not encoded in the correct format (utf-8).")
+
+            # Verify byte length of the UserActiveModeTirggerInstruction
+            asserts.assert_less_equal(
+                len(encodedUATInstruction), kMaxUserActiveModeTriggerInstructionByteLength, "UserActiveModeTriggerInstruction is longuer than the maximum allowed length (128).")
+
+            if uatHintInstructionDepedentBitmap > 0 and uatHintInstructionDepedentBitmap in kUatNumberInstructionBitMask:
+                # Validate Instruction is a decimal unsigned integer using the ASCII digits 0-9, and without leading zeros.
+                asserts.assert_true((re.search(r'^(?!0)[0-9]*$', userActiveModeTriggerInstruction) is not None),
+                                    "UserActiveModeTriggerInstruction is not in the correct format for the associated UserActiveModeTriggerHint")
+
+            if uatHintInstructionDepedentBitmap > 0 and uatHintInstructionDepedentBitmap in kUatColorInstructionBitMask:
+                # TODO: https://github.com/CHIP-Specifications/connectedhomeip-spec/issues/9194
+                asserts.assert_true(False, "Nothing to do for now")
+        else:
+            # Check if the UserActiveModeTriggerInstruction was required
+            asserts.assert_false(uatHintInstructionDepedentBitmap in kUatInstructionMandatoryBitMask,
+                                 "UserActiveModeTriggerHint requires the UserActiveModeTriggerInstruction")
+
+        # Verify OperatingMode
+        self.step(10)
+        if self.pics_guard(self.check_pics("ICDM.S.A0008")):
+            operatingMode = await self._read_icdm_attribute_expect_success(
+                attributes.OperatingMode)
+
+            asserts.assert_true(self.is_valid_uint8_value(operatingMode),
+                                "OperatingMode does not fit in an enum8")
+
+            asserts.assert_less(
+                operatingMode, modes.kUnknownEnumValue, "OperatingMode can only have 0 and 1 as valid values")
 
 
 if __name__ == "__main__":
diff --git a/src/python_testing/test_testing/MockTestRunner.py b/src/python_testing/test_testing/MockTestRunner.py
index 451e38d..5d6592b 100644
--- a/src/python_testing/test_testing/MockTestRunner.py
+++ b/src/python_testing/test_testing/MockTestRunner.py
@@ -37,9 +37,9 @@
 
 
 class MockTestRunner():
-    def __init__(self, filename: str, classname: str, test: str, endpoint: int):
+    def __init__(self, filename: str, classname: str, test: str, endpoint: int, pics: dict[str, bool] = {}):
         self.config = MatterTestConfig(
-            tests=[test], endpoint=endpoint, dut_node_ids=[1])
+            tests=[test], endpoint=endpoint, dut_node_ids=[1], pics=pics)
         self.stack = MatterStackState(self.config)
         self.default_controller = self.stack.certificate_authorities[0].adminList[0].NewController(
             nodeId=self.config.controller_node_id,
diff --git a/src/python_testing/test_testing/test_TC_ICDM_2_1.py b/src/python_testing/test_testing/test_TC_ICDM_2_1.py
new file mode 100755
index 0000000..5069eb6
--- /dev/null
+++ b/src/python_testing/test_testing/test_TC_ICDM_2_1.py
@@ -0,0 +1,235 @@
+#!/usr/bin/env -S python3 -B
+#
+#    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.
+#
+
+import string
+import sys
+from dataclasses import dataclass
+
+import chip.clusters as Clusters
+from chip.clusters import Attribute
+from MockTestRunner import MockTestRunner
+
+c = Clusters.IcdManagement
+attr = c.Attributes
+uat = c.Bitmaps.UserActiveModeTriggerBitmap
+
+
+@dataclass
+class ICDMData():
+    FeatureMap: int
+    IdleModeDuration: int
+    ActiveModeDuration: int
+    ActiveModeThreshold: int
+    RegisteredClients: list
+    ICDCounter: int
+    ClientsSupportedPerFabric: int
+    UserActiveModeTriggerHint: int
+    UserActiveModeTriggerInstruction: string
+    OperatingMode: c.Enums.OperatingModeEnum
+    expect_pass: bool
+
+
+long_string = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut e"
+too_long_string = long_string + "1"
+
+TEST_CASES = [
+
+    # ==============================
+    # ICDM 2.1 Test cases
+    # ==============================
+    # --------
+    # Test cases to validate IdleModeDuration
+    # --------
+    # IdleModeDuration under minimum (< 1)
+    ICDMData(0, 0, 0, 100, [], 0, 2, 0, "",
+             c.Enums.OperatingModeEnum.kSit, False),
+    # IdleModeDuration at minimum
+    ICDMData(0, 1, 0, 100, [], 0, 2, 0, "",
+             c.Enums.OperatingModeEnum.kSit, True),
+    # IdleModeDuration at maximum
+    ICDMData(0, 64800, 100, 100, [], 0, 2, 0, "",
+             c.Enums.OperatingModeEnum.kSit,  True),
+    # IdleModeDuration over maximum (>64800)
+    ICDMData(0, 64801, 100, 100, [], 0, 2, 0, "",
+             c.Enums.OperatingModeEnum.kSit, False),
+    # IdleModeDuration < ActiveModeDuration
+    ICDMData(0, 1, 1001, 100, [], 0, 2, 0, "",
+             c.Enums.OperatingModeEnum.kSit, False),
+    # --------
+    # Test cases to validate ActiveModeDuration
+    # --------
+    # ActiveModeDuration under minimum
+    ICDMData(0, 100, -1, 100, [], 0, 2, 0, "",
+             c.Enums.OperatingModeEnum.kSit, False),
+    # ActiveModeDuration at minimum
+    ICDMData(0, 100, 0, 100, [], 0, 2, 0, "",
+             c.Enums.OperatingModeEnum.kSit, True),
+    # ActiveModeDuration at maximum - value is max IdleModeDuration value - 1
+    ICDMData(0, 64800, 0x3DCC4FF, 100, [], 0, 2, 0, "",
+             c.Enums.OperatingModeEnum.kSit, True),
+    # --------
+    # Test cases to validate ActiveModeThreshold
+    # --------
+    # ActiveModeThreshold < minimum
+    ICDMData(0, 1, 0, -1, [], 0, 2, 0, "",
+             c.Enums.OperatingModeEnum.kSit, False),
+    # ActiveModeThreshold at SIT minimum
+    ICDMData(0, 1, 0, 0, [], 0, 2, 0, "",
+             c.Enums.OperatingModeEnum.kSit, True),
+    # ActiveModeThreshold under LIT minimum
+    ICDMData(0x7, 1, 0, 4999, [], 0, 2, 0, "",
+             c.Enums.OperatingModeEnum.kLit, False),
+    # ActiveModeThreshold at LIT minimum
+    ICDMData(0x7, 1, 0, 5000, [], 0, 2, 0, "",
+             c.Enums.OperatingModeEnum.kLit, True),
+    # ActiveModeThreshold at Maximum
+    ICDMData(0, 1, 0, 0xFFFF, [], 0, 2, 0, "",
+             c.Enums.OperatingModeEnum.kSit, True),
+    # ActiveModeThreshold over Maximum
+    ICDMData(0, 1, 0, 0x10000, [], 0, 2, 0, "",
+             c.Enums.OperatingModeEnum.kSit, False),
+    # --------
+    # Test cases to validate ClientsSupportedPerFabric
+    # --------
+    # ClientsSupportedPerFabric under minimum (< 1)
+    ICDMData(0, 1, 0, 100, [], 0, 0, 0, "",
+             c.Enums.OperatingModeEnum.kLit, False),
+    # ClientsSupportedPerFabric at minimum
+    ICDMData(0, 1, 0, 100, [], 0, 1, 0, "",
+             c.Enums.OperatingModeEnum.kLit, True),
+    # ClientsSupportedPerFabric at maximum
+    ICDMData(0, 1, 0, 100, [], 0, 255, 0, "",
+             c.Enums.OperatingModeEnum.kLit, True),
+    # ClientsSupportedPerFabric > maximum
+    ICDMData(0, 1, 0, 100, [], 0, 256, 0, "",
+             c.Enums.OperatingModeEnum.kLit, True),
+    # --------
+    # Test cases to validate RegisteredClients
+    # --------
+    # Incorrect type
+    ICDMData(0, 1, 0, 100, 0, 0, 1, 0, "",
+             c.Enums.OperatingModeEnum.kLit, False),
+    # Correct type
+    ICDMData(0, 1, 0, 100, [], 0, 1, 0, "",
+             c.Enums.OperatingModeEnum.kLit, True),
+    # --------
+    # Test cases to validate ICDCounter
+    # --------
+    # ICDCounter under minimum (< 0)
+    ICDMData(0, 1, 0, 100, [], -1, 1, 0, "",
+             c.Enums.OperatingModeEnum.kLit, False),
+    # ICDCounter at minimum
+    ICDMData(0, 1, 0, 100, [], 0, 1, 0, "",
+             c.Enums.OperatingModeEnum.kLit, True),
+    # ICDCounter at maximum
+    ICDMData(0, 1, 0, 100, [], 0xFFFFFFFF, 1, 0, "",
+             c.Enums.OperatingModeEnum.kLit, True),
+    # ICDCounter over maximum
+    ICDMData(0, 1, 0, 100, [], 0x100000000, 1, 0, "",
+             c.Enums.OperatingModeEnum.kLit, False),
+    # --------
+    # Test cases to validate UserActiveModeTriggerHint
+    # --------
+    # UserActiveModeTriggerHint outsite valid range
+    ICDMData(0, 1, 0, 100, [], 0, 1, 0x1FFFF, "",
+             c.Enums.OperatingModeEnum.kLit, False),
+    # UserActiveModeTriggerHint outsite valid range
+    ICDMData(0, 1, 0, 100, [], 0, 1, -1, "",
+             c.Enums.OperatingModeEnum.kLit, False),
+    # UserActiveModeTriggerHint with no hints
+    ICDMData(0, 1, 0, 100, [], 0, 1, 0, "",
+             c.Enums.OperatingModeEnum.kLit, True),
+    # UserActiveModeTriggerHint wiht two instruction depedent bits set
+    ICDMData(0, 1, 0, 100, [], 0, 1, uat.kCustomInstruction | uat.kActuateSensorSeconds, "",
+             c.Enums.OperatingModeEnum.kLit, False),
+    # --------
+    # Test cases to validate UserActiveModeTriggerInstruction
+    # --------
+    # UserActiveModeTriggerInstruction with wrong encoding
+    ICDMData(0, 1, 0, 100, [], 0, 1, uat.kCustomInstruction, "Hello\uD83D\uDE00World",
+             c.Enums.OperatingModeEnum.kLit, False),
+    # UserActiveModeTriggerInstruction with empty string
+    ICDMData(0, 1, 0, 100, [], 0, 1, uat.kCustomInstruction, "",
+             c.Enums.OperatingModeEnum.kLit, True),
+    # UserActiveModeTriggerInstruction with empty string
+    ICDMData(0, 1, 0, 100, [], 0, 1, uat.kCustomInstruction, "",
+             c.Enums.OperatingModeEnum.kLit, True),
+    # UserActiveModeTriggerInstruction with max string length
+    ICDMData(0, 1, 0, 100, [], 0, 1, uat.kCustomInstruction, long_string,
+             c.Enums.OperatingModeEnum.kLit, True),
+    # UserActiveModeTriggerInstruction > max string length
+    ICDMData(0, 1, 0, 100, [], 0, 1, uat.kCustomInstruction, too_long_string,
+             c.Enums.OperatingModeEnum.kLit, False),
+    # UserActiveModeTriggerInstruction invalid number - Trailing 0s
+    ICDMData(0, 1, 0, 100, [], 0, 1, uat.kActuateSensorSeconds, "001",
+             c.Enums.OperatingModeEnum.kLit, False),
+    # UserActiveModeTriggerInstruction invalid number - Letters
+    ICDMData(0, 1, 0, 100, [], 0, 1,  uat.kActuateSensorSeconds, "not a number",
+             c.Enums.OperatingModeEnum.kLit, False),
+    # UserActiveModeTriggerInstruction Valid number
+    ICDMData(0, 1, 0, 100, [], 0, 1,  uat.kActuateSensorSeconds, "100000",
+             c.Enums.OperatingModeEnum.kLit, True),
+    # --------
+    # Test cases to validate OpertingMode
+    # --------
+    # OpertingMode with negative value
+    ICDMData(0, 1, 0, 100, [], 0, 1,  uat.kActuateSensorSeconds, "100000",
+             -1, False),
+    # OpertingMode with Accepted value
+    ICDMData(0, 1, 0, 100, [], 0, 1,  uat.kActuateSensorSeconds, "100000",
+             c.Enums.OperatingModeEnum.kLit, True),
+    # OpertingMode with unkown value
+    ICDMData(0, 1, 0, 100, [], 0, 1,  uat.kActuateSensorSeconds, "100000",
+             c.Enums.OperatingModeEnum.kUnknownEnumValue, False),
+
+]
+
+
+def test_spec_to_attribute_cache(test_icdm: ICDMData) -> Attribute.AsyncReadTransaction.ReadResponse:
+    resp = Attribute.AsyncReadTransaction.ReadResponse({}, [], {})
+    resp.attributes = {0: {c: {attr.FeatureMap: test_icdm.FeatureMap, attr.IdleModeDuration: test_icdm.IdleModeDuration, attr.ActiveModeDuration: test_icdm.ActiveModeDuration, attr.ActiveModeThreshold: test_icdm.ActiveModeThreshold,
+                               attr.RegisteredClients: test_icdm.RegisteredClients, attr.ICDCounter: test_icdm.ICDCounter,
+                               attr.ClientsSupportedPerFabric: test_icdm.ClientsSupportedPerFabric, attr.UserActiveModeTriggerHint: test_icdm.UserActiveModeTriggerHint,
+                               attr.UserActiveModeTriggerInstruction: test_icdm.UserActiveModeTriggerInstruction, attr.OperatingMode: test_icdm.OperatingMode}}}
+    return resp
+
+
+def main():
+    pics = {"ICDM.S.A0000": True, "ICDM.S.A0001": True, "ICDM.S.A0002": True, "ICDM.S.A0003": True, "ICDM.S.A0004": True,
+            "ICDM.S.A0005": True, "ICDM.S.A0006": True, "ICDM.S.A0007": True, "ICDM.S.A0008": True, }
+
+    test_runner = MockTestRunner(
+        'TC_ICDM_2_1', 'TC_ICDM_2_1', 'test_TC_ICDM_2_1', 0, pics)
+    failures = []
+    for idx, t in enumerate(TEST_CASES):
+        ok = test_runner.run_test_with_mock_read(
+            test_spec_to_attribute_cache(t)) == t.expect_pass
+        if not ok:
+            failures.append(f"Measured test case failure: {idx} {t}")
+
+    test_runner.Shutdown()
+    print(
+        f"Test of tests: run {len(TEST_CASES)}, test response correct: {len(TEST_CASES) - len(failures)} | test response incorrect: {len(failures)}")
+    for f in failures:
+        print(f)
+
+    return 1 if failures else 0
+
+
+if __name__ == "__main__":
+    sys.exit(main())