[ReadHandler] Report Scheduler class (#27553)

* Added a new class that will handle the scheduling of reports.

* Restyled by clang-format

* Removed un-necessary define in TestReportScheduler and applied refactor of SetReportingIntervals to SetMaxReportingIntervals to platform code

* Added TimerDelegate and wrapper functions around calls to Timer. Remove unnecessary checks for nullptr

* Added VerifyOrReturn after NL_TEST_ASSERTS for nullptr

* Completed TimerDelegate class and modified ReadHandlerNodes so they carry their own callback

* Modified TimerDelegate to allow to pass different objects as context

* ifdefing out ScheduleRun() to debug failing CI

* Added issue # to TODOs, refactored Min/Max Intervals to Min/Max Timestamp

* Clarified some comments regarding timing

* Restyled by whitespace

* Restyled by clang-format

* Added interface to GetMonotonicTimestamp in the timer delegate

* Apply suggestions from code review

Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>

* Completed renaming to eliminate compiling error, moved TestReporScehduler in reporting namespace, addressed some low hanging fruits

* Removed useless objects from tests as well as useless typecasting, and unnecessary check

* Fixed comment about private methods used in ReportScheduler as a friend class

* Changed to SetMinReportInterval to SetMinReportingIntervalForTests, removed the IsChunkedReport from comment about friend class, added a mock timestamp and timer to test to better control time in simulation for specific timing test cases

* Apply suggestions from code review

Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>

* Restyled by clang-format

* Removed all calls to ReadHandler States to prevent Engine calls from the Test as it seems to impact the CI

---------

Co-authored-by: Restyled.io <commits@restyled.io>
Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>
diff --git a/examples/platform/nrfconnect/util/ICDUtil.cpp b/examples/platform/nrfconnect/util/ICDUtil.cpp
index fd2130c..b3dc9c8 100644
--- a/examples/platform/nrfconnect/util/ICDUtil.cpp
+++ b/examples/platform/nrfconnect/util/ICDUtil.cpp
@@ -36,5 +36,5 @@
         agreedMaxInterval = kSubscriptionMaxIntervalPublisherLimit;
     }
 
-    return aReadHandler.SetReportingIntervals(agreedMaxInterval);
+    return aReadHandler.SetMaxReportingInterval(agreedMaxInterval);
 }
diff --git a/examples/platform/silabs/ICDSubscriptionCallback.cpp b/examples/platform/silabs/ICDSubscriptionCallback.cpp
index 735246f..eba2896 100644
--- a/examples/platform/silabs/ICDSubscriptionCallback.cpp
+++ b/examples/platform/silabs/ICDSubscriptionCallback.cpp
@@ -61,5 +61,5 @@
         decidedMaxInterval = maximumMaxInterval;
     }
 
-    return aReadHandler.SetReportingIntervals(decidedMaxInterval);
+    return aReadHandler.SetMaxReportingInterval(decidedMaxInterval);
 }
diff --git a/src/app/BUILD.gn b/src/app/BUILD.gn
index 8211d22..7f2c4d8 100644
--- a/src/app/BUILD.gn
+++ b/src/app/BUILD.gn
@@ -193,6 +193,9 @@
     "WriteHandler.cpp",
     "reporting/Engine.cpp",
     "reporting/Engine.h",
+    "reporting/ReportScheduler.h",
+    "reporting/ReportSchedulerImpl.cpp",
+    "reporting/ReportSchedulerImpl.h",
     "reporting/reporting.h",
   ]
 
diff --git a/src/app/ReadHandler.cpp b/src/app/ReadHandler.cpp
index 4a92fcf..a5209ae 100644
--- a/src/app/ReadHandler.cpp
+++ b/src/app/ReadHandler.cpp
@@ -39,7 +39,7 @@
 using Status = Protocols::InteractionModel::Status;
 
 ReadHandler::ReadHandler(ManagementCallback & apCallback, Messaging::ExchangeContext * apExchangeContext,
-                         InteractionType aInteractionType) :
+                         InteractionType aInteractionType, Observer * observer) :
     mExchangeCtx(*this),
     mManagementCallback(apCallback)
 #if CHIP_CONFIG_PERSIST_SUBSCRIPTIONS
@@ -63,15 +63,37 @@
     SetStateFlag(ReadHandlerFlags::PrimingReports);
 
     mSessionHandle.Grab(mExchangeCtx->GetSessionHandle());
+
+// TODO (#27672): Uncomment when the ReportScheduler is implemented
+#if 0
+    if (nullptr != observer)
+    {
+        if (CHIP_NO_ERROR == SetObserver(observer))
+        {
+            mObserver->OnReadHandlerCreated(this);
+        }
+    }
+#endif
 }
 
 #if CHIP_CONFIG_PERSIST_SUBSCRIPTIONS
-ReadHandler::ReadHandler(ManagementCallback & apCallback) :
+ReadHandler::ReadHandler(ManagementCallback & apCallback, Observer * observer) :
     mExchangeCtx(*this), mManagementCallback(apCallback), mOnConnectedCallback(HandleDeviceConnected, this),
     mOnConnectionFailureCallback(HandleDeviceConnectionFailure, this)
 {
     mInteractionType = InteractionType::Subscribe;
     mFlags.ClearAll();
+
+// TODO (#27672): Uncomment when the ReportScheduler is implemented
+#if 0
+    if (nullptr != observer)
+    {
+        if (CHIP_NO_ERROR == SetObserver(observer))
+        {
+            mObserver->OnReadHandlerCreated(this);
+        }
+    }
+#endif
 }
 
 void ReadHandler::ResumeSubscription(CASESessionManager & caseSessionManager,
@@ -115,6 +137,13 @@
 
 ReadHandler::~ReadHandler()
 {
+    // TODO (#27672): Enable when the ReportScheduler is implemented and move in Close() after testing
+#if 0
+    if (nullptr != mObserver)
+    {
+        mObserver->OnReadHandlerDestroyed(this);
+    }
+#endif
     auto * appCallback = mManagementCallback.GetAppCallback();
     if (mFlags.Has(ReadHandlerFlags::ActiveSubscription) && appCallback)
     {
@@ -319,6 +348,15 @@
 
         if (IsType(InteractionType::Subscribe) && !IsPriming())
         {
+// TODO (#27672): Enable when the ReportScheduler is implemented and remove call to UpdateReportTimer, will be handled by
+// the report Scheduler
+#if 0
+            if (nullptr != mObserver)
+            {
+                mObserver->OnSubscriptionAction(this);
+            }
+#endif
+
             // Ignore the error from UpdateReportTimer.  If we've
             // successfully sent the message, we need to return success from
             // this method.
@@ -593,6 +631,13 @@
     //
     if (aTargetState == HandlerState::GeneratingReports && IsReportableNow())
     {
+// TODO (#27672): Enable when the ReportScheduler is implemented and remove the call to ScheduleRun()
+#if 0
+         if(nullptr != mObserver)
+        {
+            mObserver->OnBecameReportable(this);
+        }
+#endif
         InteractionModelEngine::GetInstance()->GetReportingEngine().ScheduleRun();
     }
 }
@@ -634,6 +679,14 @@
     ReturnErrorOnFailure(writer.Finalize(&packet));
     VerifyOrReturnLogError(mExchangeCtx, CHIP_ERROR_INCORRECT_STATE);
 
+    // TODO (#27672): Uncomment when the ReportScheduler is implemented and remove call to UpdateReportTimer, handled by
+    // the report Scheduler
+#if 0
+    if (nullptr != mObserver)
+    {
+        mObserver->OnSubscriptionAction(this);
+    }
+#endif
     ReturnErrorOnFailure(UpdateReportTimer());
 
     ClearStateFlag(ReadHandlerFlags::PrimingReports);
@@ -753,6 +806,7 @@
     }
 }
 
+// TODO (#27672): Remove when ReportScheduler is enabled as timing will now be handled by the ReportScheduler
 void ReadHandler::MinIntervalExpiredCallback(System::Layer * apSystemLayer, void * apAppState)
 {
     VerifyOrReturn(apAppState != nullptr);
@@ -764,6 +818,7 @@
         readHandler);
 }
 
