Add min event filter to chip-repl ReadEvent (#23657)

* Add min event filter to chip-repl ReadEvent

* Add test to chip-repl

* Minor fix while pulling in master branch
diff --git a/src/controller/python/chip/ChipDeviceCtrl.py b/src/controller/python/chip/ChipDeviceCtrl.py
index 96ccacc..9a49296 100644
--- a/src/controller/python/chip/ChipDeviceCtrl.py
+++ b/src/controller/python/chip/ChipDeviceCtrl.py
@@ -1016,7 +1016,7 @@
         typing.Tuple[int,
                      typing.Type[ClusterObjects.ClusterEvent], int]
     ]] = None,
-            returnClusterObject: bool = False, reportInterval: typing.Tuple[int, int] = None, fabricFiltered: bool = True, keepSubscriptions: bool = False):
+            eventNumberFilter: typing.Optional[int] = None, returnClusterObject: bool = False, reportInterval: typing.Tuple[int, int] = None, fabricFiltered: bool = True, keepSubscriptions: bool = False):
         '''
         Read a list of attributes and/or events from a target node
 
@@ -1046,6 +1046,8 @@
             Clusters.ClusterA:                          Endpoint = *,           Cluster = specific,   Event = *, Urgent = True/False
             '*' or ():                                  Endpoint = *,           Cluster = *,          Event = *, Urgent = True/False
 
+        eventNumberFilter: Optional minimum event number filter.
+
         returnClusterObject: This returns the data as consolidated cluster objects, with all attributes for a cluster inside
                              a single cluster-wide cluster object.
 
@@ -1065,7 +1067,7 @@
         eventPaths = [self._parseEventPathTuple(
             v) for v in events] if events else None
 
-        ClusterAttribute.Read(future=future, eventLoop=eventLoop, device=device.deviceProxy, devCtrl=self, attributes=attributePaths, dataVersionFilters=clusterDataVersionFilters, events=eventPaths, returnClusterObject=returnClusterObject,
+        ClusterAttribute.Read(future=future, eventLoop=eventLoop, device=device.deviceProxy, devCtrl=self, attributes=attributePaths, dataVersionFilters=clusterDataVersionFilters, events=eventPaths, eventNumberFilter=eventNumberFilter, returnClusterObject=returnClusterObject,
                               subscriptionParameters=ClusterAttribute.SubscriptionParameters(reportInterval[0], reportInterval[1]) if reportInterval else None, fabricFiltered=fabricFiltered, keepSubscriptions=keepSubscriptions).raise_on_error()
         return await future
 
@@ -1124,7 +1126,7 @@
         typing.Tuple[int, typing.Type[ClusterObjects.Cluster], int],
         # Concrete path
         typing.Tuple[int, typing.Type[ClusterObjects.ClusterEvent], int]
-    ]], reportInterval: typing.Tuple[int, int] = None, keepSubscriptions: bool = False):
+    ]], eventNumberFilter: typing.Optional[int] = None, reportInterval: typing.Tuple[int, int] = None, keepSubscriptions: bool = False):
         '''
         Read a list of events from a target node, this is a wrapper of DeviceController.Read()
 
