[Camera] Add Matter Camera TCs (TC-WEBRTC-1.1, TC-WEBRTC-1.2, TC-WEBRTC-1.3, TC-WEBRTC-1.4, TC-WEBRTC-1.5) (#38584)

* Add TC_WEBRTC_1_1 to python_testing

Signed-off-by: abhishek.h <abhishek.h@samsung.com>
Signed-off-by: Charles Kim <chulspro.kim@samsung.com>

* Add TC_WEBRTC_1_2 to python_testing

Signed-off-by: abhishek.h <abhishek.h@samsung.com>
Signed-off-by: Charles Kim <chulspro.kim@samsung.com>

* Add TC_WEBRTC_1_3 to python_testing

Signed-off-by: abhishek.h <abhishek.h@samsung.com>
Signed-off-by: Charles Kim <chulspro.kim@samsung.com>

* Add TC_WEBRTC_1_4 to python_testing

Signed-off-by: abhishek.h <abhishek.h@samsung.com>
Signed-off-by: Charles Kim <chulspro.kim@samsung.com>

* Add TC_WEBRTC_1_5 to python_testing

Signed-off-by: abhishek.h <abhishek.h@samsung.com>
Signed-off-by: Charles Kim <chulspro.kim@samsung.com>

* Restyled by isort

Signed-off-by: Charles Kim <chulspro.kim@samsung.com>

* ruff linting fix

Signed-off-by: Charles Kim <chulspro.kim@samsung.com>

* Don't wait for user inetraction in CI - check peer connection state instead for session establishment

Signed-off-by: Charles Kim <chulspro.kim@samsung.com>

* Exclude WEBRTC TCs from the CI.

Exclude WEBRTC TCs from the CI, until python webrtc bindings and
python controller changes are landed.

Signed-off-by: suyambu.rm <suyambu.rm@samsung.com>
Signed-off-by: Charles Kim <chulspro.kim@samsung.com>

* Update TCs based on controller refactor

Signed-off-by: Charles Kim <chulspro.kim@samsung.com>

* Restyled by prettier-yaml

Signed-off-by: Charles Kim <chulspro.kim@samsung.com>

* Allocate video stream based on values from DUT

Signed-off-by: Charles Kim <chulspro.kim@samsung.com>

---------

Signed-off-by: abhishek.h <abhishek.h@samsung.com>
Signed-off-by: Charles Kim <chulspro.kim@samsung.com>
Signed-off-by: suyambu.rm <suyambu.rm@samsung.com>
Co-authored-by: abhishek.h <abhishek.h@samsung.com>
Co-authored-by: Restyled.io <commits@restyled.io>
Co-authored-by: suyambu.rm <suyambu.rm@samsung.com>
diff --git a/src/python_testing/TC_WEBRTC_1_1.py b/src/python_testing/TC_WEBRTC_1_1.py
new file mode 100644
index 0000000..764efe7
--- /dev/null
+++ b/src/python_testing/TC_WEBRTC_1_1.py
@@ -0,0 +1,130 @@
+#
+#    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.
+
+from chip.ChipDeviceCtrl import TransportPayloadCapability
+from chip.clusters import WebRTCTransportProvider
+from chip.clusters.Types import NullValue
+from chip.testing.matter_testing import MatterBaseTest, TestStep, async_test_body, default_matter_test_main
+from chip.webrtc import PeerConnection, WebRTCManager
+from mobly import asserts
+from TC_WEBRTC_Utils import WebRTCTestHelper
+from test_plan_support import commission_if_required
+
+
+class TC_WEBRTC_1_1(MatterBaseTest, WebRTCTestHelper):
+    def steps_TC_WEBRTC_1_1(self) -> list[TestStep]:
+        steps = [
+            TestStep("precondition-1", commission_if_required(), is_commissioning=True),
+            TestStep("precondition-2", "Confirm no active WebRTC sessions exist in DUT"),
+            TestStep(1, "TH sends the ProvideOffer command with an SDP Offer and null WebRTCSessionID to the DUT."),
+            TestStep(2, "DUT sends Answer command to the TH/WEBRTCR."),
+            TestStep(3, "TH sends the SUCCESS status code to the DUT."),
+            TestStep(4, "TH sends the ICECandidates command with a its ICE candidates to the DUT."),
+            TestStep(5, "DUT sends ProvideICECandidates command to the TH/WEBRTCR."),
+            TestStep(6, "TH waits for 5 seconds. Verify the WebRTC session has been successfully established."),
+        ]
+
+        return steps
+
+    def desc_TC_WEBRTC_1_1(self) -> str:
+        return "[TC-WEBRTC-1.1] Validate that setting an SDP Offer successfully initiates a new WebRTC session."
+
+    def pics_TC_WEBRTC_1_1(self) -> list[str]:
+        return ["WEBRTCR", "WEBRTCP"]
+
+    @async_test_body
+    async def test_TC_WEBRTC_1_1(self):
+        self.step("precondition-1")
+
+        endpoint = self.get_endpoint(default=1)
+        webrtc_manager = WebRTCManager()
+        webrtc_peer: PeerConnection = webrtc_manager.create_peer(
+            node_id=self.dut_node_id, fabric_index=self.default_controller.GetFabricIndexInternal(), endpoint=endpoint
+        )
+
+        self.step("precondition-2")
+        current_sessions = await self.read_single_attribute_check_success(
+            cluster=WebRTCTransportProvider, attribute=WebRTCTransportProvider.Attributes.CurrentSessions, endpoint=endpoint
+        )
+        asserts.assert_equal(len(current_sessions), 0, "Found an existing WebRTC session")
+
+        # Allocate video stream in DUT if possible, to receive actual video stream
+        # This step is not part of test plan
+        aVideoStreamID = await self.allocate_video_stream(endpoint)
+
+        # Test Invokation
+
+        self.step(1)
+
+        offer = webrtc_peer.get_local_offer()
+
+        provide_offer_response: WebRTCTransportProvider.Commands.ProvideOfferResponse = await self.send_single_cmd(
+            cmd=WebRTCTransportProvider.Commands.ProvideOffer(
+                webRTCSessionID=NullValue,
+                sdp=offer,
+                streamUsage=WebRTCTransportProvider.Enums.StreamUsageEnum.kLiveView,
+                videoStreamID=aVideoStreamID,
+                audioStreamID=NullValue,
+                originatingEndpointID=1,
+            ),
+            endpoint=endpoint,
+            payloadCapability=TransportPayloadCapability.LARGE_PAYLOAD,
+        )
+        session_id = provide_offer_response.webRTCSessionID
+        asserts.assert_true(session_id >= 0, "Invalid response")
+
+        webrtc_manager.session_id_created(session_id, self.dut_node_id)
+
+        self.step(2)
+
+        answer_sessionId, answer = webrtc_peer.get_remote_answer()
+
+        asserts.assert_equal(session_id, answer_sessionId, "ProvideAnswer invoked with wrong session id")
+        asserts.assert_true(len(answer) > 0, "Invalid answer SDP received")
+
+        self.step(3)
+
+        webrtc_peer.set_remote_answer(answer)
+
+        self.step(4)
+        local_candidates = webrtc_peer.get_local_ice_candidates()
+
+        await self.send_single_cmd(
+            cmd=WebRTCTransportProvider.Commands.ProvideICECandidates(
+                webRTCSessionID=answer_sessionId, ICECandidates=local_candidates
+            ),
+            endpoint=endpoint,
+            payloadCapability=TransportPayloadCapability.LARGE_PAYLOAD,
+        )
+
+        self.step(5)
+        ice_session_id, remote_candidates = webrtc_peer.get_remote_ice_candidates()
+        asserts.assert_equal(session_id, ice_session_id, "ProvideIceCandidates invoked with wrong session id")
+        asserts.assert_true(len(remote_candidates) > 0, "Invalid remote ice candidates received")
+
+        webrtc_peer.set_remote_ice_candidates(remote_candidates)
+
+        self.step(6)
+        if not self.is_pics_sdk_ci_only and aVideoStreamID != NullValue:
+            self.wait_for_user_input("Verify WebRTC session is established")
+        else:
+            webrtc_peer.wait_for_session_establishment()
+
+        webrtc_manager.close_all()
+
+
+if __name__ == "__main__":
+    default_matter_test_main()
diff --git a/src/python_testing/TC_WEBRTC_1_2.py b/src/python_testing/TC_WEBRTC_1_2.py
new file mode 100644
index 0000000..d8533e8
--- /dev/null
+++ b/src/python_testing/TC_WEBRTC_1_2.py
@@ -0,0 +1,167 @@
+#
+#    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.
+
+from chip.ChipDeviceCtrl import TransportPayloadCapability
+from chip.clusters import WebRTCTransportProvider
+from chip.clusters.Types import NullValue
+from chip.testing.matter_testing import MatterBaseTest, TestStep, async_test_body, default_matter_test_main
+from chip.webrtc import PeerConnection, WebRTCManager
+from mobly import asserts
+from test_plan_support import commission_if_required
+
+
+class TC_WEBRTC_1_2(MatterBaseTest):
+    def steps_TC_WEBRTC_1_2(self) -> list[TestStep]:
+        steps = [
+            TestStep("precondition-1", commission_if_required(), is_commissioning=True),
+            TestStep("precondition-2", "Confirm there is an active WebRTC sessions exist in DUT"),
+            TestStep(0, "TH Reads CurrentSessions attribute from WEBRTCP (DUT)"),
+            TestStep(1, "TH sends the ProvideOffer command with an SDP Offer with the WebRTCSessionID saved in step 1 to the DUT."),
+            TestStep(2, "TH waits up to 30 seconds for Answer command from the DUT."),
+            TestStep(3, "TH sends the SUCCESS status code to the DUT."),
+            TestStep(4, "TH sends the ICECandidates command with a its ICE candidates to the DUT."),
+            TestStep(5, "TH waits up to 30 seconds for ProvideICECandidates command from the DUT."),
+            TestStep(6, "TH waits for 10 seconds."),
+            TestStep(7, "TH sends the EndSession command with the WebRTCSessionID saved in step 1 to the DUT."),
+        ]
+
+        return steps
+
+    def desc_TC_WEBRTC_1_2(self) -> str:
+        return "[TC-WEBRTC-1.2] Validate that providing an existing WebRTC session ID with an SDP Offer successfully triggers the re-offer flow."
+
+    def pics_TC_WEBRTC_1_2(self) -> list[str]:
+        return ["WEBRTCR", "WEBRTCP"]
+
+    @async_test_body
+    async def test_TC_WEBRTC_1_2(self):
+        self.step("precondition-1")
+
+        endpoint = self.get_endpoint(default=1)
+        webrtc_manager = WebRTCManager()
+        webrtc_peer: PeerConnection = webrtc_manager.create_peer(
+            node_id=self.dut_node_id, fabric_index=self.default_controller.GetFabricIndexInternal(), endpoint=endpoint
+        )
+        # Test Invokation
+        self.step("precondition-2")
+        await establish_webrtc_session(webrtc_manager, webrtc_peer, endpoint, self)
+
+        self.step(0)
+        current_sessions = await self.read_single_attribute_check_success(
+            cluster=WebRTCTransportProvider, attribute=WebRTCTransportProvider.Attributes.CurrentSessions, endpoint=endpoint
+        )
+        asserts.assert_equal(len(current_sessions), 1, "No Pre-Existing session found")
+        prev_sessionid = current_sessions[0].id
+
+        self.step(1)
+        offer = webrtc_peer.get_local_offer()
+
+        provide_offer_response: WebRTCTransportProvider.Commands.ProvideOfferResponse = await self.send_single_cmd(
+            cmd=WebRTCTransportProvider.Commands.ProvideOffer(
+                webRTCSessionID=prev_sessionid,
+                sdp=offer,
+                streamUsage=WebRTCTransportProvider.Enums.StreamUsageEnum.kLiveView,
+                videoStreamID=NullValue,
+                audioStreamID=NullValue,
+                originatingEndpointID=1,
+            ),
+            endpoint=endpoint,
+            payloadCapability=TransportPayloadCapability.LARGE_PAYLOAD,
+        )
+
+        asserts.assert_equal(
+            provide_offer_response.webRTCSessionID, prev_sessionid, "Session id does not match with the previous session"
+        )
+
+        self.step(2)
+        answer_sessionId, answer = webrtc_peer.get_remote_answer(timeout=30)
+
+        asserts.assert_equal(prev_sessionid, answer_sessionId, "Session id does not match with the previous session")
+        asserts.assert_true(len(answer) > 0, "Invalid answer SDP received")
+
+        self.step(3)
+        webrtc_peer.set_remote_answer(answer)
+
+        self.step(4)
+        local_candidates = webrtc_peer.get_local_ice_candidates()
+
+        await self.send_single_cmd(
+            cmd=WebRTCTransportProvider.Commands.ProvideICECandidates(
+                webRTCSessionID=answer_sessionId, ICECandidates=local_candidates
+            ),
+            endpoint=endpoint,
+            payloadCapability=TransportPayloadCapability.LARGE_PAYLOAD,
+        )
+
+        self.step(5)
+        ice_session_id, remote_candidates = webrtc_peer.get_remote_ice_candidates()
+        asserts.assert_equal(prev_sessionid, ice_session_id, "ProvideIceCandidates invoked with wrong session id")
+        asserts.assert_true(len(remote_candidates) > 0, "Invalid remote ice candidates received")
+        webrtc_peer.set_remote_ice_candidates(remote_candidates)
+
+        self.step(6)
+        if not self.is_pics_sdk_ci_only:
+            self.wait_for_user_input("Verify WebRTC session is established")
+        else:
+            webrtc_peer.wait_for_session_establishment()
+
+        self.step(7)
+        await self.send_single_cmd(
+            cmd=WebRTCTransportProvider.Commands.EndSession(
+                webRTCSessionID=prev_sessionid, reason=WebRTCTransportProvider.Enums.WebRTCEndReasonEnum.kUserHangup
+            ),
+            endpoint=endpoint,
+            payloadCapability=TransportPayloadCapability.LARGE_PAYLOAD,
+        )
+
+        webrtc_manager.close_all()
+
+
+async def establish_webrtc_session(webrtc_manager, webrtc_peer, endpoint, ctrl):
+    offer = webrtc_peer.get_local_offer()
+    provide_offer_response: WebRTCTransportProvider.Commands.ProvideOfferResponse = await ctrl.send_single_cmd(
+        cmd=WebRTCTransportProvider.Commands.ProvideOffer(
+            webRTCSessionID=NullValue,
+            sdp=offer,
+            streamUsage=WebRTCTransportProvider.Enums.StreamUsageEnum.kLiveView,
+            videoStreamID=NullValue,
+            audioStreamID=NullValue,
+            originatingEndpointID=1,
+        ),
+        endpoint=endpoint,
+        payloadCapability=TransportPayloadCapability.LARGE_PAYLOAD,
+    )
+    asserts.assert_true(provide_offer_response.webRTCSessionID >= 0, "Invalid response")
+    webrtc_manager.session_id_created(provide_offer_response.webRTCSessionID, ctrl.dut_node_id)
+
+    answer_sessionId, answer = webrtc_peer.get_remote_answer(timeout=30)
+    webrtc_peer.set_remote_answer(answer)
+
+    local_candidates = webrtc_peer.get_local_ice_candidates()
+    await ctrl.send_single_cmd(
+        cmd=WebRTCTransportProvider.Commands.ProvideICECandidates(webRTCSessionID=answer_sessionId, ICECandidates=local_candidates),
+        endpoint=endpoint,
+        payloadCapability=TransportPayloadCapability.LARGE_PAYLOAD,
+    )
+
+    ice_session_id, remote_candidates = webrtc_peer.get_remote_ice_candidates()
+    webrtc_peer.set_remote_ice_candidates(remote_candidates)
+
+    return webrtc_peer.wait_for_session_establishment()
+
+
+if __name__ == "__main__":
+    default_matter_test_main()
diff --git a/src/python_testing/TC_WEBRTC_1_3.py b/src/python_testing/TC_WEBRTC_1_3.py
new file mode 100644
index 0000000..e14f6d9
--- /dev/null
+++ b/src/python_testing/TC_WEBRTC_1_3.py
@@ -0,0 +1,130 @@
+#
+#    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.
+
+from chip.ChipDeviceCtrl import TransportPayloadCapability
+from chip.clusters import WebRTCTransportProvider
+from chip.clusters.Types import NullValue
+from chip.testing.matter_testing import MatterBaseTest, TestStep, async_test_body, default_matter_test_main
+from chip.webrtc import PeerConnection, WebRTCManager
+from mobly import asserts
+from test_plan_support import commission_if_required
+
+
+class TC_WEBRTC_1_3(MatterBaseTest):
+    def steps_TC_WEBRTC_1_3(self) -> list[TestStep]:
+        steps = [
+            TestStep("precondition-1", commission_if_required(), is_commissioning=True),
+            TestStep("precondition-2", "Confirm no active WebRTC sessions exist in DUT"),
+            TestStep(1, "TH sends the SolicitOffer command without ICEServers and ICETransportPolicy to the DUT."),
+            TestStep(2, "TH waits up to 30 seconds for Offer command from the DUT."),
+            TestStep(3, "TH sends the SUCCESS status code to the DUT."),
+            TestStep(4, "TH sends the ProvideAnswer command with the WebRTCSessionID saved in step 1 and a SDP Offer to the DUT."),
+            TestStep(5, "TH sends the ICECandidates command with a its ICE candidates to the DUT."),
+            TestStep(6, "TH waits up to 30 seconds for ProvideICECandidates command from the DUT."),
+            TestStep(7, "TH waits for 10 seconds."),
+            TestStep(8, "TH sends the EndSession command with the WebRTCSessionID saved in step 1 to the DUT."),
+        ]
+
+        return steps
+
+    def desc_TC_WEBRTC_1_3(self) -> str:
+        return "[TC-WEBRTC-1.3] Validate Deferred Offer Flow for Battery-Powered Camera in Standby Mode."
+
+    def pics_TC_WEBRTC_1_3(self) -> list[str]:
+        return ["WEBRTCR", "WEBRTCP"]
+
+    @async_test_body
+    async def test_TC_WEBRTC_1_3(self):
+        self.step("precondition-1")
+
+        endpoint = self.get_endpoint(default=1)
+        webrtc_manager = WebRTCManager()
+        webrtc_peer: PeerConnection = webrtc_manager.create_peer(
+            node_id=self.dut_node_id, fabric_index=self.default_controller.GetFabricIndexInternal(), endpoint=endpoint
+        )
+
+        self.step("precondition-2")
+        current_sessions = await self.read_single_attribute_check_success(
+            cluster=WebRTCTransportProvider, attribute=WebRTCTransportProvider.Attributes.CurrentSessions, endpoint=endpoint
+        )
+        asserts.assert_equal(len(current_sessions), 0, "Found an existing WebRTC session")
+
+        self.step(1)
+        solicit_offer_response: WebRTCTransportProvider.Commands.SolicitOfferResponse = await self.send_single_cmd(
+            cmd=WebRTCTransportProvider.Commands.SolicitOffer(
+                streamUsage=WebRTCTransportProvider.Enums.StreamUsageEnum.kLiveView,
+                videoStreamID=NullValue,
+                audioStreamID=NullValue,
+                originatingEndpointID=1,
+            ),
+            endpoint=endpoint,
+            payloadCapability=TransportPayloadCapability.LARGE_PAYLOAD,
+        )
+        session_id = solicit_offer_response.webRTCSessionID
+        asserts.assert_true(session_id >= 0, "Invalid response")
+        asserts.assert_true(solicit_offer_response.deferredOffer, "Expected deferredOffer = True")
+        webrtc_manager.session_id_created(session_id, self.dut_node_id)
+
+        self.step(2)
+        offer_sessionId, remote_offer_sdp = webrtc_peer.get_remote_offer(timeout=30)
+        asserts.assert_equal(offer_sessionId, session_id, "Invalid session id")
+        asserts.assert_true(len(remote_offer_sdp) > 0, "Invalid offer sdp received")
+
+        self.step(3)
+        webrtc_peer.set_remote_offer(remote_offer_sdp)
+
+        self.step(4)
+        local_answer = webrtc_peer.get_local_answer()
+        await self.send_single_cmd(
+            cmd=WebRTCTransportProvider.Commands.ProvideAnswer(webRTCSessionID=session_id, sdp=local_answer),
+            endpoint=endpoint,
+            payloadCapability=TransportPayloadCapability.LARGE_PAYLOAD,
+        )
+
+        self.step(5)
+        local_candidates = webrtc_peer.get_local_ice_candidates()
+        await self.send_single_cmd(
+            cmd=WebRTCTransportProvider.Commands.ProvideICECandidates(webRTCSessionID=session_id, ICECandidates=local_candidates),
+            endpoint=endpoint,
+            payloadCapability=TransportPayloadCapability.LARGE_PAYLOAD,
+        )
+
+        self.step(6)
+        ice_session_id, remote_candidates = webrtc_peer.get_remote_ice_candidates(timeout=30)
+        asserts.assert_equal(ice_session_id, session_id, "Invalid session id")
+        asserts.assert_true(len(remote_candidates) > 0, "Invalid ice candidates received")
+        webrtc_peer.set_remote_ice_candidates(remote_candidates)
+
+        self.step(7)
+        if not self.is_pics_sdk_ci_only:
+            self.wait_for_user_input("Verify WebRTC session is established")
+        else:
+            webrtc_peer.wait_for_session_establishment()
+
+        self.step(8)
+        await self.send_single_cmd(
+            cmd=WebRTCTransportProvider.Commands.EndSession(
+                webRTCSessionID=session_id, reason=WebRTCTransportProvider.Enums.WebRTCEndReasonEnum.kUserHangup
+            ),
+            endpoint=endpoint,
+            payloadCapability=TransportPayloadCapability.LARGE_PAYLOAD,
+        )
+
+        webrtc_manager.close_all()
+
+
+if __name__ == "__main__":
+    default_matter_test_main()
diff --git a/src/python_testing/TC_WEBRTC_1_4.py b/src/python_testing/TC_WEBRTC_1_4.py
new file mode 100644
index 0000000..3bf02d2
--- /dev/null
+++ b/src/python_testing/TC_WEBRTC_1_4.py
@@ -0,0 +1,133 @@
+#
+#    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.
+
+
+from chip.ChipDeviceCtrl import TransportPayloadCapability
+from chip.clusters import WebRTCTransportProvider
+from chip.clusters.Types import NullValue
+from chip.testing.matter_testing import MatterBaseTest, TestStep, async_test_body, default_matter_test_main
+from chip.webrtc import PeerConnection, WebRTCManager
+from mobly import asserts
+from test_plan_support import commission_if_required
+
+
+class TC_WEBRTC_1_4(MatterBaseTest):
+    def steps_TC_WEBRTC_1_4(self) -> list[TestStep]:
+        steps = [
+            TestStep("precondition-1", commission_if_required(), is_commissioning=True),
+            TestStep("precondition-2", "Confirm no active WebRTC sessions exist in DUT"),
+            TestStep(1, "TH sends the SolicitOffer command without ICEServers and ICETransportPolicy to the DUT."),
+            TestStep(2, "DUT sends Offer command to TH/WEBRTCR."),
+            TestStep(3, "TH sends the SUCCESS status code to the DUT."),
+            TestStep(4, "TH sends the ProvideAnswer command with the WebRTCSessionID saved in step 1 and a SDP Offer to the DUT."),
+            TestStep(5, "TH sends the ICECandidates command with a its ICE candidates to the DUT."),
+            TestStep(6, "TH waits up to 30 seconds for ProvideICECandidates command from the DUT."),
+            TestStep(7, "TH waits for 10 seconds."),
+            TestStep(8, "TH sends the EndSession command with the WebRTCSessionID saved in step 1 to the DUT."),
+        ]
+
+        return steps
+
+    def desc_TC_WEBRTC_1_4(self) -> str:
+        return "[TC-WEBRTC-1.3] Validate Deferred Offer Flow for Battery-Powered Camera in Standby Mode."
+
+    def pics_TC_WEBRTC_1_4(self) -> list[str]:
+        return ["WEBRTCR", "WEBRTCP"]
+
+    @async_test_body
+    async def test_TC_WEBRTC_1_4(self):
+        self.step("precondition-1")
+
+        endpoint = self.get_endpoint(default=1)
+        webrtc_manager = WebRTCManager()
+        webrtc_peer: PeerConnection = webrtc_manager.create_peer(
+            node_id=self.dut_node_id, fabric_index=self.default_controller.GetFabricIndexInternal(), endpoint=endpoint
+        )
+
+        self.step("precondition-2")
+        current_sessions = await self.read_single_attribute_check_success(
+            cluster=WebRTCTransportProvider, attribute=WebRTCTransportProvider.Attributes.CurrentSessions, endpoint=endpoint
+        )
+        asserts.assert_equal(len(current_sessions), 0, "Found an existing WebRTC session")
+
+        self.step(1)
+        solicit_offer_response: WebRTCTransportProvider.Commands.SolicitOfferResponse = await self.send_single_cmd(
+            cmd=WebRTCTransportProvider.Commands.SolicitOffer(
+                streamUsage=WebRTCTransportProvider.Enums.StreamUsageEnum.kLiveView,
+                videoStreamID=NullValue,
+                audioStreamID=NullValue,
+                originatingEndpointID=1,
+            ),
+            endpoint=endpoint,
+            payloadCapability=TransportPayloadCapability.LARGE_PAYLOAD,
+        )
+        session_id = solicit_offer_response.webRTCSessionID
+        asserts.assert_true(session_id >= 0, "Invalid response")
+        asserts.assert_false(solicit_offer_response.deferredOffer, "Expected deferredOffer = False")
+
+        webrtc_manager.session_id_created(session_id, self.dut_node_id)
+
+        self.step(2)
+        # TH should immediately get Offer from DUT
+        offer_sessionId, remote_offer_sdp = webrtc_peer.get_remote_offer(timeout=5)
+        asserts.assert_equal(offer_sessionId, session_id, "Invalid session id")
+        asserts.assert_true(len(remote_offer_sdp) > 0, "Invalid offer sdp received")
+
+        self.step(3)
+        webrtc_peer.set_remote_offer(remote_offer_sdp)
+
+        self.step(4)
+        local_answer = webrtc_peer.get_local_answer()
+        await self.send_single_cmd(
+            cmd=WebRTCTransportProvider.Commands.ProvideAnswer(webRTCSessionID=session_id, sdp=local_answer),
+            endpoint=endpoint,
+            payloadCapability=TransportPayloadCapability.LARGE_PAYLOAD,
+        )
+
+        self.step(5)
+        local_candidates = webrtc_peer.get_local_ice_candidates()
+        await self.send_single_cmd(
+            cmd=WebRTCTransportProvider.Commands.ProvideICECandidates(webRTCSessionID=session_id, ICECandidates=local_candidates),
+            endpoint=endpoint,
+            payloadCapability=TransportPayloadCapability.LARGE_PAYLOAD,
+        )
+
+        self.step(6)
+        ice_session_id, remote_candidates = webrtc_peer.get_remote_ice_candidates(timeout=30)
+        asserts.assert_equal(ice_session_id, session_id, "Invalid session id")
+        asserts.assert_true(len(remote_candidates) > 0, "Invalid ice candidates received")
+        webrtc_peer.set_remote_ice_candidates(remote_candidates)
+
+        self.step(7)
+        if not self.is_pics_sdk_ci_only:
+            self.wait_for_user_input("Verify WebRTC session is established")
+        else:
+            webrtc_peer.wait_for_session_establishment()
+
+        self.step(8)
+        await self.send_single_cmd(
+            cmd=WebRTCTransportProvider.Commands.EndSession(
+                webRTCSessionID=session_id, reason=WebRTCTransportProvider.Enums.WebRTCEndReasonEnum.kUserHangup
+            ),
+            endpoint=endpoint,
+            payloadCapability=TransportPayloadCapability.LARGE_PAYLOAD,
+        )
+
+        webrtc_manager.close_all()
+
+
+if __name__ == "__main__":
+    default_matter_test_main()
diff --git a/src/python_testing/TC_WEBRTC_1_5.py b/src/python_testing/TC_WEBRTC_1_5.py
new file mode 100644
index 0000000..0fc945f
--- /dev/null
+++ b/src/python_testing/TC_WEBRTC_1_5.py
@@ -0,0 +1,113 @@
+#
+#    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.
+
+
+from chip.ChipDeviceCtrl import TransportPayloadCapability
+from chip.clusters import WebRTCTransportProvider
+from chip.testing.matter_testing import MatterBaseTest, TestStep, async_test_body, default_matter_test_main
+from chip.webrtc import PeerConnection, WebRTCManager
+from mobly import asserts
+from test_plan_support import commission_if_required
+
+
+class TC_WEBRTC_1_5(MatterBaseTest):
+    def steps_TC_WEBRTC_1_5(self) -> list[TestStep]:
+        steps = [
+            TestStep("precondition-1", commission_if_required(), is_commissioning=True),
+            TestStep("precondition-2", "Confirm no active WebRTC sessions exist in DUT"),
+            TestStep(1, "Follow manufacturer provided instructions to have DUT send the Offer command to the TH."),
+            TestStep(2, "TH sends the SUCCESS status code to the DUT."),
+            TestStep(3, "TH sends the ProvideAnswer command with the WebRTCSessionID saved in step 1 and a SDP Offer to the DUT."),
+            TestStep(4, "TH sends the ICECandidates command with a its ICE candidates to the DUT."),
+            TestStep(5, "DUT sends ProvideICECandidates command to TH/WEBRTCR."),
+            TestStep(6, "TH waits for 5 seconds."),
+            TestStep(7, "TH sends the EndSession command with the WebRTCSessionID saved in step 1 to the DUT."),
+        ]
+
+        return steps
+
+    def desc_TC_WEBRTC_1_5(self) -> str:
+        return "[TC-WEBRTC-1.3] Validate Deferred Offer Flow for Battery-Powered Camera in Standby Mode."
+
+    def pics_TC_WEBRTC_1_5(self) -> list[str]:
+        return ["WEBRTCR", "WEBRTCP"]
+
+    @async_test_body
+    async def test_TC_WEBRTC_1_5(self):
+        self.step("precondition-1")
+        endpoint = self.get_endpoint(default=1)
+        webrtc_manager = WebRTCManager()
+        webrtc_peer: PeerConnection = webrtc_manager.create_peer(
+            node_id=self.dut_node_id, fabric_index=self.default_controller.GetFabricIndexInternal(), endpoint=endpoint
+        )
+
+        self.step("precondition-2")
+        current_sessions = await self.read_single_attribute_check_success(
+            cluster=WebRTCTransportProvider, attribute=WebRTCTransportProvider.Attributes.CurrentSessions, endpoint=endpoint
+        )
+        asserts.assert_equal(len(current_sessions), 0, "Found an existing WebRTC session")
+
+        self.step(1)
+        session_id, remote_offer_sdp = webrtc_peer.get_remote_offer(timeout=30)
+        asserts.assert_true(session_id >= 0, "Invalid session id")
+        asserts.assert_true(len(remote_offer_sdp) > 0, "Invalid offer sdp received")
+        webrtc_manager.session_id_created(session_id, self.dut_node_id)
+
+        self.step(2)
+        webrtc_peer.set_remote_offer(remote_offer_sdp)
+
+        self.step(3)
+        local_answer = webrtc_peer.get_local_answer()
+        await self.send_single_cmd(
+            cmd=WebRTCTransportProvider.Commands.ProvideAnswer(webRTCSessionID=session_id, sdp=local_answer),
+            endpoint=endpoint,
+            payloadCapability=TransportPayloadCapability.LARGE_PAYLOAD,
+        )
+
+        self.step(4)
+        local_candidates = webrtc_peer.get_local_ice_candidates()
+        await self.send_single_cmd(
+            cmd=WebRTCTransportProvider.Commands.ProvideICECandidates(webRTCSessionID=session_id, ICECandidates=local_candidates),
+            endpoint=endpoint,
+            payloadCapability=TransportPayloadCapability.LARGE_PAYLOAD,
+        )
+
+        self.step(5)
+        ice_session_id, remote_candidates = webrtc_peer.get_remote_ice_candidates(timeout=30)
+        asserts.assert_equal(ice_session_id, session_id, "Invalid session id")
+        asserts.assert_true(len(remote_candidates) > 0, "Invalid ice candidates received")
+        webrtc_peer.set_remote_ice_candidates(remote_candidates)
+
+        self.step(6)
+        if not self.is_pics_sdk_ci_only:
+            self.wait_for_user_input("Verify WebRTC session is established")
+        else:
+            webrtc_peer.wait_for_session_establishment()
+
+        self.step(7)
+        await self.send_single_cmd(
+            cmd=WebRTCTransportProvider.Commands.EndSession(
+                webRTCSessionID=session_id, reason=WebRTCTransportProvider.Enums.WebRTCEndReasonEnum.kUserHangup
+            ),
+            endpoint=endpoint,
+            payloadCapability=TransportPayloadCapability.LARGE_PAYLOAD,
+        )
+
+        webrtc_manager.close_all()
+
+
+if __name__ == "__main__":
+    default_matter_test_main()
diff --git a/src/python_testing/TC_WEBRTC_Utils.py b/src/python_testing/TC_WEBRTC_Utils.py
new file mode 100644
index 0000000..a79bcac
--- /dev/null
+++ b/src/python_testing/TC_WEBRTC_Utils.py
@@ -0,0 +1,68 @@
+#
+#    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.
+
+import logging
+
+from chip.clusters import CameraAvStreamManagement
+from chip.clusters.Types import NullValue
+
+
+class WebRTCTestHelper:
+    async def read_avstr_attribute_expect_success(self, endpoint, attribute):
+        return await self.read_single_attribute_check_success(
+            endpoint=endpoint, cluster=CameraAvStreamManagement, attribute=attribute
+        )
+
+    async def allocate_video_stream(self, endpoint):
+        attrs = CameraAvStreamManagement.Attributes
+        try:
+            # Check for watermark and OSD features
+            feature_map = await self.read_avstr_attribute_expect_success(endpoint, attrs.FeatureMap)
+            watermark = True if (feature_map & CameraAvStreamManagement.Bitmaps.Feature.kWatermark) != 0 else None
+            osd = True if (feature_map & CameraAvStreamManagement.Bitmaps.Feature.kOnScreenDisplay) != 0 else None
+
+            # Get the parms from the device (those which are available)
+            aStreamUsagePriorities = await self.read_avstr_attribute_expect_success(endpoint, attrs.StreamUsagePriorities)
+            aRateDistortionTradeOffPoints = await self.read_avstr_attribute_expect_success(
+                endpoint, attrs.RateDistortionTradeOffPoints
+            )
+            aMinViewport = await self.read_avstr_attribute_expect_success(endpoint, attrs.MinViewport)
+            aVideoSensorParams = await self.read_avstr_attribute_expect_success(endpoint, attrs.VideoSensorParams)
+
+            response = await self.send_single_cmd(
+                cmd=CameraAvStreamManagement.Commands.VideoStreamAllocate(
+                    streamUsage=aStreamUsagePriorities[0],
+                    videoCodec=aRateDistortionTradeOffPoints[0].codec,
+                    minFrameRate=30,
+                    maxFrameRate=aVideoSensorParams.maxFPS,
+                    minResolution=aMinViewport,
+                    maxResolution=CameraAvStreamManagement.Structs.VideoResolutionStruct(
+                        width=aVideoSensorParams.sensorWidth, height=aVideoSensorParams.sensorHeight
+                    ),
+                    minBitRate=aRateDistortionTradeOffPoints[0].minBitRate,
+                    maxBitRate=aRateDistortionTradeOffPoints[0].minBitRate,
+                    minKeyFrameInterval=2000,
+                    maxKeyFrameInterval=8000,
+                    watermarkEnabled=watermark,
+                    OSDEnabled=osd,
+                ),
+                endpoint=endpoint,
+            )
+            return response.videoStreamID
+
+        except Exception as e:
+            logging.error(f"Failed to allocate video stream. {e}")
+            return NullValue
diff --git a/src/python_testing/test_metadata.yaml b/src/python_testing/test_metadata.yaml
index afd9131..49100bc 100644
--- a/src/python_testing/test_metadata.yaml
+++ b/src/python_testing/test_metadata.yaml
@@ -123,6 +123,28 @@
     - name: TC_AVSUMTestBase.py
       reason:
           Shared code for Camera AV Settings (TC_AVSUM*), not a standalone test.
+    - name: TC_WEBRTC_1_1.py
+      reason:
+          Depends on Python WebRTC bindings. Will be enabled once local tests
+          are fully verified.
+    - name: TC_WEBRTC_1_2.py
+      reason:
+          Depends on Python WebRTC bindings. Will be enabled once local tests
+          are fully verified.
+    - name: TC_WEBRTC_1_3.py
+      reason:
+          Depends on Python WebRTC bindings. Will be enabled once local tests
+          are fully verified.
+    - name: TC_WEBRTC_1_4.py
+      reason:
+          Depends on Python WebRTC bindings. Will be enabled once local tests
+          are fully verified.
+    - name: TC_WEBRTC_1_5.py
+      reason:
+          Depends on Python WebRTC bindings. Will be enabled once local tests
+          are fully verified.
+    - name: TC_WEBRTC_Utils.py
+      reason: Helper methods for WebRTC TCs, not a standalone test.
 
 # This is a list of slow tests (just arbitrarily picked around 20 seconds)
 # used in some script reporting for "be patient" messages as well as potentially