+// TODO (#27672): Remove when ReportScheduler is enabled as timing will now be handled by the ReportScheduler
 void ReadHandler::MaxIntervalExpiredCallback(System::Layer * apSystemLayer, void * apAppState)
 {
     VerifyOrReturn(apAppState != nullptr);
@@ -773,6 +828,7 @@
                     readHandler->mMaxInterval - readHandler->mMinIntervalFloorSeconds);
 }
 
+// TODO (#27672): Remove when ReportScheduler is enabled as timing will now be handled by the ReportScheduler
 CHIP_ERROR ReadHandler::UpdateReportTimer()
 {
     InteractionModelEngine::GetInstance()->GetExchangeManager()->GetSessionManager()->SystemLayer()->CancelTimer(
@@ -812,7 +868,7 @@
     // Here we just reset the iterator to the beginning of the current cluster, if the dirty path affects it.
     // This will ensure the reports are consistent within a single cluster generated from a single path in the request.
 
-    // TODO (#16699): Currently we can only gurentee the reports generated from a single path in the request are consistent. The
+    // TODO (#16699): Currently we can only guarantee the reports generated from a single path in the request are consistent. The
     // data might be inconsistent if the user send a request with two paths from the same cluster. We need to clearify the behavior
     // or make it consistent.
     if (mAttributePathExpandIterator.Get(path) &&
@@ -831,6 +887,13 @@
 
     if (IsReportableNow())
     {
+        // TODO (#27672): Enable when the ReportScheduler is implemented and remove the call to ScheduleRun()
+#if 0
+        if(nullptr != mObserver)
+        {
+            mObserver->OnBecameReportable(this);
+        }
+#endif
         InteractionModelEngine::GetInstance()->GetReportingEngine().ScheduleRun();
     }
 }
@@ -853,9 +916,17 @@
 {
     bool oldReportable = IsReportableNow();
     mFlags.Set(aFlag, aValue);
+
     // If we became reportable, schedule a reporting run.
     if (!oldReportable && IsReportableNow())
     {
+// TODO (#27672): Enable when the ReportScheduler is implemented and remove the call to ScheduleRun()
+#if 0
+        if(nullptr != mObserver)
+        {
+            mObserver->OnBecameReportable(this);
+        }
+#endif
         InteractionModelEngine::GetInstance()->GetReportingEngine().ScheduleRun();
     }
 }
diff --git a/src/app/ReadHandler.h b/src/app/ReadHandler.h
index f460188..77c88c1 100644
--- a/src/app/ReadHandler.h
+++ b/src/app/ReadHandler.h
@@ -64,6 +64,8 @@
 namespace reporting {
 class Engine;
 class TestReportingEngine;
+class ReportScheduler;
+class TestReportScheduler;
 } // namespace reporting
 
 class InteractionModelEngine;
@@ -152,6 +154,38 @@
         virtual ApplicationCallback * GetAppCallback() = 0;
     };
 
+    // TODO (#27675) : Merge existing callback and observer into one class and have an observer pool in the Readhandler to notify
+    // every
+    /*
+     * Observer class for ReadHandler, meant to allow multiple objects to observe the ReadHandler. Currently only one observer is
+     * supported but all above callbacks should be merged into observer type and an observer pool should be added to allow multiple
+     * objects to observe ReadHandler
+     */
+    class Observer
+    {
+    public:
+        virtual ~Observer() = default;
+
+        /// @brief Callback invoked to notify a ReadHandler was created and can be registered
+        /// @param[in] apReadHandler  ReadHandler getting added
+        virtual void OnReadHandlerCreated(ReadHandler * apReadHandler) = 0;
+
+        /// @brief Callback invoked when a ReadHandler went from a non reportable state to a reportable state so a report can be
+        /// sent immediately if the minimal interval allows it. Otherwise the report should be rescheduled to the earliest time
+        /// allowed.
+        /// @param[in] apReadHandler  ReadHandler that became dirty
+        virtual void OnBecameReportable(ReadHandler * apReadHandler) = 0;
+
+        /// @brief Callback invoked when the read handler needs to make sure to send a message to the subscriber within the next
+        /// maxInterval time period.
+        /// @param[in] apReadHandler ReadHandler that has generated a report
+        virtual void OnSubscriptionAction(ReadHandler * apReadHandler) = 0;
+
+        /// @brief Callback invoked when a ReadHandler is getting removed so it can be unregistered
+        /// @param[in] apReadHandler  ReadHandler getting destroyed
+        virtual void OnReadHandlerDestroyed(ReadHandler * apReadHandler) = 0;
+    };
+
     /*
      * Destructor - as part of destruction, it will abort the exchange context
      * if a valid one still exists.
@@ -167,7 +201,8 @@
      *  The callback passed in has to outlive this handler object.
      *
      */
-    ReadHandler(ManagementCallback & apCallback, Messaging::ExchangeContext * apExchangeContext, InteractionType aInteractionType);
+    ReadHandler(ManagementCallback & apCallback, Messaging::ExchangeContext * apExchangeContext, InteractionType aInteractionType,
+                Observer * observer = nullptr);
 
 #if CHIP_CONFIG_PERSIST_SUBSCRIPTIONS
     /**
@@ -177,7 +212,7 @@
      *  The callback passed in has to outlive this handler object.
      *
      */
-    ReadHandler(ManagementCallback & apCallback);
+    ReadHandler(ManagementCallback & apCallback, Observer * observer = nullptr);
 #endif
 
     const ObjectList<AttributePathParams> * GetAttributePathList() const { return mpAttributePathList; }
@@ -190,13 +225,22 @@
         aMaxInterval = mMaxInterval;
     }
 
+    CHIP_ERROR SetMinReportingIntervalForTests(uint16_t aMinInterval)
+    {
+        VerifyOrReturnError(IsIdle(), CHIP_ERROR_INCORRECT_STATE);
+        VerifyOrReturnError(aMinInterval <= mMaxInterval, CHIP_ERROR_INVALID_ARGUMENT);
+        // Ensures the new min interval is higher than the subscriber established one.
+        mMinIntervalFloorSeconds = std::max(mMinIntervalFloorSeconds, aMinInterval);
+        return CHIP_NO_ERROR;
+    }
+
     /*
-     * Set the reporting intervals for the subscription. This SHALL only be called
+     * Set the maximum reporting interval for the subscription. This SHALL only be called
      * from the OnSubscriptionRequested callback above. The restriction is as below
      * MinIntervalFloor ≤ MaxInterval ≤ MAX(SUBSCRIPTION_MAX_INTERVAL_PUBLISHER_LIMIT, MaxIntervalCeiling)
      * Where SUBSCRIPTION_MAX_INTERVAL_PUBLISHER_LIMIT is set to 60m in the spec.
      */
