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 a28eb56c6eb299..6ea480e199be51 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) MatterThermostatClusterServerAttributeChangedCallback, \ (EmberAfGenericClusterFunction) MatterThermostatClusterServerShutdownCallback, \ (EmberAfGenericClusterFunction) MatterThermostatClusterServerPreAttributeChangedCallback, \ }; \ @@ -3756,7 +3757,7 @@ .attributes = ZAP_ATTRIBUTE_INDEX(616), \ .attributeCount = 26, \ .clusterSize = 72, \ - .mask = ZAP_CLUSTER_MASK(SERVER) | ZAP_CLUSTER_MASK(INIT_FUNCTION) | ZAP_CLUSTER_MASK(SHUTDOWN_FUNCTION) | ZAP_CLUSTER_MASK(PRE_ATTRIBUTE_CHANGED_FUNCTION), \ + .mask = ZAP_CLUSTER_MASK(SERVER) | ZAP_CLUSTER_MASK(INIT_FUNCTION) | ZAP_CLUSTER_MASK(ATTRIBUTE_CHANGED_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-server.cpp b/src/app/clusters/thermostat-server/thermostat-server.cpp index fe8ccf8aab3cf0..9e6e0f074d5e52 100644 --- a/src/app/clusters/thermostat-server/thermostat-server.cpp +++ b/src/app/clusters/thermostat-server/thermostat-server.cpp @@ -468,6 +468,52 @@ void ThermostatAttrAccess::OnFabricRemoved(const FabricTable & fabricTable, Fabr } } +void MatterThermostatClusterServerAttributeChangedCallback(const ConcreteAttributePath & attributePath) +{ + uint32_t flags; + if (FeatureMap::Get(attributePath.mEndpointId, &flags) != Status::Success) + { + ChipLogError(Zcl, "MatterThermostatClusterServerAttributeChangedCallback: could not get feature flags"); + return; + } + + auto featureMap = BitMask(flags); + if (!featureMap.Has(Feature::kPresets)) + { + // This server does not support presets, so nothing to do + return; + } + + bool occupied = true; + if (featureMap.Has(Feature::kOccupancy)) + { + BitMask occupancy; + if (Occupancy::Get(attributePath.mEndpointId, &occupancy) == Status::Success) + { + occupied = occupancy.Has(OccupancyBitmap::kOccupied); + } + } + + bool clearActivePreset = false; + switch (attributePath.mAttributeId) + { + case OccupiedHeatingSetpoint::Id: + case OccupiedCoolingSetpoint::Id: + clearActivePreset = occupied; + break; + case UnoccupiedHeatingSetpoint::Id: + case UnoccupiedCoolingSetpoint::Id: + clearActivePreset = !occupied; + break; + } + if (!clearActivePreset) + { + return; + } + ChipLogProgress(Zcl, "Setting active preset to null"); + gThermostatAttrAccess.SetActivePreset(attributePath.mEndpointId, std::nullopt); +} + } // namespace Thermostat } // namespace Clusters } // namespace app @@ -762,6 +808,11 @@ MatterThermostatClusterServerPreAttributeChangedCallback(const app::ConcreteAttr } } +void MatterThermostatClusterServerAttributeChangedCallback(const ConcreteAttributePath & attributePath) +{ + Thermostat::MatterThermostatClusterServerAttributeChangedCallback(attributePath); +} + bool emberAfThermostatClusterClearWeeklyScheduleCallback(app::CommandHandler * commandObj, const app::ConcreteCommandPath & commandPath, const Commands::ClearWeeklySchedule::DecodableType & commandData) diff --git a/src/app/clusters/thermostat-server/thermostat-server.h b/src/app/clusters/thermostat-server/thermostat-server.h index cc941cfa766d92..2d365ffd8288d1 100644 --- a/src/app/clusters/thermostat-server/thermostat-server.h +++ b/src/app/clusters/thermostat-server/thermostat-server.h @@ -207,6 +207,7 @@ class ThermostatAttrAccess : public chip::app::AttributeAccessInterface, public friend void TimerExpiredCallback(System::Layer * systemLayer, void * callbackContext); friend void MatterThermostatClusterServerShutdownCallback(EndpointId endpoint); + friend void MatterThermostatClusterServerAttributeChangedCallback(const chip::app::ConcreteAttributePath & attributePath); friend bool emberAfThermostatClusterSetActivePresetRequestCallback( CommandHandler * commandObj, const ConcreteCommandPath & commandPath, diff --git a/src/app/common/templates/config-data.yaml b/src/app/common/templates/config-data.yaml index 660ca0b3aa1df4..61e7c45582268a 100644 --- a/src/app/common/templates/config-data.yaml +++ b/src/app/common/templates/config-data.yaml @@ -72,6 +72,7 @@ ClustersWithAttributeChangedFunctions: - Pump Configuration and Control - Window Covering - Fan Control + - Thermostat ClustersWithShutdownFunctions: - Barrier Control diff --git a/src/python_testing/TC_TSTAT_4_2.py b/src/python_testing/TC_TSTAT_4_2.py index 641c827b388882..56cf356b6b8098 100644 --- a/src/python_testing/TC_TSTAT_4_2.py +++ b/src/python_testing/TC_TSTAT_4_2.py @@ -291,6 +291,11 @@ async def test_TC_TSTAT_4_2(self): logger.info(f"Rx'd Presets: {presets}") asserts.assert_equal(presets, new_presets_with_handle, "Presets were not updated which is not expected") + # Send the SetActivePresetRequest command + await self.send_set_active_preset_handle_request_command(value=b'\x03') + + activePresetHandle = await self.read_single_attribute_check_success(endpoint=endpoint, cluster=cluster, attribute=cluster.Attributes.ActivePresetHandle) + self.step("5") 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")): @@ -327,6 +332,13 @@ async def test_TC_TSTAT_4_2(self): # Send the AtomicRequest commit command and expect InvalidInState for presets. await self.send_atomic_request_commit_command(expected_overall_status=Status.Failure, expected_preset_status=Status.InvalidInState) + # Write the occupied cooling setpoint to a different value + await self.write_single_attribute(attribute_value=cluster.Attributes.OccupiedCoolingSetpoint(2300), endpoint_id=endpoint) + + activePresetHandle = await self.read_single_attribute_check_success(endpoint=endpoint, cluster=cluster, attribute=cluster.Attributes.ActivePresetHandle) + logger.info(f"Rx'd ActivePresetHandle: {activePresetHandle}") + asserts.assert_equal(activePresetHandle, NullValue, "Active preset handle was not cleared as expected") + 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")):