[HVAC] Allow Thermostat delegate to supply atomic write timeout (#34823)

* Add support for Presets attributes and commands to the Thermostat cluster

Clean up the Thermostat cluster and remove the TemperatureSetpointHoldPolicy attribute
and SetTemperatureSetpointHoldPolicy command

* Restyled by whitespace

* Restyled by clang-format

* Restyled by gn.

* Fix build error for Linux configure build of all-clusters-app

* Fix Darwin CI issues

Editorial fixes

* Restyled by clang-format

* More fixes

* Restyled by clang-format

* BUILD.gn fixes for CI

* Apply suggestions from code review

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

* Address review comments.

* Restyled by clang-format

* Regenerate Thermostat XML from spec

* Move atomic enum to global-enums.xml, actually

# Conflicts:
#	src/app/zap-templates/zcl/data-model/chip/global-structs.xml

* Regenerate XML and convert thermostat-server to atomic writes

* Pull in ACCapacityFormat typo un-fix

* Update Test_TC_TSTAT_1_1 to know about AtomicResponse command.

* Restyled patch

* Fix weird merge with upstream

* Fix emberAfIsTypeSigned not understanding temperature type

* Merge fixes from atomic write branch

* Relocate thermostat-manager sample code to all-clusters-common

* Fix g++ build error on linux

* Fix C formatter for long int, cast whole expression

* Sync cast fix with master

* Add thermostat-common dependency to thermostat app under linux

* Remove MatterPostAttributeChangeCallback from thermostat-manager, as it conflicts with other implementations

* Convert Atomic enums and structs to global

* Restyled patch

* Apply suggestions from code review

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

* Regen with alchemy 0.6.1

* Updates based on comments

* Add TC_MCORE_FS_1_3.py test implementation (#34650)

* Fix most TC-SWTCH-2.4 remaining issues (#34677)

- Move 2.4 in a better place in the file
- Add test steps properly
- Allow default button press position override

Issue #34656

Testing done:

- Test still passes on DUT with automation

* Initial test script for Fabric Sync TC_MCORE_FS_1_2 (#34675)

* Initial test script for Fabric Sync TC_MCORE_FS_1_2

* Apply suggestions from code review

Co-authored-by: C Freeman <cecille@google.com>

* Address Review Comments

* Address review comments

* Fix default timeout after other timeouts changed

* Restyled by autopep8

* Fix linter error

---------

Co-authored-by: C Freeman <cecille@google.com>
Co-authored-by: Restyled.io <commits@restyled.io>

* Test automation for FabricSync ICD BridgedDeviceBasicInfoCluster (#34628)

* WIP Bridged ICD, commissioning to both fabrics

* wip testing sending KeepActive

* wip most steps implemented

* using SIGSTOP and SIGCONT to control ICD server pausing

* Update src/python_testing/TC_BRBINFO_4_1.py

Co-authored-by: Terence Hampson <thampson@google.com>

* comments addressed

* more comments addressed

* lint pass

* Update src/python_testing/TC_BRBINFO_4_1.py

Co-authored-by: C Freeman <cecille@google.com>

* comments addressed, incl TH_SERVER configurable

* added setupQRCode and setupManualCode as options for DUT commissioning

* Restyled by autopep8

* Restyled by isort

* Update src/python_testing/TC_BRBINFO_4_1.py

Co-authored-by: Terence Hampson <thampson@google.com>

* Update src/python_testing/TC_BRBINFO_4_1.py

Co-authored-by: Terence Hampson <thampson@google.com>

* Update src/python_testing/TC_BRBINFO_4_1.py

Co-authored-by: Terence Hampson <thampson@google.com>

* comments addressed

* Restyled by autopep8

---------

Co-authored-by: Terence Hampson <thampson@google.com>
Co-authored-by: C Freeman <cecille@google.com>
Co-authored-by: Restyled.io <commits@restyled.io>

* ServiceArea test scripts (#34548)

* initial commit

* fix bugs

* fix issues reported by the linter

* fix bug in checking for unique areaDesc

* add TC 1.5

* Update src/python_testing/TC_SEAR_1_2.py

Co-authored-by: William <hicklin@users.noreply.github.com>

* Update src/python_testing/TC_SEAR_1_2.py

Co-authored-by: William <hicklin@users.noreply.github.com>

* address code review comments

* fix issue introduced by the previous commit

* address code review feedback

* Update src/python_testing/TC_SEAR_1_2.py

Co-authored-by: Kiel Oleson <kielo@apple.com>

* address code review feedback

* remove PICS checked by the TC_SEAR_1.6

* more code review updates

* Restyled by autopep8

---------

Co-authored-by: William <hicklin@users.noreply.github.com>
Co-authored-by: Kiel Oleson <kielo@apple.com>
Co-authored-by: Restyled.io <commits@restyled.io>

* Remove manual tests for Thermostat presets (#34679)

* Dump details about leaked ExchangeContexts before aborting (#34617)

* Dump details about leaked ExchangeContexts before aborting

This is implemented via a VerifyOrDieWithObject() variant of the existing
VerifyOrDie() macro that calls a DumpToLog() method on the provided object if
it exists (otherwise this is simply a no-op).

If CHIP_CONFIG_VERBOSE_VERIFY_OR_DIE is not enabled, VerifyOrDieWithObject()
simply behaves like a plain VerifyOrDie(). DumpToLog() implementations can use
ChipLogFormatRtti to log type information about an object (usually a delegate);
if RTTI is disabled this simply outputs whether the object was null or not.

* Address review comments

* Make gcc happy and improve documentation

* Remove unused include

* Fix compile error without CHIP_CONFIG_VERBOSE_VERIFY_OR_DIE

* Avoid unused parameter warning

* [TI] CC13x4_26x4 build fixes (#34682)

* lwip pbuf, map file, and hex creation when OTA is disabled

* added cc13x4 family define around the non OTA hex creation

* whitespace fix

* reversed custom factoy data flash with cc13x4 check

* more whitespace fixes

* [ICD] Add missing polling function to NoWifi connectivity manager (#34684)

* Add missing polling function to NoWifi connectivity manager

* Update GenericConnectivityManagerImpl_NoWiFi.h

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

---------

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

* [OPSTATE] Add Q test script for CountdownTime (#34632)

* Add Q test

* Added test to test set

* Remove unused var

* Restyled by autopep8

* Restyled by isort

* Fix name

* Use pics over other method

* Removed unused stuff

* Added pipe commands

* Fix reset

* Get example to report appropriate changes.

* WiP

* Added some comments

* Changes to make things work

* Removed dev msgs

* Missed some

* Removed dev msgs

* Straggler

* Restyled by clang-format

* Restyled by autopep8

* Restyled by isort

* Commented unused var

* Update examples/all-clusters-app/linux/AllClustersCommandDelegate.cpp

* Fix bug

---------

Co-authored-by: Restyled.io <commits@restyled.io>

* YAML update to BRBINFO, ProductId (#34513)

* Bridged Device Information Cluster, Attribute ProductID test reflects marking as O, not X

* Update src/app/tests/suites/certification/Test_TC_BRBINFO_2_1.yaml

Co-authored-by: Terence Hampson <thampson@google.com>

* corrected pics

* corrected pics

* WIP Bridged ICD, commissioning to both fabrics

* wip testing sending KeepActive

* update to bridged-device-basic-information.xml and zap generated files

* removed unrelated file

---------

Co-authored-by: Terence Hampson <thampson@google.com>
Co-authored-by: Andrei Litvin <andy314@gmail.com>

* Fix simplified Linux tv-casting-app gn build error. (#34692)

* adding parallel execution to restyle-diff (#34663)

* adding parallel execution to restyle-diff

* using xargs to call restyle-paths

* fixing Copyright year

* restyle the restyler

* Add some bits to exercise global structs/enums to Unit Testing cluster. (#34540)

* Adds things to the Unit Testing cluster XML.
* This requires those things to be enabled in all-clusters-app,
  all-clusters-minimal-app, and one of the chef contact sensors to pass CI.
* That requires an implementation in test-cluster-server
* At which point might as well add a YAML test to exercise it all.

* [Silabs] Port platform specific Multi-Chip OTA work  (#34440)

* Pull request #1836: Cherry multi ota

Merge in WMN_TOOLS/matter from cherry-multi-ota to silabs_slc_1.3

Squashed commit of the following:

commit 4320bb46571658bc44fb82345348265def394991
Author: Michael Rupp <michael.rupp@silabs.com>
Date:   Fri May 10 14:26:07 2024 -0400

    remove some unwanted diffs in provision files

commit be160931dc600de7e7ead378b70d6a43c3945e46
Author: Michael Rupp <michael.rupp@silabs.com>
Date:   Fri May 10 14:24:25 2024 -0400

    revert changes to generator.project.mak

commit 14b6605887166e6d5284a61feb2bf407d850bdcf
Author: Michael Rupp <michael.rupp@silabs.com>
Date:   Fri May 10 13:06:12 2024 -0400

    revert NVM key changes and script changes

... and 8 more commits

* Restyled by whitespace

* Restyled by clang-format

* Restyled by gn

* Restyled by autopep8

* remove unused libs caught by linter

* update doctree with new readmes

* rerun CI, cirque failing for unknown reasons

* fix include guards in provision examples

* Restyled by clang-format

---------

Co-authored-by: Restyled.io <commits@restyled.io>

* Add python tests for Thermostat presets feature (#34693)

* Add python tests for Thermostat presets feature

* Restyled by autopep8

* Restyled by isort

* Update the PICS code for presets attribute

---------

Co-authored-by: Restyled.io <commits@restyled.io>

* removing unneccessary git fetch (#34698)

* Restyle patch

* Regen to fix ordering of global structs

* Apply suggestions from code review

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

* Return correct AtomicResponse when committing or rolling back

* Patch tests for atomic write of presets

* Fix tests to work with the new setup.

Specific changes:

* Enable SetActivePresetRequest command in all-clusters-app.
* Fix assignment of a PresetStructWithOwnedMembers to another
  PresetStructWithOwnedMembers to actually work correctly.
* Move constraint checks that happen on write from commit to write.
* Fix sending of atomic responses to not have use-stack-after-return.
* Fix PICS for the tests involved.

* Fix PICS values for atomic requests

* Remove PresetsSchedulesEditable and QueuedPreset from various places

* Restyled patch

* Restyled patch, again

* Remove PICS value for PresetsSchedulesEditable

* clang-tidy fixes

* clang-tidy fixes

* Clear associated atomic writes when fabric is removed

* Add tests for fabric removal and lockout of clients outside of atomic write

* Python linter

* Restyled patch

* Clear timer when fabric is removed

* Check for open atomic write before resetting

* Revert auto delegate declaration on lines where there's no collision

* Allow Thermostat delegate to provide timeout for atomic requests

* Apply suggestions from code review

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

* Document GetAtomicWriteTimeout

* Restyled patch

* Switch to enum for atomic write state

* Use std::optional<timeout> instead of magic zero value

---------

Co-authored-by: Nivedita Sarkar <nivedita_sarkar@apple.com>
Co-authored-by: Restyled.io <commits@restyled.io>
Co-authored-by: Nivi Sarkar <55898241+nivi-apple@users.noreply.github.com>
Co-authored-by: Boris Zbarsky <bzbarsky@apple.com>
Co-authored-by: Terence Hampson <thampson@google.com>
Co-authored-by: Tennessee Carmel-Veilleux <tennessee.carmelveilleux@gmail.com>
Co-authored-by: Chris Letnick <cletnick@google.com>
Co-authored-by: C Freeman <cecille@google.com>
Co-authored-by: Douglas Rocha Ferraz <rochaferraz@google.com>
Co-authored-by: Petru Lauric <81822411+plauric@users.noreply.github.com>
Co-authored-by: William <hicklin@users.noreply.github.com>
Co-authored-by: Kiel Oleson <kielo@apple.com>
Co-authored-by: Karsten Sperling <113487422+ksperling-apple@users.noreply.github.com>
Co-authored-by: Anu Biradar <104591549+abiradarti@users.noreply.github.com>
Co-authored-by: mkardous-silabs <84793247+mkardous-silabs@users.noreply.github.com>
Co-authored-by: Rob Bultman <rob.Bultman@gmail.com>
Co-authored-by: Andrei Litvin <andy314@gmail.com>
Co-authored-by: Shao Ling Tan <161761051+shaoltan-amazon@users.noreply.github.com>
Co-authored-by: Amine Alami <43780877+Alami-Amine@users.noreply.github.com>
Co-authored-by: Michael Rupp <95718139+mykrupp@users.noreply.github.com>
diff --git a/examples/thermostat/linux/include/thermostat-delegate-impl.h b/examples/thermostat/linux/include/thermostat-delegate-impl.h
index f559977..8252f22 100644
--- a/examples/thermostat/linux/include/thermostat-delegate-impl.h
+++ b/examples/thermostat/linux/include/thermostat-delegate-impl.h
@@ -44,6 +44,10 @@
 public:
     static inline ThermostatDelegate & GetInstance() { return sInstance; }
 
+    std::optional<System::Clock::Milliseconds16>
+    GetAtomicWriteTimeout(DataModel::DecodableList<chip::AttributeId> attributeRequests,
+                          System::Clock::Milliseconds16 timeoutRequest) override;
+
     CHIP_ERROR GetPresetTypeAtIndex(size_t index, Structs::PresetTypeStruct::Type & presetType) override;
 
     uint8_t GetNumberOfPresets() override;
diff --git a/examples/thermostat/linux/thermostat-delegate-impl.cpp b/examples/thermostat/linux/thermostat-delegate-impl.cpp
index 61d496f..c39a757 100644
--- a/examples/thermostat/linux/thermostat-delegate-impl.cpp
+++ b/examples/thermostat/linux/thermostat-delegate-impl.cpp
@@ -148,6 +148,47 @@
     return CHIP_NO_ERROR;
 }
 
+std::optional<System::Clock::Milliseconds16>
+ThermostatDelegate::GetAtomicWriteTimeout(DataModel::DecodableList<AttributeId> attributeRequests,
+                                          System::Clock::Milliseconds16 timeoutRequest)
+{
+    auto attributeIdsIter = attributeRequests.begin();
+    bool requestedPresets = false, requestedSchedules = false;
+    while (attributeIdsIter.Next())
+    {
+        auto & attributeId = attributeIdsIter.GetValue();
+
+        switch (attributeId)
+        {
+        case Attributes::Presets::Id:
+            requestedPresets = true;
+            break;
+        case Attributes::Schedules::Id:
+            requestedSchedules = true;
+            break;
+        default:
+            return System::Clock::Milliseconds16(0);
+        }
+    }
+    if (attributeIdsIter.GetStatus() != CHIP_NO_ERROR)
+    {
+        return System::Clock::Milliseconds16(0);
+    }
+    auto timeout = System::Clock::Milliseconds16(0);
+    if (requestedPresets)
+    {
+        // If the client expects to edit the presets, then we'll give it 3 seconds to do so
+        timeout += std::chrono::milliseconds(3000);
+    }
+    if (requestedSchedules)
+    {
+        // If the client expects to edit the schedules, then we'll give it 9 seconds to do so
+        timeout += std::chrono::milliseconds(9000);
+    }
+    // If the client requested an even smaller timeout, then use that one
+    return std::min(timeoutRequest, timeout);
+}
+
 void ThermostatDelegate::InitializePendingPresets()
 {
     mNextFreeIndexInPendingPresetsList = 0;
diff --git a/scripts/tools/zap/tests/outputs/all-clusters-app/app-templates/endpoint_config.h b/scripts/tools/zap/tests/outputs/all-clusters-app/app-templates/endpoint_config.h
index bb4e1a7..3146dd9 100644
--- a/scripts/tools/zap/tests/outputs/all-clusters-app/app-templates/endpoint_config.h
+++ b/scripts/tools/zap/tests/outputs/all-clusters-app/app-templates/endpoint_config.h
@@ -2217,6 +2217,7 @@
     };                                                                                                                             \
     const EmberAfGenericClusterFunction chipFuncArrayThermostatServer[] = {                                                        \
         (EmberAfGenericClusterFunction) emberAfThermostatClusterServerInitCallback,                                                \
+        (EmberAfGenericClusterFunction) MatterThermostatClusterServerShutdownCallback,                                             \
         (EmberAfGenericClusterFunction) MatterThermostatClusterServerPreAttributeChangedCallback,                                  \
     };                                                                                                                             \
     const EmberAfGenericClusterFunction chipFuncArrayFanControlServer[] = {                                                        \
@@ -3755,7 +3756,7 @@
       .attributes = ZAP_ATTRIBUTE_INDEX(616), \
       .attributeCount = 26, \
       .clusterSize = 72, \
-      .mask = ZAP_CLUSTER_MASK(SERVER) | ZAP_CLUSTER_MASK(INIT_FUNCTION) | ZAP_CLUSTER_MASK(PRE_ATTRIBUTE_CHANGED_FUNCTION), \
+      .mask = ZAP_CLUSTER_MASK(SERVER) | ZAP_CLUSTER_MASK(INIT_FUNCTION) | ZAP_CLUSTER_MASK(SHUTDOWN_FUNCTION) | ZAP_CLUSTER_MASK(PRE_ATTRIBUTE_CHANGED_FUNCTION), \
       .functions = chipFuncArrayThermostatServer, \
       .acceptedCommandList = ZAP_GENERATED_COMMANDS_INDEX( 241 ), \
       .generatedCommandList = ZAP_GENERATED_COMMANDS_INDEX( 246 ), \
diff --git a/src/app/clusters/thermostat-server/thermostat-delegate.h b/src/app/clusters/thermostat-server/thermostat-delegate.h
index 0c09b9d..c8c21d8 100644
--- a/src/app/clusters/thermostat-server/thermostat-delegate.h
+++ b/src/app/clusters/thermostat-server/thermostat-delegate.h
@@ -39,6 +39,17 @@
     virtual ~Delegate() = default;
 
     /**
+     * @brief Get the maximum timeout for atomically writing to a set of attributes
+     *
+     * @param[in] attributeRequests The list of attributes to write to.
+     * @param[out] timeoutRequest The timeout proposed by the client.
+     * @return The maximum allowed timeout; zero if the request is invalid.
+     */
+    virtual std::optional<System::Clock::Milliseconds16>
+    GetAtomicWriteTimeout(DataModel::DecodableList<AttributeId> attributeRequests,
+                          System::Clock::Milliseconds16 timeoutRequest) = 0;
+
+    /**
      * @brief Get the preset type at a given index in the PresetTypes attribute
      *
      * @param[in] index The index of the preset type in the list.
diff --git a/src/app/clusters/thermostat-server/thermostat-server.cpp b/src/app/clusters/thermostat-server/thermostat-server.cpp
index 6b8a50a..91c045c 100644
--- a/src/app/clusters/thermostat-server/thermostat-server.cpp
+++ b/src/app/clusters/thermostat-server/thermostat-server.cpp
@@ -27,6 +27,8 @@
 #include <app/CommandHandler.h>
 #include <app/ConcreteAttributePath.h>
 #include <app/ConcreteCommandPath.h>
+#include <app/server/Server.h>
+#include <app/util/endpoint-config-api.h>
 #include <lib/core/CHIPEncoding.h>
 #include <platform/internal/CHIPDeviceLayerInternal.h>
 
@@ -116,8 +118,7 @@
     VerifyOrReturn(delegate != nullptr, ChipLogError(Zcl, "Delegate is null. Unable to handle timer expired"));
 
     delegate->ClearPendingPresetList();
-    gThermostatAttrAccess.SetAtomicWrite(endpoint, false);
-    gThermostatAttrAccess.SetAtomicWriteScopedNodeId(endpoint, ScopedNodeId());
+    gThermostatAttrAccess.SetAtomicWrite(endpoint, ScopedNodeId(), kAtomicWriteState_Closed);
 }
 
 /**
@@ -205,8 +206,7 @@
         delegate->ClearPendingPresetList();
     }
     ClearTimer(endpoint);
-    gThermostatAttrAccess.SetAtomicWrite(endpoint, false);
-    gThermostatAttrAccess.SetAtomicWriteScopedNodeId(endpoint, ScopedNodeId());
+    gThermostatAttrAccess.SetAtomicWrite(endpoint, ScopedNodeId(), kAtomicWriteState_Closed);
 }
 
 /**
@@ -605,14 +605,16 @@
     }
 }
 
-void ThermostatAttrAccess::SetAtomicWrite(EndpointId endpoint, bool inProgress)
+void ThermostatAttrAccess::SetAtomicWrite(EndpointId endpoint, ScopedNodeId originatorNodeId, AtomicWriteState state)
 {
     uint16_t ep =
         emberAfGetClusterServerEndpointIndex(endpoint, Thermostat::Id, MATTER_DM_THERMOSTAT_CLUSTER_SERVER_ENDPOINT_COUNT);
 
-    if (ep < ArraySize(mAtomicWriteState))
+    if (ep < ArraySize(mAtomicWriteSessions))
     {
-        mAtomicWriteState[ep] = inProgress;
+        mAtomicWriteSessions[ep].state      = state;
+        mAtomicWriteSessions[ep].endpointId = endpoint;
+        mAtomicWriteSessions[ep].nodeId     = originatorNodeId;
     }
 }
 
@@ -622,9 +624,9 @@
     uint16_t ep =
         emberAfGetClusterServerEndpointIndex(endpoint, Thermostat::Id, MATTER_DM_THERMOSTAT_CLUSTER_SERVER_ENDPOINT_COUNT);
 
-    if (ep < ArraySize(mAtomicWriteState))
+    if (ep < ArraySize(mAtomicWriteSessions))
     {
-        inAtomicWrite = mAtomicWriteState[ep];
+        inAtomicWrite = (mAtomicWriteSessions[ep].state == kAtomicWriteState_Open);
     }
     return inAtomicWrite;
 }
@@ -649,26 +651,15 @@
     return GetAtomicWriteScopedNodeId(endpoint) == sourceNodeId;
 }
 
-void ThermostatAttrAccess::SetAtomicWriteScopedNodeId(EndpointId endpoint, ScopedNodeId originatorNodeId)
-{
-    uint16_t ep =
-        emberAfGetClusterServerEndpointIndex(endpoint, Thermostat::Id, MATTER_DM_THERMOSTAT_CLUSTER_SERVER_ENDPOINT_COUNT);
-
-    if (ep < ArraySize(mAtomicWriteNodeIds))
-    {
-        mAtomicWriteNodeIds[ep] = originatorNodeId;
-    }
-}
-
 ScopedNodeId ThermostatAttrAccess::GetAtomicWriteScopedNodeId(EndpointId endpoint)
 {
     ScopedNodeId originatorNodeId = ScopedNodeId();
     uint16_t ep =
         emberAfGetClusterServerEndpointIndex(endpoint, Thermostat::Id, MATTER_DM_THERMOSTAT_CLUSTER_SERVER_ENDPOINT_COUNT);
 
-    if (ep < ArraySize(mAtomicWriteNodeIds))
+    if (ep < ArraySize(mAtomicWriteSessions))
     {
-        originatorNodeId = mAtomicWriteNodeIds[ep];
+        originatorNodeId = mAtomicWriteSessions[ep].nodeId;
     }
     return originatorNodeId;
 }
@@ -704,7 +695,7 @@
         }
         break;
     case PresetTypes::Id: {
-        Delegate * delegate = GetDelegate(aPath.mEndpointId);
+        auto delegate = GetDelegate(aPath.mEndpointId);
         VerifyOrReturnError(delegate != nullptr, CHIP_ERROR_INCORRECT_STATE, ChipLogError(Zcl, "Delegate is null"));
 
         return aEncoder.EncodeList([delegate](const auto & encoder) -> CHIP_ERROR {
@@ -723,14 +714,14 @@
     }
     break;
     case NumberOfPresets::Id: {
-        Delegate * delegate = GetDelegate(aPath.mEndpointId);
+        auto delegate = GetDelegate(aPath.mEndpointId);
         VerifyOrReturnError(delegate != nullptr, CHIP_ERROR_INCORRECT_STATE, ChipLogError(Zcl, "Delegate is null"));
 
         ReturnErrorOnFailure(aEncoder.Encode(delegate->GetNumberOfPresets()));
     }
     break;
     case Presets::Id: {
-        Delegate * delegate = GetDelegate(aPath.mEndpointId);
+        auto delegate = GetDelegate(aPath.mEndpointId);
         VerifyOrReturnError(delegate != nullptr, CHIP_ERROR_INCORRECT_STATE, ChipLogError(Zcl, "Delegate is null"));
 
         auto & subjectDescriptor = aEncoder.GetSubjectDescriptor();
@@ -766,7 +757,7 @@
     }
     break;
     case ActivePresetHandle::Id: {
-        Delegate * delegate = GetDelegate(aPath.mEndpointId);
+        auto delegate = GetDelegate(aPath.mEndpointId);
         VerifyOrReturnError(delegate != nullptr, CHIP_ERROR_INCORRECT_STATE, ChipLogError(Zcl, "Delegate is null"));
 
         uint8_t buffer[kPresetHandleSize];
@@ -812,7 +803,7 @@
     {
     case Presets::Id: {
 
-        Delegate * delegate = GetDelegate(endpoint);
+        auto delegate = GetDelegate(endpoint);
         VerifyOrReturnError(delegate != nullptr, CHIP_ERROR_INCORRECT_STATE, ChipLogError(Zcl, "Delegate is null"));
 
         // Presets are not editable, return INVALID_IN_STATE.
@@ -897,7 +888,7 @@
     return CHIP_NO_ERROR;
 }
 
-CHIP_ERROR ThermostatAttrAccess::AppendPendingPreset(Delegate * delegate, const PresetStruct::Type & preset)
+CHIP_ERROR ThermostatAttrAccess::AppendPendingPreset(Thermostat::Delegate * delegate, const PresetStruct::Type & preset)
 {
     if (!IsValidPresetEntry(preset))
     {
@@ -951,6 +942,23 @@
     return delegate->AppendToPendingPresetList(preset);
 }
 
+void ThermostatAttrAccess::OnFabricRemoved(const FabricTable & fabricTable, FabricIndex fabricIndex)
+{
+    for (size_t i = 0; i < ArraySize(mAtomicWriteSessions); ++i)
+    {
+        auto atomicWriteState = mAtomicWriteSessions[i];
+        if (atomicWriteState.state == kAtomicWriteState_Open && atomicWriteState.nodeId.GetFabricIndex() == fabricIndex)
+        {
+            auto delegate = GetDelegate(atomicWriteState.endpointId);
+            if (delegate == nullptr)
+            {
+                continue;
+            }
+            resetAtomicWrite(delegate, atomicWriteState.endpointId);
+        }
+    }
+}
+
 } // namespace Thermostat
 } // namespace Clusters
 } // namespace app
@@ -1394,8 +1402,6 @@
         return;
     }
 
-    auto timeout = commandData.timeout.Value();
-
     if (!validAtomicAttributes(commandData, false))
     {
         commandObj->AddStatus(commandPath, imcode::InvalidCommand);
@@ -1412,13 +1418,18 @@
     // needs to keep track of a pending preset list now.
     delegate->InitializePendingPresets();
 
-    uint16_t maxTimeout = 5000;
-    timeout             = std::min(timeout, maxTimeout);
+    auto timeout =
+        delegate->GetAtomicWriteTimeout(commandData.attributeRequests, System::Clock::Milliseconds16(commandData.timeout.Value()));
 
-    ScheduleTimer(endpoint, System::Clock::Milliseconds16(timeout));
-    gThermostatAttrAccess.SetAtomicWrite(endpoint, true);
-    gThermostatAttrAccess.SetAtomicWriteScopedNodeId(endpoint, GetSourceScopedNodeId(commandObj));
-    sendAtomicResponse(commandObj, commandPath, imcode::Success, imcode::Success, imcode::Success, MakeOptional(timeout));
+    if (!timeout.has_value())
+    {
+        commandObj->AddStatus(commandPath, imcode::InvalidCommand);
+        return;
+    }
+    ScheduleTimer(endpoint, timeout.value());
+    gThermostatAttrAccess.SetAtomicWrite(endpoint, GetSourceScopedNodeId(commandObj), kAtomicWriteState_Open);
+    sendAtomicResponse(commandObj, commandPath, imcode::Success, imcode::Success, imcode::Success,
+                       MakeOptional(timeout.value().count()));
 }
 
 imcode commitPresets(Delegate * delegate, EndpointId endpoint)
@@ -1868,5 +1879,17 @@
 
 void MatterThermostatPluginServerInitCallback()
 {
+    Server::GetInstance().GetFabricTable().AddFabricDelegate(&gThermostatAttrAccess);
     AttributeAccessInterfaceRegistry::Instance().Register(&gThermostatAttrAccess);
 }
+
+void MatterThermostatClusterServerShutdownCallback(EndpointId endpoint)
+{
+    ChipLogProgress(Zcl, "Shutting down thermostat server cluster on endpoint %d", endpoint);
+    Delegate * delegate = GetDelegate(endpoint);
+
+    if (delegate != nullptr)
+    {
+        resetAtomicWrite(delegate, endpoint);
+    }
+}
diff --git a/src/app/clusters/thermostat-server/thermostat-server.h b/src/app/clusters/thermostat-server/thermostat-server.h
index 306a2a6..ddede8a 100644
--- a/src/app/clusters/thermostat-server/thermostat-server.h
+++ b/src/app/clusters/thermostat-server/thermostat-server.h
@@ -37,10 +37,15 @@
 static constexpr size_t kThermostatEndpointCount =
     MATTER_DM_THERMOSTAT_CLUSTER_SERVER_ENDPOINT_COUNT + CHIP_DEVICE_CONFIG_DYNAMIC_ENDPOINT_COUNT;
 
+enum AtomicWriteState
+{
+    kAtomicWriteState_Closed = 0,
+    kAtomicWriteState_Open,
+};
 /**
  * @brief  Thermostat Attribute Access Interface.
  */
-class ThermostatAttrAccess : public chip::app::AttributeAccessInterface
+class ThermostatAttrAccess : public chip::app::AttributeAccessInterface, public chip::FabricTable::Delegate
 {
 public:
     ThermostatAttrAccess() : AttributeAccessInterface(Optional<chip::EndpointId>::Missing(), Thermostat::Id) {}
@@ -49,15 +54,6 @@
     CHIP_ERROR Write(const ConcreteDataAttributePath & aPath, chip::app::AttributeValueDecoder & aDecoder) override;
 
     /**
-     * @brief Sets the scoped node id of the originator that sent the last successful
-     *        AtomicRequest of type BeginWrite for the given endpoint.
-     *
-     * @param[in] endpoint The endpoint.
-     * @param[in] originatorNodeId The originator scoped node id.
-     */
-    void SetAtomicWriteScopedNodeId(EndpointId endpoint, ScopedNodeId originatorNodeId);
-
-    /**
      * @brief Gets the scoped node id of the originator that sent the last successful
      *        AtomicRequest of type BeginWrite for the given endpoint.
      *
@@ -68,12 +64,13 @@
     ScopedNodeId GetAtomicWriteScopedNodeId(EndpointId endpoint);
 
     /**
-     * @brief Sets whether an atomic write is in progress for the given endpoint
+     * @brief Sets the atomic write state for the given endpoint and originatorNodeId
      *
      * @param[in] endpoint The endpoint.
-     * @param[in] inProgress Whether or not an atomic write is in progress.
+     * @param[in] originatorNodeId The originator scoped node id.
+     * @param[in] state Whether or not an atomic write is open or closed.
      */
-    void SetAtomicWrite(EndpointId endpoint, bool inProgress);
+    void SetAtomicWrite(EndpointId endpoint, ScopedNodeId originatorNodeId, AtomicWriteState state);
 
     /**
      * @brief Gets whether an atomic write is in progress for the given endpoint
@@ -105,10 +102,18 @@
     bool InAtomicWrite(CommandHandler * commandObj, EndpointId endpoint);
 
 private:
-    CHIP_ERROR AppendPendingPreset(Delegate * delegate, const Structs::PresetStruct::Type & preset);
+    CHIP_ERROR AppendPendingPreset(Thermostat::Delegate * delegate, const Structs::PresetStruct::Type & preset);
 
-    ScopedNodeId mAtomicWriteNodeIds[kThermostatEndpointCount];
-    bool mAtomicWriteState[kThermostatEndpointCount];
+    void OnFabricRemoved(const FabricTable & fabricTable, FabricIndex fabricIndex) override;
+
+    struct AtomicWriteSession
+    {
+        AtomicWriteState state = kAtomicWriteState_Closed;
+        ScopedNodeId nodeId;
+        EndpointId endpointId = kInvalidEndpointId;
+    };
+
+    AtomicWriteSession mAtomicWriteSessions[kThermostatEndpointCount];
 };
 
 /**
diff --git a/src/app/common/templates/config-data.yaml b/src/app/common/templates/config-data.yaml
index d8d276f..660ca0b 100644
--- a/src/app/common/templates/config-data.yaml
+++ b/src/app/common/templates/config-data.yaml
@@ -81,6 +81,7 @@
     - Color Control
     - Sample MEI
     - Scenes Management
+    - Thermostat
 
 ClustersWithPreAttributeChangeFunctions:
     - Door Lock
diff --git a/src/python_testing/TC_TSTAT_4_2.py b/src/python_testing/TC_TSTAT_4_2.py
index 58d78ef..9cb7336 100644
--- a/src/python_testing/TC_TSTAT_4_2.py
+++ b/src/python_testing/TC_TSTAT_4_2.py
@@ -30,6 +30,7 @@
 import logging
 
 import chip.clusters as Clusters
+from chip import ChipDeviceCtrl  # Needed before chip.FabricAdmin
 from chip.clusters import Globals
 from chip.clusters.Types import NullValue
 from chip.interaction_model import InteractionModelError, Status
@@ -58,53 +59,76 @@
 
 class TC_TSTAT_4_2(MatterBaseTest):
 
-    async def write_presets(self, endpoint, presets) -> Status:
-        result = await self.default_controller.WriteAttribute(self.dut_node_id, [(endpoint, cluster.Attributes.Presets(presets))])
-        return result[0].Status
+    def check_atomic_response(self, response: object, expected_status: Status = Status.Success,
+                              expected_overall_status: Status = Status.Success,
+                              expected_preset_status: Status = Status.Success):
+        asserts.assert_equal(expected_status, Status.Success, "We expected we had a valid response")
+        asserts.assert_equal(response.statusCode, expected_overall_status, "Response should have the right overall status")
+        found_preset_status = False
+        for attrStatus in response.attributeStatus:
+            if attrStatus.attributeID == cluster.Attributes.Presets.attribute_id:
+                asserts.assert_equal(attrStatus.statusCode, expected_preset_status,
+                                     "Preset attribute should have the right status")
+                found_preset_status = True
+        asserts.assert_true(found_preset_status, "Preset attribute should have a status")
 
-    async def send_edit_atomic_request_begin_command(self,
-                                                     endpoint: int = None,
-                                                     expected_status: Status = Status.Success):
+    async def write_presets(self,
+                            endpoint,
+                            presets,
+                            dev_ctrl: ChipDeviceCtrl = None,
+                            expected_status: Status = Status.Success) -> Status:
+        if dev_ctrl is None:
+            dev_ctrl = self.default_controller
+        result = await dev_ctrl.WriteAttribute(self.dut_node_id, [(endpoint, cluster.Attributes.Presets(presets))])
+        status = result[0].Status
+        asserts.assert_equal(status, expected_status, f"Presets write returned {status.name}; expected {expected_status.name}")
+        return status
+
+    async def send_atomic_request_begin_command(self,
+                                                dev_ctrl: ChipDeviceCtrl = None,
+                                                endpoint: int = None,
+                                                expected_status: Status = Status.Success,
+                                                expected_overall_status: Status = Status.Success,
+                                                expected_preset_status: Status = Status.Success):
         try:
-            await self.send_single_cmd(cmd=cluster.Commands.AtomicRequest(requestType=Globals.Enums.AtomicRequestTypeEnum.kBeginWrite,
-                                                                          attributeRequests=[
-                                                                              cluster.Attributes.Presets.attribute_id],
-                                                                          timeout=1800),
-                                       endpoint=endpoint)
-            asserts.assert_equal(expected_status, Status.Success)
+            response = await self.send_single_cmd(cmd=cluster.Commands.AtomicRequest(requestType=Globals.Enums.AtomicRequestTypeEnum.kBeginWrite,
+                                                                                     attributeRequests=[
+                                                                                         cluster.Attributes.Presets.attribute_id],
+                                                                                     timeout=1800),
+                                                  dev_ctrl=dev_ctrl,
+                                                  endpoint=endpoint)
+            self.check_atomic_response(response, expected_status, expected_overall_status, expected_preset_status)
 
         except InteractionModelError as e:
             asserts.assert_equal(e.status, expected_status, "Unexpected error returned")
 
-    async def send_edit_atomic_request_commit_command(self,
-                                                      endpoint: int = None,
-                                                      expected_status: Status = Status.Success,
-                                                      expected_overall_status: Status = Status.Success,
-                                                      expected_preset_status: Status = Status.Success):
+    async def send_atomic_request_commit_command(self,
+                                                 dev_ctrl: ChipDeviceCtrl = None,
+                                                 endpoint: int = None,
+                                                 expected_status: Status = Status.Success,
+                                                 expected_overall_status: Status = Status.Success,
+                                                 expected_preset_status: Status = Status.Success):
         try:
             response = await self.send_single_cmd(cmd=cluster.Commands.AtomicRequest(requestType=Globals.Enums.AtomicRequestTypeEnum.kCommitWrite,
                                                                                      attributeRequests=[cluster.Attributes.Presets.attribute_id, cluster.Attributes.Schedules.attribute_id]),
+                                                  dev_ctrl=dev_ctrl,
                                                   endpoint=endpoint)
-            asserts.assert_equal(expected_status, Status.Success, "We expected we had a valid commit command")
-            asserts.assert_equal(response.statusCode, expected_overall_status, "Commit should have the right overall status")
-            found_preset_status = False
-            for attrStatus in response.attributeStatus:
-                if attrStatus.attributeID == cluster.Attributes.Presets.attribute_id:
-                    asserts.assert_equal(attrStatus.statusCode, expected_preset_status,
-                                         "Preset attribute commit should have the right status")
-                    found_preset_status = True
-            asserts.assert_true(found_preset_status, "Preset attribute commit should have a status")
+            self.check_atomic_response(response, expected_status, expected_overall_status, expected_preset_status)
         except InteractionModelError as e:
             asserts.assert_equal(e.status, expected_status, "Unexpected error returned")
 
-    async def send_edit_atomic_request_rollback_command(self,
-                                                        endpoint: int = None,
-                                                        expected_status: Status = Status.Success):
+    async def send_atomic_request_rollback_command(self,
+                                                   dev_ctrl: ChipDeviceCtrl = None,
+                                                   endpoint: int = None,
+                                                   expected_status: Status = Status.Success,
+                                                   expected_overall_status: Status = Status.Success,
+                                                   expected_preset_status: Status = Status.Success):
         try:
-            await self.send_single_cmd(cmd=cluster.Commands.AtomicRequest(requestType=Globals.Enums.AtomicRequestTypeEnum.kRollbackWrite,
-                                                                          attributeRequests=[cluster.Attributes.Presets.attribute_id, cluster.Attributes.Schedules.attribute_id]),
-                                       endpoint=endpoint)
-            asserts.assert_equal(expected_status, Status.Success)
+            response = await self.send_single_cmd(cmd=cluster.Commands.AtomicRequest(requestType=Globals.Enums.AtomicRequestTypeEnum.kRollbackWrite,
+                                                                                     attributeRequests=[cluster.Attributes.Presets.attribute_id, cluster.Attributes.Schedules.attribute_id]),
+                                                  dev_ctrl=dev_ctrl,
+                                                  endpoint=endpoint)
+            self.check_atomic_response(response, expected_status, expected_overall_status, expected_preset_status)
         except InteractionModelError as e:
             asserts.assert_equal(e.status, expected_status, "Unexpected error returned")
 
@@ -134,39 +158,57 @@
                      is_commissioning=True),
             TestStep("2", "TH writes to the Presets attribute without calling the AtomicRequest command",
                      " Verify that the write request returns INVALID_IN_STATE error since the client didn't send a request to edit the presets by calling AtomicRequest command."),
-            TestStep("3", "TH writes to the Presets attribute after calling the AtomicRequest command but doesn't call CommitPresetsSchedulesRequest to commit",
-                     "Verify that the Presets attribute was not updated since CommitPresetsSchedulesRequest command was not called."),
-            TestStep("4", "TH writes to the Presets attribute after calling the AtomicRequest command and calls CommitPresetsSchedulesRequest to commit",
+            TestStep("3", "TH writes to the Presets attribute after calling the AtomicRequest begin command but doesn't call AtomicRequest commit",
+                     "Verify that the Presets attribute was not updated since AtomicRequest commit command was not called."),
+            TestStep("4", "TH writes to the Presets attribute after calling the AtomicRequest begin command and calls AtomicRequest commit",
                      "Verify that the Presets attribute was updated with new presets."),
             TestStep("5", "TH writes to the Presets attribute with a built-in preset removed",
-                     "Verify that the CommitPresetsSchedulesRequest returned UNSUPPORTED_ACCESS (0x7e)."),
+                     "Verify that the AtomicRequest commit returned CONSTRAINT_ERROR (0x87)."),
             TestStep("6", "TH writes to the Presets attribute with a preset removed whose handle matches the value in the ActivePresetHandle attribute",
-                     "Verify that the CommitPresetsSchedulesRequest returned INVALID_IN_STATE (0xcb)."),
+                     "Verify that the AtomicRequest commit returned INVALID_IN_STATE (0xcb)."),
             TestStep("7", "TH writes to the Presets attribute with a built-in preset modified to be not built-in",
-                     "Verify that the CommitPresetsSchedulesRequest returned UNSUPPORTED_ACCESS (0x7e)."),
+                     "Verify that the AtomicRequest commit returned CONSTRAINT_ERROR (0x87)."),
             TestStep("8", "TH writes to the Presets attribute with a new preset having builtIn set to true",
-                     "Verify that the CommitPresetsSchedulesRequest returned CONSTRAINT_ERROR (0x87)."),
+                     "Verify that the AtomicRequest commit returned CONSTRAINT_ERROR (0x87)."),
             TestStep("9", "TH writes to the Presets attribute with a new preset having a preset handle that doesn't exist in the Presets attribute",
-                     "Verify that the CommitPresetsSchedulesRequest returned NOT_FOUND (0x8b)."),
+                     "Verify that the AtomicRequest commit returned NOT_FOUND (0x8b)."),
             TestStep("10", "TH writes to the Presets attribute with duplicate presets",
-                     "Verify that the CommitPresetsSchedulesRequest returned CONSTRAINT_ERROR (0x87)."),
+                     "Verify that the AtomicRequest commit returned CONSTRAINT_ERROR (0x87)."),
             TestStep("11", "TH writes to the Presets attribute with a non built-in preset modified to be built-in",
-                     "Verify that the CommitPresetsSchedulesRequest returned UNSUPPORTED_ACCESS (0x7e)."),
+                     "Verify that the AtomicRequest commit returned CONSTRAINT_ERROR (0x87)."),
             TestStep("12", "TH writes to the Presets attribute with a preset that doesn't support names in the PresetTypeFeatures bitmap but has a name",
-                     "Verify that the CommitPresetsSchedulesRequest returned CONSTRAINT_ERROR (0x87)."),
-            TestStep("13", "TH writes to the Presets attribute but calls the CancelPresetsSchedulesEditRequest command to cancel the edit request",
-                     "Verify that the edit request was cancelled"),
+                     "Verify that the AtomicRequest commit returned CONSTRAINT_ERROR (0x87)."),
+            TestStep("13", "TH writes to the Presets attribute but calls the AtomicRequest rollback command to cancel the edit request",
+                     "Verify that the edit request was rolled back"),
+            TestStep("14", "TH starts an atomic write, and TH2 attempts to open an atomic write before TH is complete",
+                     "Verify that the atomic request is rejected"),
+            TestStep("15", "TH starts an atomic write, and TH2 attempts to write to presets",
+                     "Verify that the write request is rejected"),
+            TestStep("16", "TH starts an atomic write, and before it's complete, TH2 removes TH's fabric; TH2 then opens an atomic write",
+                     "Verify that the atomic request is successful"),
         ]
 
         return steps
 
-    @async_test_body
+    @ async_test_body
     async def test_TC_TSTAT_4_2(self):
         endpoint = self.user_params.get("endpoint", 1)
 
         self.step("1")
         # Commission DUT - already done
 
+        logger.info("Commissioning under second controller")
+        params = await self.default_controller.OpenCommissioningWindow(
+            nodeid=self.dut_node_id, timeout=600, iteration=10000, discriminator=1234, option=1)
+
+        secondary_authority = self.certificate_authority_manager.NewCertificateAuthority()
+        secondary_fabric_admin = secondary_authority.NewFabricAdmin(vendorId=0xFFF1, fabricId=2)
+        secondary_controller = secondary_fabric_admin.NewController(nodeId=112233)
+
+        await secondary_controller.CommissionOnNetwork(
+            nodeId=self.dut_node_id, setupPinCode=params.setupPinCode,
+            filterType=ChipDeviceCtrl.DiscoveryFilterType.LONG_DISCRIMINATOR, filter=1234)
+
         self.step("2")
         if self.pics_guard(self.check_pics("TSTAT.S.F08") and self.check_pics("TSTAT.S.A0050")):
             presets = await self.read_single_attribute_check_success(endpoint=endpoint, cluster=cluster, attribute=cluster.Attributes.Presets)
@@ -174,13 +216,11 @@
             asserts.assert_equal(presets, initial_presets, "Presets do not match initial value")
 
             # Write to the presets attribute without calling AtomicRequest command
-            status = await self.write_presets(endpoint=endpoint, presets=new_presets)
-            status_ok = (status == Status.InvalidInState)
-            asserts.assert_true(status_ok, "Presets write did not return InvalidInState as expected")
+            await self.write_presets(endpoint=endpoint, presets=new_presets, expected_status=Status.InvalidInState)
 
         self.step("3")
         if self.pics_guard(self.check_pics("TSTAT.S.F08") and self.check_pics("TSTAT.S.A0050") and self.check_pics("TSTAT.S.CFE.Rsp")):
-            await self.send_edit_atomic_request_begin_command()
+            await self.send_atomic_request_begin_command()
 
             # Write to the presets attribute after calling AtomicRequest command
             status = await self.write_presets(endpoint=endpoint, presets=new_presets)
@@ -192,7 +232,7 @@
             logger.info(f"Rx'd Presets: {presets}")
             asserts.assert_equal(presets, new_presets_with_handle, "Presets were updated, as expected")
 
-            await self.send_edit_atomic_request_rollback_command()
+            await self.send_atomic_request_rollback_command()
 
             # Read the presets attribute and verify it has been properly rolled back
             presets = await self.read_single_attribute_check_success(endpoint=endpoint, cluster=cluster, attribute=cluster.Attributes.Presets)
@@ -202,15 +242,13 @@
         if self.pics_guard(self.check_pics("TSTAT.S.F08") and self.check_pics("TSTAT.S.A0050") and self.check_pics("TSTAT.S.CFE.Rsp")):
 
             # Send the AtomicRequest begin command
-            await self.send_edit_atomic_request_begin_command()
+            await self.send_atomic_request_begin_command()
 
             # Write to the presets attribute after calling AtomicRequest command
-            status = await self.write_presets(endpoint=endpoint, presets=new_presets)
-            status_ok = (status == Status.Success)
-            asserts.assert_true(status_ok, "Presets write did not return Success as expected")
+            await self.write_presets(endpoint=endpoint, presets=new_presets)
 
             # Send the AtomicRequest commit command
-            await self.send_edit_atomic_request_commit_command()
+            await self.send_atomic_request_commit_command()
 
             # Read the presets attribute and verify it was updated since AtomicRequest commit was called after writing presets
             presets = await self.read_single_attribute_check_success(endpoint=endpoint, cluster=cluster, attribute=cluster.Attributes.Presets)
@@ -221,17 +259,15 @@
         if self.pics_guard(self.check_pics("TSTAT.S.F08") and self.check_pics("TSTAT.S.A0050") and self.check_pics("TSTAT.S.CFE.Rsp")):
 
             # Send the AtomicRequest begin command
-            await self.send_edit_atomic_request_begin_command()
+            await self.send_atomic_request_begin_command()
 
             # Write to the presets attribute after removing a built in preset from the list. Remove the first entry.
             test_presets = new_presets_with_handle.copy()
             test_presets.pop(0)
-            status = await self.write_presets(endpoint=endpoint, presets=test_presets)
-            status_ok = (status == Status.Success)
-            asserts.assert_true(status_ok, "Presets write did not return Success as expected")
+            await self.write_presets(endpoint=endpoint, presets=test_presets)
 
             # Send the AtomicRequest commit command and expect ConstraintError for presets.
-            await self.send_edit_atomic_request_commit_command(expected_overall_status=Status.Failure, expected_preset_status=Status.ConstraintError)
+            await self.send_atomic_request_commit_command(expected_overall_status=Status.Failure, expected_preset_status=Status.ConstraintError)
 
         self.step("6")
         if self.pics_guard(self.check_pics("TSTAT.S.F08") and self.check_pics("TSTAT.S.A0050") and self.check_pics("TSTAT.S.C06.Rsp") and self.check_pics("TSTAT.S.CFE.Rsp")):
@@ -245,144 +281,164 @@
             asserts.assert_equal(activePresetHandle, b'\x03', "Active preset handle was not updated as expected")
 
             # Send the AtomicRequest begin command
-            await self.send_edit_atomic_request_begin_command()
+            await self.send_atomic_request_begin_command()
 
             # Write to the presets attribute after removing the preset that was set as the active preset handle. Remove the last entry with preset handle (b'\x03')
             test_presets = new_presets_with_handle.copy()
             del test_presets[-1]
-            status = await self.write_presets(endpoint=endpoint, presets=test_presets)
-            status_ok = (status == Status.Success)
-            asserts.assert_true(status_ok, "Presets write did not return Success as expected")
+            await self.write_presets(endpoint=endpoint, presets=test_presets)
 
             # Send the AtomicRequest commit command and expect InvalidInState for presets.
-            await self.send_edit_atomic_request_commit_command(expected_overall_status=Status.Failure, expected_preset_status=Status.InvalidInState)
+            await self.send_atomic_request_commit_command(expected_overall_status=Status.Failure, expected_preset_status=Status.InvalidInState)
 
         self.step("7")
         if self.pics_guard(self.check_pics("TSTAT.S.F08") and self.check_pics("TSTAT.S.A0050") and self.check_pics("TSTAT.S.CFE.Rsp")):
 
             # Send the AtomicRequest begin command
-            await self.send_edit_atomic_request_begin_command()
+            await self.send_atomic_request_begin_command()
 
             # Write to the presets attribute after setting the builtIn flag to False for preset with handle (b'\x01')
             test_presets = copy.deepcopy(new_presets_with_handle)
             test_presets[0].builtIn = False
 
-            status = await self.write_presets(endpoint=endpoint, presets=test_presets)
-            asserts.assert_equal(status, Status.ConstraintError,
-                                 "Presets write should return ConstraintError, because BuiltIn values do not match")
+            await self.write_presets(endpoint=endpoint, presets=test_presets, expected_status=Status.ConstraintError)
 
             # Clear state for next test.
-            await self.send_edit_atomic_request_rollback_command()
+            await self.send_atomic_request_rollback_command()
 
         self.step("8")
         if self.pics_guard(self.check_pics("TSTAT.S.F08") and self.check_pics("TSTAT.S.A0050") and self.check_pics("TSTAT.S.CFE.Rsp")):
 
             # Send the AtomicRequest begin command
-            await self.send_edit_atomic_request_begin_command()
+            await self.send_atomic_request_begin_command()
 
             # Write to the presets attribute after adding a preset with builtIn set to True
             test_presets = copy.deepcopy(new_presets_with_handle)
             test_presets.append(cluster.Structs.PresetStruct(presetHandle=NullValue, presetScenario=cluster.Enums.PresetScenarioEnum.kWake,
                                 name="Wake", coolingSetpoint=2800, heatingSetpoint=1800, builtIn=True))
 
-            status = await self.write_presets(endpoint=endpoint, presets=test_presets)
-            status_ok = (status == Status.Success)
-            asserts.assert_equal(status, Status.ConstraintError,
-                                 "Presets write should return ConstraintError, since we are trying to add a new built-in preset")
+            status = await self.write_presets(endpoint=endpoint, presets=test_presets, expected_status=Status.ConstraintError)
 
             # Clear state for next test.
-            await self.send_edit_atomic_request_rollback_command()
+            await self.send_atomic_request_rollback_command()
 
         self.step("9")
         if self.pics_guard(self.check_pics("TSTAT.S.F08") and self.check_pics("TSTAT.S.A0050") and self.check_pics("TSTAT.S.CFE.Rsp")):
 
             # Send the AtomicRequest begin command
-            await self.send_edit_atomic_request_begin_command()
+            await self.send_atomic_request_begin_command()
 
             # Write to the presets attribute after adding a preset with a preset handle that doesn't exist in Presets attribute
             test_presets = copy.deepcopy(new_presets_with_handle)
             test_presets.append(cluster.Structs.PresetStruct(presetHandle=b'\x08', presetScenario=cluster.Enums.PresetScenarioEnum.kWake,
                                 name="Wake", coolingSetpoint=2800, heatingSetpoint=1800, builtIn=True))
 
-            status = await self.write_presets(endpoint=endpoint, presets=test_presets)
-            asserts.assert_equal(status, Status.NotFound,
-                                 "Presets write should return NotFound, since we are trying to modify non-existent preset")
+            status = await self.write_presets(endpoint=endpoint, presets=test_presets, expected_status=Status.NotFound)
 
             # Clear state for next test.
-            await self.send_edit_atomic_request_rollback_command()
+            await self.send_atomic_request_rollback_command()
 
         self.step("10")
         if self.pics_guard(self.check_pics("TSTAT.S.F08") and self.check_pics("TSTAT.S.A0050") and self.check_pics("TSTAT.S.CFE.Rsp")):
 
             # Send the AtomicRequest begin command
-            await self.send_edit_atomic_request_begin_command()
+            await self.send_atomic_request_begin_command()
 
             # Write to the presets attribute after adding a duplicate preset with handle (b'\x03')
             test_presets = copy.deepcopy(new_presets_with_handle)
             test_presets.append(cluster.Structs.PresetStruct(
                 presetHandle=b'\x03', presetScenario=cluster.Enums.PresetScenarioEnum.kSleep, name="Sleep", coolingSetpoint=2700, heatingSetpoint=1900, builtIn=False))
 
-            status = await self.write_presets(endpoint=endpoint, presets=test_presets)
-            asserts.assert_equal(status, Status.ConstraintError,
-                                 "Presets write should return ConstraintError, since we have duplicated presets")
+            await self.write_presets(endpoint=endpoint, presets=test_presets, expected_status=Status.ConstraintError)
 
             # Clear state for next test.
-            await self.send_edit_atomic_request_rollback_command()
+            await self.send_atomic_request_rollback_command()
 
         self.step("11")
         if self.pics_guard(self.check_pics("TSTAT.S.F08") and self.check_pics("TSTAT.S.A0050") and self.check_pics("TSTAT.S.CFE.Rsp")):
 
             # Send the AtomicRequest begin command
-            await self.send_edit_atomic_request_begin_command()
+            await self.send_atomic_request_begin_command()
 
             # Write to the presets attribute after setting the builtIn flag to True for preset with handle (b'\x03')
             test_presets = copy.deepcopy(new_presets_with_handle)
             test_presets[2].builtIn = True
 
-            status = await self.write_presets(endpoint=endpoint, presets=test_presets)
-            asserts.assert_equal(status, Status.ConstraintError,
-                                 "Presets write should return ConstraintError, since we are trying to change whether a preset is BuiltIn")
+            await self.write_presets(endpoint=endpoint, presets=test_presets, expected_status=Status.ConstraintError)
 
             # Clear state for next test.
-            await self.send_edit_atomic_request_rollback_command()
+            await self.send_atomic_request_rollback_command()
 
         self.step("12")
         if self.pics_guard(self.check_pics("TSTAT.S.F08") and self.check_pics("TSTAT.S.A0050") and self.check_pics("TSTAT.S.CFE.Rsp")):
 
             # Send the AtomicRequest begin command
-            await self.send_edit_atomic_request_begin_command()
+            await self.send_atomic_request_begin_command()
 
             # Write to the presets attribute after setting a name for preset with handle (b'\x01') that doesn't support names
             test_presets = copy.deepcopy(new_presets_with_handle)
             test_presets[0].name = "Occupied"
 
-            status = await self.write_presets(endpoint=endpoint, presets=test_presets)
-            asserts.assert_equal(status, Status.ConstraintError,
-                                 "Presets write should return ConstraintError, since we are trying to set a name for a preset that does not support that")
+            await self.write_presets(endpoint=endpoint, presets=test_presets, expected_status=Status.ConstraintError)
 
             # Clear state for next test.
-            await self.send_edit_atomic_request_rollback_command()
+            await self.send_atomic_request_rollback_command()
 
         self.step("13")
         if self.pics_guard(self.check_pics("TSTAT.S.F08") and self.check_pics("TSTAT.S.A0050") and self.check_pics("TSTAT.S.CFE.Rsp")):
 
             # Send the AtomicRequest begin command
-            await self.send_edit_atomic_request_begin_command()
+            await self.send_atomic_request_begin_command()
 
             # Write to the presets attribute with a new valid preset added
             test_presets = copy.deepcopy(new_presets_with_handle)
             test_presets.append(cluster.Structs.PresetStruct(presetHandle=NullValue, presetScenario=cluster.Enums.PresetScenarioEnum.kWake,
                                 name="Wake", coolingSetpoint=2800, heatingSetpoint=1800, builtIn=False))
 
-            status = await self.write_presets(endpoint=endpoint, presets=test_presets)
-            status_ok = (status == Status.Success)
-            asserts.assert_equal(status, Status.Success, "Presets write did not return Success as expected")
+            await self.write_presets(endpoint=endpoint, presets=test_presets)
 
             # Roll back
-            await self.send_edit_atomic_request_rollback_command()
+            await self.send_atomic_request_rollback_command()
 
             # Send the AtomicRequest commit command and expect InvalidInState as the previous edit request was cancelled
-            await self.send_edit_atomic_request_commit_command(expected_status=Status.InvalidInState)
+            await self.send_atomic_request_commit_command(expected_status=Status.InvalidInState)
+
+        self.step("14")
+        if self.pics_guard(self.check_pics("TSTAT.S.F08") and self.check_pics("TSTAT.S.A0050") and self.check_pics("TSTAT.S.CFE.Rsp")):
+
+            # Send the AtomicRequest begin command
+            await self.send_atomic_request_begin_command()
+
+            # Send the AtomicRequest begin command from separate controller, which should receive busy
+            status = await self.send_atomic_request_begin_command(dev_ctrl=secondary_controller, expected_overall_status=Status.Failure, expected_preset_status=Status.Busy)
+
+            # Roll back
+            await self.send_atomic_request_rollback_command()
+
+        self.step("15")
+        if self.pics_guard(self.check_pics("TSTAT.S.F08") and self.check_pics("TSTAT.S.A0050") and self.check_pics("TSTAT.S.CFE.Rsp")):
+            # Send the AtomicRequest begin command from the secondary controller
+            await self.send_atomic_request_begin_command()
+
+            await self.write_presets(endpoint=endpoint, presets=test_presets, dev_ctrl=secondary_controller, expected_status=Status.Busy)
+
+            # Roll back
+            await self.send_atomic_request_rollback_command()
+
+        self.step("16")
+        if self.pics_guard(self.check_pics("TSTAT.S.F08") and self.check_pics("TSTAT.S.A0050") and self.check_pics("TSTAT.S.CFE.Rsp")):
+
+            # Send the AtomicRequest begin command from the secondary controller
+            await self.send_atomic_request_begin_command(dev_ctrl=secondary_controller)
+
+            # Primary controller removes the second fabric
+            await self.send_single_cmd(Clusters.OperationalCredentials.Commands.RemoveFabric(fabricIndex=2),  endpoint=0)
+
+            # Send the AtomicRequest begin command from primary controller, which should succeed, as the secondary controller's atomic write state has been cleared
+            status = await self.send_atomic_request_begin_command()
+
+            # Roll back
+            await self.send_atomic_request_rollback_command()
 
         # TODO: Add tests for the total number of Presets exceeds the NumberOfPresets supported. Also Add tests for adding presets with preset scenario not present in PresetTypes.