Proposal for TC_MOD_1.2 (#37476)

* Min fixes

* Added manual step for confirm humnan readable text.

* Restyled by isort

* Fixes from code-lint

* Fix 16bit assert. Update to use self.endpoint

* Fixing issue from code review.

* Restyled by isort

* Added attribute_guard to OnMode attribute with DEPONOFF type.

* Used Hex valie instead of variable.
Confirm mode values are unique.
Removed manual check for description value.

* Fix typos

* Restyled by isort

---------

Co-authored-by: Restyled.io <commits@restyled.io>
diff --git a/src/python_testing/TC_MOD_1_2.py b/src/python_testing/TC_MOD_1_2.py
new file mode 100644
index 0000000..adb9fb2
--- /dev/null
+++ b/src/python_testing/TC_MOD_1_2.py
@@ -0,0 +1,176 @@
+#
+#    Copyright (c) 2025 Project CHIP Authors
+#    All rights reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License");
+#    you may not use this file except in compliance with the License.
+#    You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS,
+#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#    See the License for the specific language governing permissions and
+#    limitations under the License.
+#
+
+# See https://github.com/project-chip/connectedhomeip/blob/master/docs/testing/python.md"#defining-the-ci-test-arguments
+# for details about the block below.
+#
+# === BEGIN CI TEST ARGUMENTS ===
+# test-runner-runs:
+#   run1:
+#     app: ${ALL_CLUSTERS_APP}
+#     app-args: --discriminator 1234 --KVS kvs1 --trace-to json:${TRACE_APP}.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:${TRACE_TEST_JSON}.json
+#       --trace-to perfetto:${TRACE_TEST_PERFETTO}.perfetto
+#     factory-reset: true
+#     quiet: true
+# === END CI TEST ARGUMENTS ===""
+
+import logging
+
+import chip.clusters as Clusters
+from chip.clusters.Types import NullValue
+from chip.testing.matter_testing import MatterBaseTest, TestStep, async_test_body, default_matter_test_main
+from mobly import asserts
+
+logger = logging.getLogger(__name__)
+
+
+class MOD_1_2(MatterBaseTest):
+    """Proposal test for Mode Select Cluster attributes as a server."""
+
+    def desc_MOD_1_2(self) -> str:
+        return "80.2.1. [TC-MOD-1.2] Cluster attributes with DUT as Server"
+
+    def pics_MOD_1_2(self):
+        """Return PICS definitions asscociated with this test."""
+        pics = [
+            "MOD.S"
+        ]
+        return pics
+
+    def steps_MOD_1_2(self) -> list[TestStep]:
+        steps = [
+            TestStep(1, "Commission DUT to TH (can be skipped if done in a preceding test).", is_commissioning=True),
+            TestStep(2, "TH reads the SupportedModes attribute from DUT"),
+            TestStep(3, "TH reads the CurrentMode attribute from the DUT"),
+            TestStep(4, "TH reads the OnMode attribute from the DUT"),
+            TestStep(5, "TH reads the StartUpMode attribute from the DUT"),
+            TestStep(6, "TH reads the Description attribute from the DUT"),
+            TestStep(7, "TH reads the StandardNamespace attribute from the DUT")
+        ]
+        return steps
+
+    def _verify_supported_mode(self, supported_mode):
+        """Verify supported mode.
+        Each mode should be a struct containing a label (user understandable string describing the mode), mode number (integer that identifies the mode and is unique within this SupportedMode list) and  list of semantic tags
+        MfgCode and Value fields present in the SemanticTags structs should be no larger than 16 bits on the TH (Chip-tool)."""
+        if not hasattr(supported_mode, 'label') and not isinstance(supported_mode.label, str):
+            asserts.fail("Supported mode struct does not have attribute label or does not contain string ")
+
+        if not hasattr(supported_mode, 'mode') and not isinstance(supported_mode.mode, int):
+            asserts.fail("Supported modes struct does not have attribute mode or does not contain int")
+
+        if not hasattr(supported_mode, 'semanticTags'):
+            asserts.fail("Supported mode struct does not have attribute semanticTags")
+
+        # veirfy if we have entries for semanticTags if there are any  verfy values no larger that 16 bits
+        # entries must have the value and manufacturer code.
+        self._log_attribute("Semantic tags", supported_mode.semanticTags)
+
+        if isinstance(supported_mode.semanticTags, list) and len(supported_mode.semanticTags) > 0:
+            logger.info(
+                "SupportedMode.semanticTags contains values, verifying attributes for manufacturedcode and value are not longer than 16bits int")
+            for semantictag in supported_mode.semanticTags:
+                asserts.assert_true(semantictag.mfgCode >= 0 and semantictag.mfgCode <= 0xFFFF,
+                                    "Element semantictag.Mfgcode is greater than 16 bits")
+                asserts.assert_true(semantictag.value >= 0 and semantictag.value <= 0xFFFF,
+                                    "Element semantictag.Value is greater than 16 bits")
+
+    def _log_attribute(self, name, value):
+        logger.info(f"{name} attribute with value: {value} with type: {type(value)}")
+
+    @async_test_body
+    async def test_MOD_1_2(self):
+        self.cluster = Clusters.ModeSelect
+        self.endpoint = self.get_endpoint(1)
+
+        # Commision device
+        # In the test plan step 1 is defined as a precondition.
+        self.step(1)
+
+        # Veirfy the Supported modes
+        # Verify contains attributes label and mode
+        # Verify if semantic tags has elements in list , and if there are values assert the values
+        self.step(2)
+        supported_modes = await self.read_single_attribute_check_success(endpoint=self.endpoint, cluster=self.cluster, attribute=self.cluster.Attributes.SupportedModes)
+        logger.info(f"Supported modes {supported_modes}")
+        # List must not be empty
+        asserts.assert_true(len(supported_modes) > 0, "Supported modes can not be empty.")
+        supported_modes_values = []
+        for supported_mode in supported_modes:
+            logger.info(
+                f"Label {supported_mode} with type {type(supported_mode)} and {supported_mode.label} and {supported_mode.mode} and {supported_mode.semanticTags}")
+            # Verify the struct values
+            self._verify_supported_mode(supported_mode=supported_mode)
+            # After verifying the struct is correct append the mode value.
+            supported_modes_values.append(supported_mode.mode)
+        # Verify mode numbers are unique
+        asserts.assert_equal(len(supported_modes_values), len(set(supported_modes_values)),
+                             f"Duplicate value found for supported mode values :{supported_modes_values}.")
+        logger.info(f"Supported modes values {supported_modes_values}")
+
+        # Currentmode attribute check must be int and must be in the supported modes values.
+        self.step(3)
+        current_mode = await self.read_single_attribute_check_success(endpoint=self.endpoint, cluster=self.cluster, attribute=self.cluster.Attributes.CurrentMode)
+        self._log_attribute('CurrentMode', current_mode)
+        asserts.assert_true(isinstance(current_mode, int), "Current mode is not int")
+        asserts.assert_in(current_mode, supported_modes_values, f"Current mode {current_mode} is not in {supported_modes_values}")
+
+        self.step(4)
+        # DEPONOFF in the Mandatory/Optional Column
+        if await self.attribute_guard(endpoint=self.endpoint, attribute=self.cluster.Attributes.OnMode):
+            on_mode = await self.read_single_attribute_check_success(endpoint=self.endpoint, cluster=self.cluster, attribute=self.cluster.Attributes.OnMode)
+            # On mode can be Nullvalue
+            self._log_attribute("OnMode", on_mode)
+            asserts.assert_true((isinstance(on_mode, int) or on_mode is NullValue),
+                                "Onmode is not int or is not Nullvalue")
+            # Verify that OnMode is in the list of Supported Modes, but if null, cant be verified.
+            if on_mode is not NullValue:
+                asserts.assert_in(on_mode, supported_modes_values, f"Onmode {on_mode} is not in {supported_modes_values}")
+
+        # Validate startup mode (attribute Startup is optional)
+        self.step(5)
+        if await self.attribute_guard(endpoint=self.endpoint, attribute=self.cluster.Attributes.StartUpMode):
+            startup_mode = await self.read_single_attribute_check_success(endpoint=self.endpoint, cluster=self.cluster, attribute=self.cluster.Attributes.StartUpMode)
+            self._log_attribute("StartupMode", startup_mode)
+            asserts.assert_true(isinstance(startup_mode, int), "Startupmode is not int")
+            asserts.assert_in(startup_mode, supported_modes_values,
+                              f"Startupmode {startup_mode} is not in {supported_modes_values}")
+
+        # Verify the string  is str and larger that 1 char.
+        self.step(6)
+        description = await self.read_single_attribute_check_success(endpoint=self.endpoint, cluster=self.cluster, attribute=self.cluster.Attributes.Description)
+        self._log_attribute("Description", description)
+        asserts.assert_true(isinstance(description, str), "Description attribute is not str")
+        asserts.assert_true(len(description) >= 1, "Description is lower that 1 char.")
+
+        # Verify the StandardNamespace can be 16 bits enum or null
+        self.step(7)
+        standard_namepace = await self.read_single_attribute_check_success(endpoint=self.endpoint, cluster=self.cluster, attribute=self.cluster.Attributes.StandardNamespace)
+        self._log_attribute("StandardNamespace", standard_namepace)
+        asserts.assert_true((standard_namepace is NullValue or (isinstance(standard_namepace, int) and (standard_namepace >= 0 and standard_namepace <= 0xFFFF))),
+                            "Standard namespace is not 16bit enum or not Nullvalue")
+
+
+if __name__ == "__main__":
+    default_matter_test_main()