-    CHIP_ERROR SetReportingIntervals(uint16_t aMaxInterval)
+    CHIP_ERROR SetMaxReportingInterval(uint16_t aMaxInterval)
     {
         VerifyOrReturnError(IsIdle(), CHIP_ERROR_INCORRECT_STATE);
         VerifyOrReturnError(mMinIntervalFloorSeconds <= aMaxInterval, CHIP_ERROR_INVALID_ARGUMENT);
@@ -206,6 +250,18 @@
         return CHIP_NO_ERROR;
     }
 
+    /// @brief Add an observer to the read handler, currently only one observer is supported but all other callbacks should be
+    /// merged with a general observer type to allow multiple object to observe readhandlers
+    /// @param aObserver observer to be added
+    /// @return CHIP_ERROR_INVALID_ARGUMENT if passing in nullptr
+    CHIP_ERROR SetObserver(Observer * aObserver)
+    {
+        VerifyOrReturnError(nullptr != aObserver, CHIP_ERROR_INVALID_ARGUMENT);
+        // TODO (#27675) : After merging the callbacks and observer, change so the method adds a new observer to an observer pool
+        mObserver = aObserver;
+        return CHIP_NO_ERROR;
+    }
+
 private:
     PriorityLevel GetCurrentPriority() const { return mCurrentPriority; }
     EventNumber & GetEventMin() { return mEventMin; }