@@ -1144,10 +1146,11 @@
             ReadEvent(1, [ Clusters.Basic ] ) -- case 5 above.
             ReadEvent(1, [ (1, Clusters.Basic.Events.Location ] ) -- case 1 above.
 
+        eventNumberFilter: Optional minimum event number filter.
         reportInterval: A tuple of two int-s for (MinIntervalFloor, MaxIntervalCeiling). Used by establishing subscriptions.
             When not provided, a read request will be sent.
         '''
-        res = await self.Read(nodeid=nodeid, events=events, reportInterval=reportInterval, keepSubscriptions=keepSubscriptions)
+        res = await self.Read(nodeid=nodeid, events=events, eventNumberFilter=eventNumberFilter, reportInterval=reportInterval, keepSubscriptions=keepSubscriptions)
         if isinstance(res, ClusterAttribute.SubscriptionTransaction):
             return res
         else:
diff --git a/src/controller/python/chip/clusters/Attribute.py b/src/controller/python/chip/clusters/Attribute.py
index 795f9ba..68499a5 100644
--- a/src/controller/python/chip/clusters/Attribute.py
+++ b/src/controller/python/chip/clusters/Attribute.py
@@ -22,7 +22,7 @@
 from asyncio.futures import Future
 import ctypes
 from dataclasses import dataclass, field
-from typing import Tuple, Type, Union, List, Any, Callable, Dict, Set
+from typing import Tuple, Type, Union, List, Any, Callable, Dict, Set, Optional
 from ctypes import CFUNCTYPE, c_char_p, c_size_t, c_void_p, c_uint64, c_uint32,  c_uint16, c_uint8, py_object, c_uint64
 import construct
 from rich.pretty import pprint
@@ -952,7 +952,7 @@
 )
 
 
-def Read(future: Future, eventLoop, device, devCtrl, attributes: List[AttributePath] = None, dataVersionFilters: List[DataVersionFilter] = None, events: List[EventPath] = None, returnClusterObject: bool = True, subscriptionParameters: SubscriptionParameters = None, fabricFiltered: bool = True, keepSubscriptions: bool = False) -> PyChipError:
+def Read(future: Future, eventLoop, device, devCtrl, attributes: List[AttributePath] = None, dataVersionFilters: List[DataVersionFilter] = None, events: List[EventPath] = None, eventNumberFilter: Optional[int] = None, returnClusterObject: bool = True, subscriptionParameters: SubscriptionParameters = None, fabricFiltered: bool = True, keepSubscriptions: bool = False) -> PyChipError:
     if (not attributes) and dataVersionFilters:
         raise ValueError(
             "Must provide valid attribute list when data version filters is not null")
@@ -1032,6 +1032,9 @@
         params.KeepSubscriptions = keepSubscriptions
     params.IsFabricFiltered = fabricFiltered
     params = _ReadParams.build(params)
+    eventNumberFilterPtr = ctypes.POINTER(ctypes.c_ulonglong)()
+    if eventNumberFilter is not None:
+        eventNumberFilterPtr = ctypes.POINTER(ctypes.c_ulonglong)(ctypes.c_ulonglong(eventNumberFilter))
 
     res = builtins.chipStack.Call(
         lambda: handle.pychip_ReadClient_Read(
@@ -1044,6 +1047,7 @@
             ctypes.c_size_t(
                 0 if dataVersionFilters is None else len(dataVersionFilters)),
             ctypes.c_size_t(0 if events is None else len(events)),
+            eventNumberFilterPtr,
             *readargs))
 
     transaction.SetClientObjPointers(readClientObj, readCallbackObj)
@@ -1057,8 +1061,8 @@
     return Read(future=future, eventLoop=eventLoop, device=device, devCtrl=devCtrl, attributes=attributes, dataVersionFilters=dataVersionFilters, events=None, returnClusterObject=returnClusterObject, subscriptionParameters=subscriptionParameters, fabricFiltered=fabricFiltered)
 
 
-def ReadEvents(future: Future, eventLoop, device, devCtrl, events: List[EventPath], returnClusterObject: bool = True, subscriptionParameters: SubscriptionParameters = None, fabricFiltered: bool = True) -> int:
-    return Read(future=future, eventLoop=eventLoop, device=device, devCtrl=devCtrl, attributes=None, dataVersionFilters=None, events=events, returnClusterObject=returnClusterObject, subscriptionParameters=subscriptionParameters, fabricFiltered=fabricFiltered)
+def ReadEvents(future: Future, eventLoop, device, devCtrl, events: List[EventPath], eventNumberFilter=None, returnClusterObject: bool = True, subscriptionParameters: SubscriptionParameters = None, fabricFiltered: bool = True) -> int:
+    return Read(future=future, eventLoop=eventLoop, device=device, devCtrl=devCtrl, attributes=None, dataVersionFilters=None, events=events, eventNumberFilter=eventNumberFilter, returnClusterObject=returnClusterObject, subscriptionParameters=subscriptionParameters, fabricFiltered=fabricFiltered)
 
 
 def Init():
diff --git a/src/controller/python/chip/clusters/attribute.cpp b/src/controller/python/chip/clusters/attribute.cpp
index b847a20..be533e7 100644
--- a/src/controller/python/chip/clusters/attribute.cpp
+++ b/src/controller/python/chip/clusters/attribute.cpp
@@ -391,7 +391,7 @@
 
 PyChipError pychip_ReadClient_Read(void * appContext, ReadClient ** pReadClient, ReadClientCallback ** pCallback,
                                    DeviceProxy * device, uint8_t * readParamsBuf, size_t numAttributePaths,
-                                   size_t numDataversionFilters, size_t numEventPaths, ...)
+                                   size_t numDataversionFilters, size_t numEventPaths, uint64_t * eventNumberFilter, ...)
 {
     CHIP_ERROR err                 = CHIP_NO_ERROR;
     PyReadAttributeParams pyParams = {};
@@ -401,7 +401,7 @@
     std::unique_ptr<ReadClientCallback> callback = std::make_unique<ReadClientCallback>(appContext);
 
     va_list args;
-    va_start(args, numEventPaths);
+    va_start(args, eventNumberFilter);
 
     std::unique_ptr<AttributePathParams[]> attributePaths(new AttributePathParams[numAttributePaths]);
     std::unique_ptr<chip::app::DataVersionFilter[]> dataVersionFilters(new chip::app::DataVersionFilter[numDataversionFilters]);
@@ -462,6 +462,14 @@
             params.mpEventPathParamsList    = eventPaths.get();
             params.mEventPathParamsListSize = numEventPaths;
         }
+        if (eventNumberFilter != nullptr)
+        {
+            static_assert(sizeof(chip::EventNumber) == sizeof(*eventNumberFilter) &&
+                              std::is_unsigned<chip::EventNumber>::value ==
+                                  std::is_unsigned<std::remove_pointer<decltype(eventNumberFilter)>::type>::value,
+                          "EventNumber type mismatch");
+            params.mEventNumber = MakeOptional(EventNumber(*eventNumberFilter));
+        }
 
         params.mIsFabricFiltered = pyParams.isFabricFiltered;
 
diff --git a/src/controller/python/test/test_scripts/cluster_objects.py b/src/controller/python/test/test_scripts/cluster_objects.py
index 162dae0..b55c7b0 100644
--- a/src/controller/python/test/test_scripts/cluster_objects.py
+++ b/src/controller/python/test/test_scripts/cluster_objects.py
@@ -320,7 +320,7 @@
         # We trigger sending an event a couple of times just to be safe.
         await devCtrl.SendCommand(nodeid=NODE_ID, endpoint=1, payload=Clusters.UnitTesting.Commands.TestEmitTestEventRequest())
         await devCtrl.SendCommand(nodeid=NODE_ID, endpoint=1, payload=Clusters.UnitTesting.Commands.TestEmitTestEventRequest())
-        await devCtrl.SendCommand(nodeid=NODE_ID, endpoint=1, payload=Clusters.UnitTesting.Commands.TestEmitTestEventRequest())
+        return await devCtrl.SendCommand(nodeid=NODE_ID, endpoint=1, payload=Clusters.UnitTesting.Commands.TestEmitTestEventRequest())
 
     @ classmethod
     async def _RetryForContent(cls, request, until, retryCount=10, intervalSeconds=1):
@@ -338,6 +338,28 @@
         await cls._RetryForContent(request=lambda: devCtrl.ReadEvent(nodeid=NODE_ID, events=req), until=lambda res: res != 0)
 
     @ classmethod
+    async def TriggerAndWaitForEventsWithFilter(cls, devCtrl, req):
+        response = await cls._TriggerEvent(devCtrl)
+        current_event_filter = response.value
+
+        def validate_got_expected_event(events):
+            number_of_events = len(events)
+            if number_of_events != 1:
+                return False
+
+            parsed_event_number = events[0].Header.EventNumber
+            if parsed_event_number != current_event_filter:
+                return False
+            return True
+
+        await cls._RetryForContent(request=lambda: devCtrl.ReadEvent(nodeid=NODE_ID, events=req, eventNumberFilter=current_event_filter), until=validate_got_expected_event)
+
+        def validate_got_no_event(events):
+            return len(events) == 0
+
+        await cls._RetryForContent(request=lambda: devCtrl.ReadEvent(nodeid=NODE_ID, events=req, eventNumberFilter=(current_event_filter + 1)), until=validate_got_no_event)
+
+    @ classmethod
     @ base.test_case
     async def TestGenerateUndefinedFabricScopedEventRequests(cls, devCtrl):
         logger.info("Running TestGenerateUndefinedFabricScopedEventRequests")
@@ -393,6 +415,12 @@
 
         await cls.TriggerAndWaitForEvents(devCtrl, req)
 
+        logger.info("6: Reading Ex Cx Ex, with filter")
+        req = [
+            (1, Clusters.UnitTesting.Events.TestEvent, 0),
+        ]
+        await cls.TriggerAndWaitForEventsWithFilter(devCtrl, req)
+
         # TODO: Add more wildcard test for IM events.
 
     @ classmethod