@@ -214,13 +270,13 @@
     {
         // WaitingUntilMinInterval is used to prevent subscription data delivery while we are
         // waiting for the min reporting interval to elapse.
-        WaitingUntilMinInterval = (1 << 0),
+        WaitingUntilMinInterval = (1 << 0), // TODO (#27672): Remove once ReportScheduler is implemented or change to test flag
 
         // WaitingUntilMaxInterval is used to prevent subscription empty report delivery while we
         // are waiting for the max reporting interval to elaps.  When WaitingUntilMaxInterval
         // becomes false, we are allowed to send an empty report to keep the
         // subscription alive on the client.
-        WaitingUntilMaxInterval = (1 << 1),
+        WaitingUntilMaxInterval = (1 << 1), // TODO (#27672): Remove once ReportScheduler is implemented
 
         // The flag indicating we are in the middle of a series of chunked report messages, this flag will be cleared during
         // sending last chunked message.
@@ -291,6 +347,8 @@
 
     bool IsIdle() const { return mState == HandlerState::Idle; }
 
+    // TODO (#27672): Change back to IsReportable once ReportScheduler is implemented so this can assess reportability without
+    // considering timing. The ReporScheduler will handle timing.
     /// @brief Returns whether the ReadHandler is in a state where it can immediately send a report. This function
     /// is used to determine whether a report generation should be scheduled for the handler.
     bool IsReportableNow() const
@@ -370,6 +428,7 @@
 
     friend class TestReadInteraction;
     friend class chip::app::reporting::TestReportingEngine;
+    friend class chip::app::reporting::TestReportScheduler;
 
     //
     // The engine needs to be able to Abort/Close a ReadHandler instance upon completion of work for a given read/subscribe
@@ -379,6 +438,10 @@
     friend class chip::app::reporting::Engine;
     friend class chip::app::InteractionModelEngine;
 
+    // The report scheduler needs to be able to access StateFlag private functions IsGeneratingReports() and IsDirty() to
+    // know when to schedule a run so it is declared as a friend class.
+    friend class chip::app::reporting::ReportScheduler;
+
     enum class HandlerState : uint8_t
     {
         Idle,                   ///< The handler has been initialized and is ready
@@ -404,10 +467,13 @@
 
     /// @brief This function is called when the min interval timer has expired, it restarts the timer on a timeout equal to the
     /// difference between the max interval and the min interval.
-    static void MinIntervalExpiredCallback(System::Layer * apSystemLayer, void * apAppState);
-    static void MaxIntervalExpiredCallback(System::Layer * apSystemLayer, void * apAppState);
+    static void MinIntervalExpiredCallback(System::Layer * apSystemLayer, void * apAppState); // TODO (#27672): Remove once
+                                                                                              // ReportScheduler is implemented.
+    static void MaxIntervalExpiredCallback(System::Layer * apSystemLayer, void * apAppState); // TODO (#27672): Remove once
+                                                                                              // ReportScheduler is implemented.
     /// @brief This function is called when a report is sent and it restarts the min interval timer.
-    CHIP_ERROR UpdateReportTimer();
+    CHIP_ERROR UpdateReportTimer(); // TODO (#27672) : Remove once ReportScheduler is implemented.
+
     CHIP_ERROR SendSubscribeResponse();
     CHIP_ERROR ProcessSubscribeRequest(System::PacketBufferHandle && aPayload);
     CHIP_ERROR ProcessReadRequest(System::PacketBufferHandle && aPayload);
@@ -520,6 +586,9 @@
     BitFlags<ReadHandlerFlags> mFlags;
     InteractionType mInteractionType = InteractionType::Read;
 
+    // TODO (#27675): Merge all observers into one and that one will dispatch the callbacks to the right place.
+    Observer * mObserver = nullptr;
+
 #if CHIP_CONFIG_PERSIST_SUBSCRIPTIONS
     // Callbacks to handle server-initiated session success/failure
     chip::Callback::Callback<OnDeviceConnected> mOnConnectedCallback;
diff --git a/src/app/reporting/Engine.cpp b/src/app/reporting/Engine.cpp
index a985321..f0fea66 100644
--- a/src/app/reporting/Engine.cpp
+++ b/src/app/reporting/Engine.cpp
@@ -636,6 +636,7 @@
         ReadHandler * readHandler = imEngine->ActiveHandlerAt(mCurReadHandlerIdx % (uint32_t) imEngine->mReadHandlers.Allocated());
         VerifyOrDie(readHandler != nullptr);
 
+        // TODO (#27672): Replace with check with Report Scheduler if the read handler is reportable
         if (readHandler->IsReportableNow())
         {
             mRunningReadHandler = readHandler;
diff --git a/src/app/reporting/ReportScheduler.h b/src/app/reporting/ReportScheduler.h
new file mode 100644
index 0000000..80d391c
--- /dev/null
+++ b/src/app/reporting/ReportScheduler.h
@@ -0,0 +1,150 @@
+/*
+ *
+ *    Copyright (c) 2023 Project CHIP Authors
+ *    All rights reserved.
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ */
+
+#pragma once
+
+#include <app/ReadHandler.h>
+#include <lib/core/CHIPError.h>
+#include <lib/support/IntrusiveList.h>
+#include <system/SystemClock.h>
+
+namespace chip {
+namespace app {
+namespace reporting {
+
+// Forward declaration of TestReportScheduler to allow it to be friend with ReportScheduler
+class TestReportScheduler;
+
+using Timestamp = System::Clock::Timestamp;
+
+class ReportScheduler : public ReadHandler::Observer
+{
+public:
+    /// @brief This class acts as an interface between the report scheduler and the system timer to reduce dependencies on the
+    /// system layer.
+    class TimerDelegate
+    {
+    public:
+        virtual ~TimerDelegate() {}
+        /// @brief Start a timer for a given context. The report scheduler must always cancel an existing timer for a context (using
+        /// CancelTimer) before starting a new one for that context.
+        /// @param context context to pass to the timer callback.
+        /// @param aTimeout time in miliseconds before the timer expires
+        virtual CHIP_ERROR StartTimer(void * context, System::Clock::Timeout aTimeout) = 0;
+        /// @brief Cancel a timer for a given context
+        /// @param context used to identify the timer to cancel
+        virtual void CancelTimer(void * context)         = 0;
+        virtual bool IsTimerActive(void * context)       = 0;
+        virtual Timestamp GetCurrentMonotonicTimestamp() = 0;
+    };
+
+    class ReadHandlerNode : public IntrusiveListNodeBase<>
+    {
+    public:
+        using TimerCompleteCallback = void (*)();
+
+        ReadHandlerNode(ReadHandler * aReadHandler, TimerDelegate * aTimerDelegate, TimerCompleteCallback aCallback) :
+            mTimerDelegate(aTimerDelegate), mCallback(aCallback)
+        {
+            VerifyOrDie(aReadHandler != nullptr);
+            VerifyOrDie(aTimerDelegate != nullptr);
+            VerifyOrDie(aCallback != nullptr);
+
+            mReadHandler = aReadHandler;
+            SetIntervalTimeStamps(aReadHandler);
+        }
+        ReadHandler * GetReadHandler() const { return mReadHandler; }
+        /// @brief Check if the Node is reportable now, meaning its readhandler was made reportable by attribute dirtying and
+        /// handler state, and minimal time interval since last report has elapsed, or the maximal time interval since last
+        /// report has elapsed
+        bool IsReportableNow() const
+        {
+            // TODO: Add flags to allow for test to simulate waiting for the min interval or max intrval to elapse when integrating
+            // the scheduler in the ReadHandler
+            Timestamp now = mTimerDelegate->GetCurrentMonotonicTimestamp();
+            return (mReadHandler->IsGeneratingReports() &&
+                    ((now >= mMinTimestamp && mReadHandler->IsDirty()) || now >= mMaxTimestamp));
+        }
+
+        void SetIntervalTimeStamps(ReadHandler * aReadHandler)
+        {
+            uint16_t minInterval, maxInterval;
+            aReadHandler->GetReportingIntervals(minInterval, maxInterval);
+            Timestamp now = mTimerDelegate->GetCurrentMonotonicTimestamp();
+            mMinTimestamp = now + System::Clock::Seconds16(minInterval);
+            mMaxTimestamp = now + System::Clock::Seconds16(maxInterval);
+        }
+
+        void RunCallback() { mCallback(); }
+
+        Timestamp GetMinTimestamp() const { return mMinTimestamp; }
+        Timestamp GetMaxTimestamp() const { return mMaxTimestamp; }
+
+    private:
+        TimerDelegate * mTimerDelegate;
+        TimerCompleteCallback mCallback;
+        ReadHandler * mReadHandler;
+        Timestamp mMinTimestamp;
+        Timestamp mMaxTimestamp;
+    };
+
+    ReportScheduler(TimerDelegate * aTimerDelegate) : mTimerDelegate(aTimerDelegate) {}
+    /**
+     *  Interface to act on changes in the ReadHandler reportability
+     */
+    virtual ~ReportScheduler() = default;
+
+    /// @brief Check if a ReadHandler is scheduled for reporting
+    virtual bool IsReportScheduled(ReadHandler * aReadHandler) = 0;
+    /// @brief Check whether a ReadHandler is reportable right now, taking into account its minimum and maximum intervals.
+    /// @param aReadHandler read handler to check
+    bool IsReportableNow(ReadHandler * aReadHandler) { return FindReadHandlerNode(aReadHandler)->IsReportableNow(); };
+    /// @brief Check if a ReadHandler is reportable without considering the timing
+    bool IsReadHandlerReportable(ReadHandler * aReadHandler) const
+    {
+        return aReadHandler->IsGeneratingReports() && aReadHandler->IsDirty();
+    }
+
+    /// @brief Get the number of ReadHandlers registered in the scheduler's node pool
+    size_t GetNumReadHandlers() const { return mNodesPool.Allocated(); }
+
+protected:
+    friend class chip::app::reporting::TestReportScheduler;
+
+    /// @brief Find the ReadHandlerNode for a given ReadHandler pointer
+    /// @param [in] aReadHandler ReadHandler pointer to look for in the ReadHandler nodes list
+    /// @return Node Address if node was found, nullptr otherwise
+    ReadHandlerNode * FindReadHandlerNode(const ReadHandler * aReadHandler)
+    {
+        for (auto & iter : mReadHandlerList)
+        {
+            if (iter.GetReadHandler() == aReadHandler)
+            {
+                return &iter;
+            }
+        }
+        return nullptr;
+    }
+
+    IntrusiveList<ReadHandlerNode> mReadHandlerList;
+    ObjectPool<ReadHandlerNode, CHIP_IM_MAX_NUM_READS + CHIP_IM_MAX_NUM_SUBSCRIPTIONS> mNodesPool;
+    TimerDelegate * mTimerDelegate;
+};
+}; // namespace reporting
+}; // namespace app
+}; // namespace chip
diff --git a/src/app/reporting/ReportSchedulerImpl.cpp b/src/app/reporting/ReportSchedulerImpl.cpp
new file mode 100644
index 0000000..4b45ab9
--- /dev/null
+++ b/src/app/reporting/ReportSchedulerImpl.cpp
@@ -0,0 +1,188 @@
+/*
+ *
+ *    Copyright (c) 2023 Project CHIP Authors
+ *
+ *    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.
+ */
+
+#include <app/InteractionModelEngine.h>
+#include <app/reporting/ReportSchedulerImpl.h>
+
+namespace chip {
+namespace app {
+namespace reporting {
+
+using Seconds16       = System::Clock::Seconds16;
+using Milliseconds32  = System::Clock::Milliseconds32;
+using Timeout         = System::Clock::Timeout;
+using Timestamp       = System::Clock::Timestamp;
+using ReadHandlerNode = ReportScheduler::ReadHandlerNode;
+
+/// @brief Callback called when the report timer expires to schedule an engine run regardless of the state of the ReadHandlers, as
+/// the engine already verifies that read handlers are reportable before sending a report
+static void ReportTimerCallback()
+{
+    InteractionModelEngine::GetInstance()->GetReportingEngine().ScheduleRun();
+}
+
+ReportSchedulerImpl::ReportSchedulerImpl(TimerDelegate * aTimerDelegate) : ReportScheduler(aTimerDelegate)
+{
+    VerifyOrDie(nullptr != mTimerDelegate);
+}
+
+/// @brief When a ReadHandler is added, register it, which will schedule an engine run
+void ReportSchedulerImpl::OnReadHandlerCreated(ReadHandler * aReadHandler)
+{
+    RegisterReadHandler(aReadHandler);
+}
+
+/// @brief When a ReadHandler becomes reportable, schedule, verifies if the min interval of a handleris elapsed. If not,
+/// reschedule the report to happen when the min interval is elapsed. If it is, schedule an engine run.
+void ReportSchedulerImpl::OnBecameReportable(ReadHandler * aReadHandler)
+{
+    ReadHandlerNode * node = FindReadHandlerNode(aReadHandler);
+    VerifyOrReturn(nullptr != node);
+
+    Milliseconds32 newTimeout;
+    if (node->IsReportableNow())
+    {
+        // If the handler is reportable now, just schedule a report immediately
+        newTimeout = Milliseconds32(0);
+    }
+    else
+    {
+        // If the handler is not reportable now, schedule a report for the min interval
+        newTimeout = node->GetMinTimestamp() - mTimerDelegate->GetCurrentMonotonicTimestamp();
+    }
+
+    ScheduleReport(newTimeout, node);
+}
+
+void ReportSchedulerImpl::OnSubscriptionAction(ReadHandler * apReadHandler)
+{
+    ReadHandlerNode * node = FindReadHandlerNode(apReadHandler);
+    VerifyOrReturn(nullptr != node);
+    // Schedule callback for max interval by computing the difference between the max timestamp and the current timestamp
+    node->SetIntervalTimeStamps(apReadHandler);
+    Milliseconds32 newTimeout = node->GetMaxTimestamp() - mTimerDelegate->GetCurrentMonotonicTimestamp();
+    ScheduleReport(newTimeout, node);
+}
+
+/// @brief When a ReadHandler is removed, unregister it, which will cancel any scheduled report
+void ReportSchedulerImpl::OnReadHandlerDestroyed(ReadHandler * aReadHandler)
+{
+    UnregisterReadHandler(aReadHandler);
+}
+
+CHIP_ERROR ReportSchedulerImpl::RegisterReadHandler(ReadHandler * aReadHandler)
+{
+    ReadHandlerNode * newNode = FindReadHandlerNode(aReadHandler);
+    // Handler must not be registered yet; it's just being constructed.
+    VerifyOrDie(nullptr == newNode);
+    // The NodePool is the same size as the ReadHandler pool from the IM Engine, so we don't need a check for size here since if a
+    // ReadHandler was created, space should be available.
+    newNode = mNodesPool.CreateObject(aReadHandler, mTimerDelegate, ReportTimerCallback);
+    mReadHandlerList.PushBack(newNode);
+
+    ChipLogProgress(DataManagement,
+                    "Registered a ReadHandler that will schedule a report between system Timestamp: %" PRIu64
+                    " and system Timestamp %" PRIu64 ".",
+                    newNode->GetMinTimestamp().count(), newNode->GetMaxTimestamp().count());
+
+    Timestamp now = mTimerDelegate->GetCurrentMonotonicTimestamp();
+    Milliseconds32 newTimeout;
+    // If the handler is reportable, schedule a report for the min interval, otherwise schedule a report for the max interval
+    if (newNode->IsReportableNow())
+    {
+        // If the handler is reportable now, just schedule a report immediately
+        newTimeout = Milliseconds32(0);
+    }
+    else if (IsReadHandlerReportable(aReadHandler) && (newNode->GetMinTimestamp() > now))
+    {
+        // If the handler is reportable now, but the min interval is not elapsed, schedule a report for the moment the min interval
+        // has elapsed
+        newTimeout = newNode->GetMinTimestamp() - now;
+    }
+    else
+    {
+        // If the handler is not reportable now, schedule a report for the max interval
+        newTimeout = newNode->GetMaxTimestamp() - now;
+    }
+
+    ReturnErrorOnFailure(ScheduleReport(newTimeout, newNode));
+    return CHIP_NO_ERROR;
+}
+
+CHIP_ERROR ReportSchedulerImpl::ScheduleReport(Timeout timeout, ReadHandlerNode * node)
+{
+    // Cancel Report if it is currently scheduled
+    CancelSchedulerTimer(node);
+    StartSchedulerTimer(node, timeout);
+
+    return CHIP_NO_ERROR;
+}
+
+void ReportSchedulerImpl::CancelReport(ReadHandler * aReadHandler)
+{
+    ReadHandlerNode * node = FindReadHandlerNode(aReadHandler);
+    VerifyOrReturn(nullptr != node);
+    CancelSchedulerTimer(node);
+}
+
+void ReportSchedulerImpl::UnregisterReadHandler(ReadHandler * aReadHandler)
+{
+    CancelReport(aReadHandler);
+
+    ReadHandlerNode * removeNode = FindReadHandlerNode(aReadHandler);
+    // Nothing to remove if the handler is not found in the list
+    VerifyOrReturn(nullptr != removeNode);
+
+    mReadHandlerList.Remove(removeNode);
+    mNodesPool.ReleaseObject(removeNode);
+}
+
+void ReportSchedulerImpl::UnregisterAllHandlers()
+{
+    while (!mReadHandlerList.Empty())
+    {
+        ReadHandler * firstReadHandler = mReadHandlerList.begin()->GetReadHandler();
+        UnregisterReadHandler(firstReadHandler);
+    }
+}
+
+bool ReportSchedulerImpl::IsReportScheduled(ReadHandler * aReadHandler)
+{
+    ReadHandlerNode * node = FindReadHandlerNode(aReadHandler);
+    VerifyOrReturnValue(nullptr != node, false);
+    return CheckSchedulerTimerActive(node);
+}
+
+CHIP_ERROR ReportSchedulerImpl::StartSchedulerTimer(ReadHandlerNode * node, System::Clock::Timeout aTimeout)
+{
+    // Schedule Report
+    return mTimerDelegate->StartTimer(node, aTimeout);
+}
+
+void ReportSchedulerImpl::CancelSchedulerTimer(ReadHandlerNode * node)
+{
+    mTimerDelegate->CancelTimer(node);
+}
+
+bool ReportSchedulerImpl::CheckSchedulerTimerActive(ReadHandlerNode * node)
+{
+    return mTimerDelegate->IsTimerActive(node);
+}
+
+} // namespace reporting
+} // namespace app
+} // namespace chip
diff --git a/src/app/reporting/ReportSchedulerImpl.h b/src/app/reporting/ReportSchedulerImpl.h
new file mode 100644
index 0000000..849f9b7
--- /dev/null
+++ b/src/app/reporting/ReportSchedulerImpl.h
@@ -0,0 +1,63 @@
+/*
+ *
+ *    Copyright (c) 2023 Project CHIP Authors
+ *    All rights reserved.
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ */
+
+#pragma once
+
+#include <app/reporting/ReportScheduler.h>
+
+namespace chip {
+namespace app {
+namespace reporting {
+
+class ReportSchedulerImpl : public ReportScheduler
+{
+public:
+    ReportSchedulerImpl(TimerDelegate * aTimerDelegate);
+    ~ReportSchedulerImpl() override { UnregisterAllHandlers(); }
+
+    // ReadHandlerObserver
+    void OnReadHandlerCreated(ReadHandler * aReadHandler) override;
+    void OnBecameReportable(ReadHandler * aReadHandler) override;
+    void OnSubscriptionAction(ReadHandler * aReadHandler) override;
+    void OnReadHandlerDestroyed(ReadHandler * aReadHandler) override;
+
+protected:
+    virtual CHIP_ERROR RegisterReadHandler(ReadHandler * aReadHandler);
+    virtual CHIP_ERROR ScheduleReport(System::Clock::Timeout timeout, ReadHandlerNode * node);
+    virtual void CancelReport(ReadHandler * aReadHandler);
+    virtual void UnregisterReadHandler(ReadHandler * aReadHandler);
+    virtual void UnregisterAllHandlers();
+
+private:
+    friend class chip::app::reporting::TestReportScheduler;
+
+    bool IsReportScheduled(ReadHandler * aReadHandler) override;
+
+    /// @brief Start a timer for a given ReadHandlerNode, ensures that if a timer is already running for this node, it is cancelled
+    /// @param node Node of the ReadHandler list to start a timer for
+    /// @param aTimeout Delay before the timer expires
+    virtual CHIP_ERROR StartSchedulerTimer(ReadHandlerNode * node, System::Clock::Timeout aTimeout);
+    /// @brief Cancel the timer for a given ReadHandlerNode
+    virtual void CancelSchedulerTimer(ReadHandlerNode * node);
+    /// @brief Check if the timer for a given ReadHandlerNode is active
+    virtual bool CheckSchedulerTimerActive(ReadHandlerNode * node);
+};
+
+} // namespace reporting
+} // namespace app
+} // namespace chip
diff --git a/src/app/tests/BUILD.gn b/src/app/tests/BUILD.gn
index c716f44..365596d 100644
--- a/src/app/tests/BUILD.gn
+++ b/src/app/tests/BUILD.gn
@@ -144,6 +144,7 @@
     "TestOperationalStateDelegate.cpp",
     "TestPendingNotificationMap.cpp",
     "TestReadInteraction.cpp",
+    "TestReportScheduler.cpp",
     "TestReportingEngine.cpp",
     "TestSceneTable.cpp",
     "TestStatusIB.cpp",
diff --git a/src/app/tests/TestReportScheduler.cpp b/src/app/tests/TestReportScheduler.cpp
new file mode 100644
index 0000000..a244220
--- /dev/null
+++ b/src/app/tests/TestReportScheduler.cpp
@@ -0,0 +1,423 @@
+/*
+ *
+ *    Copyright (c) 2023 Project CHIP Authors
+ *    All rights reserved.
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ */
+
+#include <app/InteractionModelEngine.h>
+#include <app/reporting/ReportSchedulerImpl.h>
+#include <app/tests/AppTestContext.h>
+#include <lib/support/UnitTestContext.h>
+#include <lib/support/UnitTestRegistration.h>
+#include <lib/support/logging/CHIPLogging.h>
+#include <nlunit-test.h>
+
+namespace {
+
+class TestContext : public chip::Test::AppContext
+{
+public:
+    static int Initialize(void * context)
+    {
+        if (AppContext::Initialize(context) != SUCCESS)
+            return FAILURE;
+
+        auto * ctx = static_cast<TestContext *>(context);
+
+        if (ctx->mEventCounter.Init(0) != CHIP_NO_ERROR)
+        {
+            return FAILURE;
+        }
+
+        return SUCCESS;
+    }
+
+    static int Finalize(void * context)
+    {
+        chip::app::EventManagement::DestroyEventManagement();
+
+        if (AppContext::Finalize(context) != SUCCESS)
+            return FAILURE;
+
+        return SUCCESS;
+    }
+
+private:
+    chip::MonotonicallyIncreasingCounter<chip::EventNumber> mEventCounter;
+};
+
+class NullReadHandlerCallback : public chip::app::ReadHandler::ManagementCallback
+{
+public:
+    void OnDone(chip::app::ReadHandler & apReadHandlerObj) override {}
+    chip::app::ReadHandler::ApplicationCallback * GetAppCallback() override { return nullptr; }
+};
+
+} // namespace
+
+namespace chip {
+namespace app {
+namespace reporting {
+
+using InteractionModelEngine = InteractionModelEngine;
+using ReportScheduler        = reporting::ReportScheduler;
+using ReportSchedulerImpl    = reporting::ReportSchedulerImpl;
+using ReadHandlerNode        = reporting::ReportScheduler::ReadHandlerNode;
+using Milliseconds64         = System::Clock::Milliseconds64;
+
+static const size_t kNumMaxReadHandlers = 16;
+
+class TestTimerDelegate : public ReportScheduler::TimerDelegate
+{
+public:
+    struct NodeTimeoutPair
+    {
+        ReadHandlerNode * node;
+        System::Clock::Timeout timeout;
+    };
+
+    NodeTimeoutPair mPairArray[kNumMaxReadHandlers];
+    size_t mPairArraySize                         = 0;
+    System::Clock::Timestamp mMockSystemTimestamp = System::Clock::Milliseconds64(0);
+
+    NodeTimeoutPair * FindPair(ReadHandlerNode * node, size_t & position)
+    {
+        for (size_t i = 0; i < mPairArraySize; i++)
+        {
+            if (mPairArray[i].node == node)
+            {
+                position = i;
+                return &mPairArray[i];
+            }
+        }
+        return nullptr;
+    }
+
+    CHIP_ERROR insertPair(ReadHandlerNode * node, System::Clock::Timeout timeout)
+    {
+        VerifyOrReturnError(mPairArraySize < kNumMaxReadHandlers, CHIP_ERROR_NO_MEMORY);
+        mPairArray[mPairArraySize].node    = node;
+        mPairArray[mPairArraySize].timeout = timeout;
+        mPairArraySize++;
+
+        return CHIP_NO_ERROR;
+    }
+
+    void removePair(ReadHandlerNode * node)
+    {
+        size_t position;
+        NodeTimeoutPair * pair = FindPair(node, position);
+        VerifyOrReturn(pair != nullptr);
+
+        size_t nextPos = static_cast<size_t>(position + 1);
+        size_t moveNum = static_cast<size_t>(mPairArraySize - nextPos);
+
+        // Compress array after removal, if the removed position is not the last
+        if (moveNum)
+        {
+            memmove(&mPairArray[position], &mPairArray[nextPos], sizeof(NodeTimeoutPair) * moveNum);
+        }
+
+        mPairArraySize--;
+    }
+
+    static void TimerCallbackInterface(System::Layer * aLayer, void * aAppState)
+    {
+        // Normaly we would call the callback here, thus scheduling an engine run, but we don't need it for this test as we simulate
+        // all the callbacks related to report emissions. The actual callback should look like this:
+        //
+        // ReadHandlerNode * node = static_cast<ReadHandlerNode *>(aAppState);
+        // node->RunCallback();
+        ChipLogProgress(DataManagement, "Simluating engine run for Handler: %p", aAppState);
+    }
+    virtual CHIP_ERROR StartTimer(void * context, System::Clock::Timeout aTimeout) override
+    {
+        return insertPair(static_cast<ReadHandlerNode *>(context), aTimeout + mMockSystemTimestamp);
+    }
+    virtual void CancelTimer(void * context) override { removePair(static_cast<ReadHandlerNode *>(context)); }
+    virtual bool IsTimerActive(void * context) override
+    {
+        size_t position;
+        NodeTimeoutPair * pair = FindPair(static_cast<ReadHandlerNode *>(context), position);
+        VerifyOrReturnValue(pair != nullptr, false);
+
+        return pair->timeout > mMockSystemTimestamp;
+    }
+
+    virtual System::Clock::Timestamp GetCurrentMonotonicTimestamp() override { return mMockSystemTimestamp; }
+
+    void SetMockSystemTimestamp(System::Clock::Timestamp aMockTimestamp) { mMockSystemTimestamp = aMockTimestamp; }
+
+    // Increment the mock timestamp one milisecond at a time for a total of aTime miliseconds. Checks if
+    void IncrementMockTimestamp(System::Clock::Milliseconds64 aTime)
+    {
+        mMockSystemTimestamp = mMockSystemTimestamp + aTime;
+        for (size_t i = 0; i < mPairArraySize; i++)
+        {
+            if (mPairArray[i].timeout <= mMockSystemTimestamp)
+            {
+                TimerCallbackInterface(nullptr, mPairArray[i].node);
+            }
+        }
+    }
+};
+
+TestTimerDelegate sTestTimerDelegate;
+ReportSchedulerImpl sScheduler(&sTestTimerDelegate);
+
+class TestReportScheduler
+{
+public:
+    static void TestReadHandlerList(nlTestSuite * aSuite, void * aContext)
+    {
+        TestContext & ctx = *static_cast<TestContext *>(aContext);
+        NullReadHandlerCallback nullCallback;
+        // exchange context
+        Messaging::ExchangeContext * exchangeCtx = ctx.NewExchangeToAlice(nullptr, false);
+
+        // Read handler pool
+        ObjectPool<ReadHandler, kNumMaxReadHandlers> readHandlerPool;
+
+        // Initialize mock timestamp
+        sTestTimerDelegate.SetMockSystemTimestamp(Milliseconds64(0));
+
+        for (size_t i = 0; i < kNumMaxReadHandlers; i++)
+        {
+            ReadHandler * readHandler =
+                readHandlerPool.CreateObject(nullCallback, exchangeCtx, ReadHandler::InteractionType::Subscribe);
+            NL_TEST_ASSERT(aSuite, nullptr != readHandler);
+            VerifyOrReturn(nullptr != readHandler);
+            NL_TEST_ASSERT(aSuite, CHIP_NO_ERROR == sScheduler.RegisterReadHandler(readHandler));
+            NL_TEST_ASSERT(aSuite, nullptr != sScheduler.FindReadHandlerNode(readHandler));
+        }
+
+        NL_TEST_ASSERT(aSuite, readHandlerPool.Allocated() == kNumMaxReadHandlers);
+        NL_TEST_ASSERT(aSuite, sScheduler.GetNumReadHandlers() == kNumMaxReadHandlers);
+        NL_TEST_ASSERT(aSuite, ctx.GetExchangeManager().GetNumActiveExchanges() == 1);
+
+        // Test unregister first ReadHandler
+        ReadHandler * firstReadHandler = sScheduler.mReadHandlerList.begin()->GetReadHandler();
+        sScheduler.UnregisterReadHandler(firstReadHandler);
+        NL_TEST_ASSERT(aSuite, sScheduler.GetNumReadHandlers() == kNumMaxReadHandlers - 1);
+        NL_TEST_ASSERT(aSuite, nullptr == sScheduler.FindReadHandlerNode(firstReadHandler));
+
+        // Test unregister middle ReadHandler
+        auto iter = sScheduler.mReadHandlerList.begin();
+        for (size_t i = 0; i < static_cast<size_t>(kNumMaxReadHandlers / 2); i++)
+        {
+            iter++;
+        }
+        ReadHandler * middleReadHandler = iter->GetReadHandler();
+        sScheduler.UnregisterReadHandler(middleReadHandler);
+        NL_TEST_ASSERT(aSuite, sScheduler.GetNumReadHandlers() == kNumMaxReadHandlers - 2);
+        NL_TEST_ASSERT(aSuite, nullptr == sScheduler.FindReadHandlerNode(middleReadHandler));
+
+        // Test unregister last ReadHandler
+        iter = sScheduler.mReadHandlerList.end();
+        iter--;
+        ReadHandler * lastReadHandler = iter->GetReadHandler();
+        sScheduler.UnregisterReadHandler(lastReadHandler);
+        NL_TEST_ASSERT(aSuite, sScheduler.GetNumReadHandlers() == kNumMaxReadHandlers - 3);
+        NL_TEST_ASSERT(aSuite, nullptr == sScheduler.FindReadHandlerNode(lastReadHandler));
+
+        sScheduler.UnregisterAllHandlers();
+        // Confirm all ReadHandlers are unregistered from the scheduler
+        NL_TEST_ASSERT(aSuite, sScheduler.GetNumReadHandlers() == 0);
+        readHandlerPool.ForEachActiveObject([&](ReadHandler * handler) {
+            NL_TEST_ASSERT(aSuite, nullptr == sScheduler.FindReadHandlerNode(handler));
+            return Loop::Continue;
+        });
+
+        readHandlerPool.ReleaseAll();
+        exchangeCtx->Close();
+        NL_TEST_ASSERT(aSuite, ctx.GetExchangeManager().GetNumActiveExchanges() == 0);
+    }
+
+    static void TestReportTiming(nlTestSuite * aSuite, void * aContext)
+    {
+        TestContext & ctx = *static_cast<TestContext *>(aContext);
+        NullReadHandlerCallback nullCallback;
+        // exchange context
+        Messaging::ExchangeContext * exchangeCtx = ctx.NewExchangeToAlice(nullptr, false);
+
+        // Read handler pool
+        ObjectPool<ReadHandler, kNumMaxReadHandlers> readHandlerPool;
+
+        // Initialize mock timestamp
+        sTestTimerDelegate.SetMockSystemTimestamp(Milliseconds64(0));
+
+        // Dirty read handler, will be triggered at min interval
+        ReadHandler * readHandler1 =
+            readHandlerPool.CreateObject(nullCallback, exchangeCtx, ReadHandler::InteractionType::Subscribe);
+        NL_TEST_ASSERT(aSuite, CHIP_NO_ERROR == readHandler1->SetMaxReportingInterval(2));
+        NL_TEST_ASSERT(aSuite, CHIP_NO_ERROR == readHandler1->SetMinReportingIntervalForTests(1));
+        // Do those manually to avoid scheduling an engine run
+        readHandler1->mFlags.Set(ReadHandler::ReadHandlerFlags::ForceDirty, true);
+        readHandler1->mState = ReadHandler::HandlerState::GeneratingReports;
+
+        // Clean read handler, will be triggered at max interval
+        ReadHandler * readHandler2 =
+            readHandlerPool.CreateObject(nullCallback, exchangeCtx, ReadHandler::InteractionType::Subscribe);
+        NL_TEST_ASSERT(aSuite, CHIP_NO_ERROR == readHandler2->SetMaxReportingInterval(3));
+        NL_TEST_ASSERT(aSuite, CHIP_NO_ERROR == readHandler2->SetMinReportingIntervalForTests(0));
+        // Do those manually to avoid scheduling an engine run
+        readHandler2->mState = ReadHandler::HandlerState::GeneratingReports;
+
+        // Clean read handler, will be triggered at max interval, but will be cancelled before
+        ReadHandler * readHandler3 =
+            readHandlerPool.CreateObject(nullCallback, exchangeCtx, ReadHandler::InteractionType::Subscribe);
+        NL_TEST_ASSERT(aSuite, CHIP_NO_ERROR == readHandler3->SetMaxReportingInterval(3));
+        NL_TEST_ASSERT(aSuite, CHIP_NO_ERROR == readHandler3->SetMinReportingIntervalForTests(0));
+        // Do those manually to avoid scheduling an engine run
+        readHandler3->mState = ReadHandler::HandlerState::GeneratingReports;
+
+        NL_TEST_ASSERT(aSuite, CHIP_NO_ERROR == sScheduler.RegisterReadHandler(readHandler1));
+        NL_TEST_ASSERT(aSuite, CHIP_NO_ERROR == sScheduler.RegisterReadHandler(readHandler2));
+        NL_TEST_ASSERT(aSuite, CHIP_NO_ERROR == sScheduler.RegisterReadHandler(readHandler3));
+
+        // Confirms that none of the ReadHandlers are currently reportable
+        NL_TEST_ASSERT(aSuite, !sScheduler.IsReportableNow(readHandler1));
+        NL_TEST_ASSERT(aSuite, !sScheduler.IsReportableNow(readHandler2));
+        NL_TEST_ASSERT(aSuite, !sScheduler.IsReportableNow(readHandler3));
+
+        // Simulate system clock increment
+        sTestTimerDelegate.IncrementMockTimestamp(Milliseconds64(1100));
+
+        // Checks that the first ReadHandler is reportable after 1 second since it is dirty and min interval has expired
+        NL_TEST_ASSERT(aSuite, sScheduler.IsReportableNow(readHandler1));
+        NL_TEST_ASSERT(aSuite, !sScheduler.IsReportableNow(readHandler2));
+        NL_TEST_ASSERT(aSuite, !sScheduler.IsReportableNow(readHandler3));
+
+        NL_TEST_ASSERT(aSuite, sScheduler.IsReportScheduled(readHandler3));
+        sScheduler.CancelReport(readHandler3);
+        NL_TEST_ASSERT(aSuite, !sScheduler.IsReportScheduled(readHandler3));
+
+        // Simulate system clock increment
+        sTestTimerDelegate.IncrementMockTimestamp(Milliseconds64(2000));
+
+        // Checks that all ReadHandlers are reportable
+        NL_TEST_ASSERT(aSuite, sScheduler.IsReportableNow(readHandler1));
+        NL_TEST_ASSERT(aSuite, sScheduler.IsReportableNow(readHandler2));
+        // Even if its timer got cancelled, readHandler3 should still be considered reportable as the max interval has expired
+        // and it is in generating report state
+        NL_TEST_ASSERT(aSuite, sScheduler.IsReportableNow(readHandler3));
+
+        sScheduler.UnregisterAllHandlers();
+        readHandlerPool.ReleaseAll();
+        exchangeCtx->Close();
+        NL_TEST_ASSERT(aSuite, ctx.GetExchangeManager().GetNumActiveExchanges() == 0);
+    }
+
+    static void TestObserverCallbacks(nlTestSuite * aSuite, void * aContext)
+    {
+        TestContext & ctx = *static_cast<TestContext *>(aContext);
+        NullReadHandlerCallback nullCallback;
+        // exchange context
+        Messaging::ExchangeContext * exchangeCtx = ctx.NewExchangeToAlice(nullptr, false);
+
+        // Read handler pool
+        ObjectPool<ReadHandler, kNumMaxReadHandlers> readHandlerPool;
+
+        // Initialize mock timestamp
+        sTestTimerDelegate.SetMockSystemTimestamp(Milliseconds64(0));
+
+        ReadHandler * readHandler =
+            readHandlerPool.CreateObject(nullCallback, exchangeCtx, ReadHandler::InteractionType::Subscribe);
+        NL_TEST_ASSERT(aSuite, CHIP_NO_ERROR == readHandler->SetMaxReportingInterval(2));
+        NL_TEST_ASSERT(aSuite, CHIP_NO_ERROR == readHandler->SetMinReportingIntervalForTests(1));
+        // Do those manually to avoid scheduling an engine run
+        readHandler->mState = ReadHandler::HandlerState::GeneratingReports;
+        readHandler->SetObserver(&sScheduler);
+
+        // Test OnReadHandlerCreated
+        readHandler->mObserver->OnReadHandlerCreated(readHandler);
+        // Should have registered the read handler in the scheduler and scheduled a report
+        NL_TEST_ASSERT(aSuite, sScheduler.GetNumReadHandlers() == 1);
+        NL_TEST_ASSERT(aSuite, sScheduler.IsReportScheduled(readHandler));
+        ReadHandlerNode * node = sScheduler.FindReadHandlerNode(readHandler);
+        NL_TEST_ASSERT(aSuite, nullptr != node);
+        VerifyOrReturn(nullptr != node);
+        NL_TEST_ASSERT(aSuite, node->GetReadHandler() == readHandler);
+
+        // Test OnBecameReportable
+        readHandler->mFlags.Set(ReadHandler::ReadHandlerFlags::ForceDirty, true);
+        readHandler->mObserver->OnBecameReportable(readHandler);
+        // Should have changed the scheduled timeout to the handler's min interval, to check, we wait for the min interval to
+        // expire
+        // Simulate system clock increment
+        sTestTimerDelegate.IncrementMockTimestamp(Milliseconds64(1100));
+
+        // Check that no report is scheduled since the min interval has expired, the timer should now be stopped
+        NL_TEST_ASSERT(aSuite, !sScheduler.IsReportScheduled(readHandler));
+
+        // Test OnSubscriptionAction
+        readHandler->mFlags.Set(ReadHandler::ReadHandlerFlags::ForceDirty, false);
+        readHandler->mObserver->OnSubscriptionAction(readHandler);
+        // Should have changed the scheduled timeout to the handlers max interval, to check, we wait for the min interval to
+        // confirm it is not expired yet so the report should still be scheduled
+
+        NL_TEST_ASSERT(aSuite, sScheduler.IsReportScheduled(readHandler));
+        // Simulate system clock increment
+        sTestTimerDelegate.IncrementMockTimestamp(Milliseconds64(1100));
+
+        // Check that the report is still scheduled as the max interval has not expired yet and the dirty flag was cleared
+        NL_TEST_ASSERT(aSuite, sScheduler.IsReportScheduled(readHandler));
+        // Simulate system clock increment
+        sTestTimerDelegate.IncrementMockTimestamp(Milliseconds64(2100));
+
+        // Check that no report is scheduled since the max interval should have expired, the timer should now be stopped
+        NL_TEST_ASSERT(aSuite, !sScheduler.IsReportScheduled(readHandler));
+
+        // Test OnReadHandlerDestroyed
+        readHandler->mObserver->OnReadHandlerDestroyed(readHandler);
+        // Should have unregistered the read handler in the scheduler and cancelled the report
+        NL_TEST_ASSERT(aSuite, !sScheduler.IsReportScheduled(readHandler));
+        NL_TEST_ASSERT(aSuite, sScheduler.GetNumReadHandlers() == 0);
+        NL_TEST_ASSERT(aSuite, nullptr == sScheduler.FindReadHandlerNode(readHandler));
+
+        sScheduler.UnregisterReadHandler(readHandler);
+        readHandlerPool.ReleaseAll();
+        exchangeCtx->Close();
+        NL_TEST_ASSERT(aSuite, ctx.GetExchangeManager().GetNumActiveExchanges() == 0);
+    }
+};
+
+} // namespace reporting
+} // namespace app
+} // namespace chip
+
+namespace {
+
+/**
+ *   Test Suite. It lists all the test functions.
+ */
+
+static nlTest sTests[] = {
+    NL_TEST_DEF("TestReadHandlerList", chip::app::reporting::TestReportScheduler::TestReadHandlerList),
+    NL_TEST_DEF("TestReportTiming", chip::app::reporting::TestReportScheduler::TestReportTiming),
+    NL_TEST_DEF("TestObserverCallbacks", chip::app::reporting::TestReportScheduler::TestObserverCallbacks),
+    NL_TEST_SENTINEL(),
+};
+
+nlTestSuite sSuite = { "TestReportScheduler", &sTests[0], TestContext::Initialize, TestContext::Finalize };
+
+} // namespace
+
+int TestReportScheduler()
+{
+    return chip::ExecuteTestsWithContext<TestContext>(&sSuite);
+}
+
+CHIP_REGISTER_TEST_SUITE(TestReportScheduler);
diff --git a/src/controller/tests/data_model/TestRead.cpp b/src/controller/tests/data_model/TestRead.cpp
index 4bf9475..5c59c6c 100644
--- a/src/controller/tests/data_model/TestRead.cpp
+++ b/src/controller/tests/data_model/TestRead.cpp
@@ -302,7 +302,7 @@
 
         if (mAlterSubscriptionIntervals)
         {
-            ReturnErrorOnFailure(aReadHandler.SetReportingIntervals(mMaxInterval));
+            ReturnErrorOnFailure(aReadHandler.SetMaxReportingInterval(mMaxInterval));
         }
         return CHIP_NO_ERROR;
     }
diff --git a/src/platform/telink/ICDUtil.cpp b/src/platform/telink/ICDUtil.cpp
index fd2130c..b3dc9c8 100644
--- a/src/platform/telink/ICDUtil.cpp
+++ b/src/platform/telink/ICDUtil.cpp
@@ -36,5 +36,5 @@
         agreedMaxInterval = kSubscriptionMaxIntervalPublisherLimit;
     }
 
-    return aReadHandler.SetReportingIntervals(agreedMaxInterval);
+    return aReadHandler.SetMaxReportingInterval(agreedMaxInterval);
 }