diff --git a/examples/thermostat/thermostat-common/thermostat.matter b/examples/thermostat/thermostat-common/thermostat.matter index daa9883933a77b..dd4c9bba669d89 100644 --- a/examples/thermostat/thermostat-common/thermostat.matter +++ b/examples/thermostat/thermostat-common/thermostat.matter @@ -2940,6 +2940,7 @@ endpoint 1 { ram attribute minSetpointDeadBand default = 0x19; ram attribute controlSequenceOfOperation default = 0x04; persist attribute systemMode default = 0x01; + ram attribute thermostatRunningMode default = 0; callback attribute presetTypes; ram attribute numberOfPresets default = 0; ram attribute activePresetHandle; @@ -2948,7 +2949,7 @@ endpoint 1 { callback attribute generatedCommandList; callback attribute acceptedCommandList; callback attribute attributeList; - ram attribute featureMap default = 0x123; + ram attribute featureMap default = 0x323; ram attribute clusterRevision default = 8; handle command SetpointRaiseLower; diff --git a/examples/thermostat/thermostat-common/thermostat.zap b/examples/thermostat/thermostat-common/thermostat.zap index 45813628f09f58..f76a1008d310f5 100644 --- a/examples/thermostat/thermostat-common/thermostat.zap +++ b/examples/thermostat/thermostat-common/thermostat.zap @@ -5137,6 +5137,22 @@ "maxInterval": 65344, "reportableChange": 0 }, + { + "name": "ThermostatRunningMode", + "code": 30, + "mfgCode": null, + "side": "server", + "type": "ThermostatRunningModeEnum", + "included": 1, + "storageOption": "RAM", + "singleton": 0, + "bounded": 0, + "defaultValue": "0", + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, { "name": "PresetTypes", "code": 72, @@ -5275,7 +5291,7 @@ "storageOption": "RAM", "singleton": 0, "bounded": 0, - "defaultValue": "0x123", + "defaultValue": "0x323", "reportable": 1, "minInterval": 0, "maxInterval": 65344, diff --git a/src/app/clusters/thermostat-server/app_config_dependent_sources.cmake b/src/app/clusters/thermostat-server/app_config_dependent_sources.cmake index 15b27b818b34ca..1408a8b96ab877 100644 --- a/src/app/clusters/thermostat-server/app_config_dependent_sources.cmake +++ b/src/app/clusters/thermostat-server/app_config_dependent_sources.cmake @@ -20,6 +20,7 @@ TARGET_SOURCES( "${CLUSTER_DIR}/PresetStructWithOwnedMembers.h" "${CLUSTER_DIR}/thermostat-delegate.h" "${CLUSTER_DIR}/thermostat-server-atomic.cpp" + "${CLUSTER_DIR}/thermostat-server-events.cpp" "${CLUSTER_DIR}/thermostat-server-presets.cpp" "${CLUSTER_DIR}/thermostat-server.cpp" "${CLUSTER_DIR}/thermostat-server.h" diff --git a/src/app/clusters/thermostat-server/app_config_dependent_sources.gni b/src/app/clusters/thermostat-server/app_config_dependent_sources.gni index c29d105416b384..fa124350cd22f2 100644 --- a/src/app/clusters/thermostat-server/app_config_dependent_sources.gni +++ b/src/app/clusters/thermostat-server/app_config_dependent_sources.gni @@ -16,6 +16,7 @@ app_config_dependent_sources = [ "PresetStructWithOwnedMembers.h", "thermostat-delegate.h", "thermostat-server-atomic.cpp", + "thermostat-server-events.cpp", "thermostat-server-presets.cpp", "thermostat-server.cpp", "thermostat-server.h", diff --git a/src/app/clusters/thermostat-server/thermostat-server-events.cpp b/src/app/clusters/thermostat-server/thermostat-server-events.cpp new file mode 100644 index 00000000000000..356f206a5490c2 --- /dev/null +++ b/src/app/clusters/thermostat-server/thermostat-server-events.cpp @@ -0,0 +1,153 @@ +/** + * + * Copyright (c) 2025 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/clusters/thermostat-server/thermostat-server.h" + +#include "app/EventLogging.h" + +using namespace chip; +using namespace chip::app; +using namespace chip::app::Clusters; +using namespace chip::app::Clusters::Thermostat; +using namespace chip::app::Clusters::Thermostat::Attributes; + +void EmitSystemModeChangeEvent(EndpointId endpoint, Optional previousSystemMode, SystemModeEnum currentSystemMode) +{ + Events::SystemModeChange::Type event; + EventNumber eventNumber; + + event.previousSystemMode = previousSystemMode; + event.currentSystemMode = currentSystemMode; + + CHIP_ERROR err = LogEvent(event, endpoint, eventNumber); + if (CHIP_NO_ERROR != err) + { + ChipLogError(Zcl, "Failed to emit SystemModeChange event: %" CHIP_ERROR_FORMAT, err.Format()); + } +} + +void EmitLocalTemperatureChangeEvent(EndpointId endpoint, DataModel::Nullable currentLocalTemperature) +{ + Events::LocalTemperatureChange::Type event; + EventNumber eventNumber; + + event.currentLocalTemperature = currentLocalTemperature; + + CHIP_ERROR err = LogEvent(event, endpoint, eventNumber); + if (CHIP_NO_ERROR != err) + { + ChipLogError(Zcl, "Failed to emit LocalTemperatureChange event: %" CHIP_ERROR_FORMAT, err.Format()); + } +} + +void EmitOccupancyChangeEvent(EndpointId endpoint, Optional> previousOccupancy, + chip::BitMask currentOccupancy) +{ + Events::OccupancyChange::Type event; + EventNumber eventNumber; + + event.previousOccupancy = previousOccupancy; + event.currentOccupancy = currentOccupancy; + + CHIP_ERROR err = LogEvent(event, endpoint, eventNumber); + if (CHIP_NO_ERROR != err) + { + ChipLogError(Zcl, "Failed to emit OccupancyChange event: %" CHIP_ERROR_FORMAT, err.Format()); + } +} + +void EmitSetpointChangeEvent(EndpointId endpoint, SystemModeEnum systemMode, Optional> occupancy, + Optional previousSetpoint, int16_t currentSetpoint) +{ + Events::SetpointChange::Type event; + EventNumber eventNumber; + + event.systemMode = systemMode; + event.occupancy = occupancy; + event.previousSetpoint = previousSetpoint; + event.currentSetpoint = currentSetpoint; + + CHIP_ERROR err = LogEvent(event, endpoint, eventNumber); + if (CHIP_NO_ERROR != err) + { + ChipLogError(Zcl, "Failed to emit SetpointChange event: %" CHIP_ERROR_FORMAT, err.Format()); + } +} + +void EmitRunningStateChangeEvent(EndpointId endpoint, Optional> previousRunningState, + chip::BitMask currentRunningState) +{ + Events::RunningStateChange::Type event; + EventNumber eventNumber; + + event.previousRunningState = previousRunningState; + event.currentRunningState = currentRunningState; + + CHIP_ERROR err = LogEvent(event, endpoint, eventNumber); + if (CHIP_NO_ERROR != err) + { + ChipLogError(Zcl, "Failed to emit RunningStateChange event: %" CHIP_ERROR_FORMAT, err.Format()); + } +} + +void EmitRunningModeChangeEvent(EndpointId endpoint, Optional previousRunningMode, + ThermostatRunningModeEnum currentRunningMode) +{ + Events::RunningModeChange::Type event; + EventNumber eventNumber; + + event.previousRunningMode = previousRunningMode; + event.currentRunningMode = currentRunningMode; + + CHIP_ERROR err = LogEvent(event, endpoint, eventNumber); + if (CHIP_NO_ERROR != err) + { + ChipLogError(Zcl, "Failed to emit RunningModeChange event: %" CHIP_ERROR_FORMAT, err.Format()); + } +} + +void EmitActiveScheduleChangeEvent(EndpointId endpoint, Optional> previousScheduleHandle, + DataModel::Nullable currentScheduleHandle) +{ + Events::ActiveScheduleChange::Type event; + EventNumber eventNumber; + + event.previousScheduleHandle = previousScheduleHandle; + event.currentScheduleHandle = currentScheduleHandle; + + CHIP_ERROR err = LogEvent(event, endpoint, eventNumber); + if (CHIP_NO_ERROR != err) + { + ChipLogError(Zcl, "Failed to emit ActiveScheduleChange event: %" CHIP_ERROR_FORMAT, err.Format()); + } +} + +void EmitActivePresetChangeEvent(EndpointId endpoint, Optional> previousPresetHandle, + DataModel::Nullable currentPresetHandle) +{ + Events::ActivePresetChange::Type event; + EventNumber eventNumber; + + event.previousPresetHandle = previousPresetHandle; + event.currentPresetHandle = currentPresetHandle; + + CHIP_ERROR err = LogEvent(event, endpoint, eventNumber); + if (CHIP_NO_ERROR != err) + { + ChipLogError(Zcl, "Failed to emit ActivePresetChange event: %" CHIP_ERROR_FORMAT, err.Format()); + } +} diff --git a/src/app/clusters/thermostat-server/thermostat-server-events.h b/src/app/clusters/thermostat-server/thermostat-server-events.h new file mode 100644 index 00000000000000..77133d3aa26f44 --- /dev/null +++ b/src/app/clusters/thermostat-server/thermostat-server-events.h @@ -0,0 +1,56 @@ +/** + * + * Copyright (c) 2024 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. + */ + +/**************************************************************************** + * @file + * @brief APIs for sending events from the Thermostat cluster. + * + ******************************************************************************* + ******************************************************************************/ + +#pragma once + +void EmitSystemModeChangeEvent(chip::EndpointId endpoint, + chip::Optional previousSystemMode, + chip::app::Clusters::Thermostat::SystemModeEnum currentSystemMode); + +void EmitLocalTemperatureChangeEvent(chip::EndpointId endpoint, chip::app::DataModel::Nullable currentLocalTemperature); + +void EmitOccupancyChangeEvent(chip::EndpointId endpoint, + chip::Optional> previousOccupancy, + chip::BitMask currentOccupancy); + +void EmitSetpointChangeEvent(chip::EndpointId endpoint, chip::app::Clusters::Thermostat::SystemModeEnum systemMode, + chip::Optional> occupancy, + chip::Optional previousSetpoint, int16_t currentSetpoint); + +void EmitRunningStateChangeEvent( + chip::EndpointId endpoint, + chip::Optional> previousRunningState, + chip::BitMask currentRunningState); + +void EmitRunningModeChangeEvent(chip::EndpointId endpoint, + chip::Optional previousRunningMode, + chip::app::Clusters::Thermostat::ThermostatRunningModeEnum currentRunningMode); + +void EmitActiveScheduleChangeEvent(chip::EndpointId endpoint, + chip::Optional> previousScheduleHandle, + chip::app::DataModel::Nullable currentScheduleHandle); + +void EmitActivePresetChangeEvent(chip::EndpointId endpoint, + chip::Optional> previousPresetHandle, + chip::app::DataModel::Nullable currentPresetHandle); diff --git a/src/app/clusters/thermostat-server/thermostat-server.cpp b/src/app/clusters/thermostat-server/thermostat-server.cpp index 90cb5f728635b6..f0796e2dc314cd 100644 --- a/src/app/clusters/thermostat-server/thermostat-server.cpp +++ b/src/app/clusters/thermostat-server/thermostat-server.cpp @@ -17,6 +17,7 @@ #include "thermostat-server.h" #include "PresetStructWithOwnedMembers.h" +#include "thermostat-server-events.h" #include @@ -392,6 +393,7 @@ Status CheckCoolingSetpointDeadband(bool autoSupported, int16_t newHeatingSetpoi return Status::Success; } +typedef Status (*SetpointGetter)(EndpointId endpoint, int16_t * value); typedef Status (*SetpointSetter)(EndpointId endpoint, int16_t value); /** @@ -754,6 +756,105 @@ void ThermostatAttrAccess::OnFabricRemoved(const FabricTable & fabricTable, Fabr } } +void EmitSetpointEvent(const ConcreteAttributePath & attributePath, SystemModeEnum systemMode, + Optional> occupancy, SetpointGetter getter) +{ + int16_t setpoint; + auto status = getter(attributePath.mEndpointId, &setpoint); + if (status != Status::Success) + { + ChipLogError(Zcl, "EmitSetpointChangeEvent failed to queue event: could not get set point"); + return; + } + EmitSetpointChangeEvent(attributePath.mEndpointId, systemMode, occupancy, chip::Optional(), setpoint); +} + +void EmitEvents(const ConcreteAttributePath & attributePath) +{ + switch (attributePath.mAttributeId) + { + case SystemMode::Id: { + SystemModeEnum systemMode = SystemModeEnum::kOff; + if (SystemMode::Get(attributePath.mEndpointId, &systemMode) != Status::Success) + { + ChipLogError(Zcl, "Failed to queue SystemModeChange event: could not get system mode"); + } + else + { + EmitSystemModeChangeEvent(attributePath.mEndpointId, chip::Optional(), systemMode); + } + break; + } + case OccupiedHeatingSetpoint::Id: + EmitSetpointEvent(attributePath, SystemModeEnum::kHeat, + chip::Optional>(OccupancyBitmap::kOccupied), OccupiedHeatingSetpoint::Get); + break; + case OccupiedCoolingSetpoint::Id: + EmitSetpointEvent(attributePath, SystemModeEnum::kCool, + chip::Optional>(OccupancyBitmap::kOccupied), OccupiedCoolingSetpoint::Get); + break; + case UnoccupiedHeatingSetpoint::Id: + EmitSetpointEvent(attributePath, SystemModeEnum::kHeat, chip::Optional>(0), + UnoccupiedHeatingSetpoint::Get); + + break; + case UnoccupiedCoolingSetpoint::Id: + EmitSetpointEvent(attributePath, SystemModeEnum::kCool, chip::Optional>(0), + UnoccupiedCoolingSetpoint::Get); + + break; + case LocalTemperature::Id: { + DataModel::Nullable local_temperature; + if (LocalTemperature::Get(attributePath.mEndpointId, local_temperature) != Status::Success) + { + ChipLogError(Zcl, "Failed to queue LocalTemperatureChange event: could not get local temperature"); + } + else + { + EmitLocalTemperatureChangeEvent(attributePath.mEndpointId, local_temperature); + } + break; + } + case Occupancy::Id: { + BitMask occupancy; + if (Occupancy::Get(attributePath.mEndpointId, &occupancy) != Status::Success) + { + ChipLogError(Zcl, "Failed to queue OccupancyChange event: could not get occupancy"); + } + else + { + EmitOccupancyChangeEvent(attributePath.mEndpointId, chip::Optional>(), occupancy); + } + break; + } + case ThermostatRunningState::Id: { + BitMask running_state; + if (ThermostatRunningState::Get(attributePath.mEndpointId, &running_state) != Status::Success) + { + ChipLogError(Zcl, "Failed to queue RunningStateChange event: could not get running state"); + } + else + { + EmitRunningStateChangeEvent(attributePath.mEndpointId, chip::Optional>(), + running_state); + } + break; + } + case ThermostatRunningMode::Id: { + ThermostatRunningModeEnum running_mode; + if (ThermostatRunningMode::Get(attributePath.mEndpointId, &running_mode) != Status::Success) + { + ChipLogError(Zcl, "Failed to queue RunningModeChange event: could not get running mode"); + } + else + { + EmitRunningModeChangeEvent(attributePath.mEndpointId, Optional(), running_mode); + } + break; + } + } +} + void MatterThermostatClusterServerAttributeChangedCallback(const ConcreteAttributePath & attributePath) { uint32_t flags; @@ -789,6 +890,10 @@ void MatterThermostatClusterServerAttributeChangedCallback(const ConcreteAttribu EnsureDeadband(attributePath); break; } + if (featureMap.Has(Feature::kEvents)) + { + EmitEvents(attributePath); + } if (clearActivePreset) { ChipLogProgress(Zcl, "Setting active preset to null"); diff --git a/src/python_testing/TC_TSTAT_2_2.py b/src/python_testing/TC_TSTAT_2_2.py index f4e1dfbc2b70d3..ff24db6d8ce2a9 100644 --- a/src/python_testing/TC_TSTAT_2_2.py +++ b/src/python_testing/TC_TSTAT_2_2.py @@ -35,9 +35,13 @@ # === END CI TEST ARGUMENTS === import logging +import time +from typing import Optional import chip.clusters as Clusters +from chip import ChipDeviceCtrl from chip.interaction_model import Status +from chip.testing.event_attribute_reporting import EventChangeCallback from chip.testing.matter_testing import MatterBaseTest, TestStep, async_test_body, default_matter_test_main from mobly import asserts @@ -59,18 +63,23 @@ def pics_TC_TSTAT_2_2(self): def steps_TC_TSTAT_2_2(self) -> list[TestStep]: steps = [ TestStep("1", "Commissioning, already done", is_commissioning=True), + TestStep("1a", "Test Harness Client subscribes to events"), TestStep("2a", "Test Harness Client reads attribute OccupiedCoolingSetpoint from the DUT"), TestStep("2b", "Test Harness Client then attempts Writes OccupiedCoolingSetpoint to a value below the MinCoolSetpointLimit"), TestStep("2c", "Test Harness Writes the limit of MaxCoolSetpointLimit to OccupiedCoolingSetpoint attribute"), + TestStep("2d", "Test Harness checks for SetpointChange event"), TestStep("3a", "Test Harness Reads OccupiedHeatingSetpoint attribute from Server DUT and verifies that the value is within range"), TestStep("3b", "Test Harness Writes OccupiedHeatingSetpoint to value below the MinHeatSetpointLimit"), TestStep("3c", "Test Harness Writes the limit of MaxHeatSetpointLimit to OccupiedHeatingSetpoint attribute"), + TestStep("3d", "Test Harness checks for SetpointChange event"), TestStep("4a", "Test Harness Reads UnoccupiedCoolingSetpoint attribute from Server DUT and verifies that the value is within range"), TestStep("4b", "Test Harness Writes UnoccupiedCoolingSetpoint to value below the MinCoolSetpointLimit"), TestStep("4c", "Test Harness Writes the limit of MaxCoolSetpointLimit to UnoccupiedCoolingSetpoint attribute"), + TestStep("4d", "Test Harness checks for SetpointChange event"), TestStep("5a", "Test Harness Reads UnoccupiedHeatingSetpoint attribute from Server DUT and verifies that the value is within range"), TestStep("5b", "Test Harness Writes UnoccupiedHeatingSetpoint to value below the MinHeatSetpointLimit"), TestStep("5c", "Test Harness Writes the limit of MaxHeatSetpointLimit to UnoccupiedHeatingSetpoint attribute"), + TestStep("5d", "Test Harness checks for SetpointChange event"), TestStep("6a", "Test Harness Reads MinHeatSetpointLimit attribute from Server DUT and verifies that the value is within range"), TestStep("6b", "Test Harness Writes a value back that is different but violates the deadband"), TestStep("6c", "Test Harness Writes the limit of MaxHeatSetpointLimit to MinHeatSetpointLimit attribute"), @@ -99,6 +108,29 @@ def steps_TC_TSTAT_2_2(self) -> list[TestStep]: return steps + async def check_setpoint_event(self, + events_callback: EventChangeCallback, + attribute: Clusters.ClusterObjects.ClusterAttributeDescriptor, + system_mode: Clusters.Thermostat.Enums.SystemModeEnum, + occupancy: Clusters.Thermostat.Bitmaps.OccupancyBitmap, + endpoint: Optional[int] = None, + dev_ctrl: ChipDeviceCtrl = None) -> Status: + + event_data = events_callback.wait_for_event_report(cluster.Events.SetpointChange) + + asserts.assert_equal(system_mode, event_data.systemMode) + asserts.assert_equal(occupancy, event_data.occupancy) + + if dev_ctrl is None: + dev_ctrl = self.default_controller + + setpoint = await self.read_single_attribute_check_success(endpoint=endpoint, cluster=cluster, attribute=attribute) + asserts.assert_equal(setpoint, event_data.currentSetpoint) + + def flush_events(self, events_callback: EventChangeCallback, wait_sec: float = 0.5): + time.sleep(wait_sec) + events_callback.flush_events() + @ async_test_body async def test_TC_TSTAT_2_2(self): endpoint = self.get_endpoint() @@ -157,6 +189,20 @@ async def test_TC_TSTAT_2_2(self): UnoccupiedHeatingSetpointValue = None UnoccupiedCoolingSetpointValue = None + events_callback = None + hasEventsFeature = False + + feat_should_be_there = await self.feature_guard(endpoint=endpoint, cluster=cluster, feature_int=cluster.Bitmaps.Feature.kPresets) + asserts.assert_true(feat_should_be_there, True) + + self.step("1a") + if await self.feature_guard(endpoint=endpoint, cluster=cluster, feature_int=cluster.Bitmaps.Feature.kEvents): + hasEventsFeature = True + events_callback = EventChangeCallback(cluster) + await events_callback.start(self.default_controller, + self.dut_node_id, + endpoint=endpoint) + ControlSequenceOfOperation = await self.read_single_attribute_check_success(endpoint=endpoint, cluster=cluster, attribute=cluster.Attributes.ControlSequenceOfOperation) if self.pics_guard(hasMinCoolSetpointLimitAttribute): @@ -226,6 +272,9 @@ async def test_TC_TSTAT_2_2(self): val = await self.read_single_attribute_check_success(endpoint=endpoint, cluster=cluster, attribute=cluster.Attributes.OccupiedCoolingSetpoint) asserts.assert_equal(val, MinCoolSetpointLimitValue + ((MaxCoolSetpointLimitValue - MinCoolSetpointLimitValue) // 2)) + if hasEventsFeature: + await self.check_setpoint_event(events_callback=events_callback, attribute=cluster.Attributes.OccupiedCoolingSetpoint, system_mode=cluster.Enums.SystemModeEnum.kCool, occupancy=cluster.Bitmaps.OccupancyBitmap.kOccupied, endpoint=endpoint, dev_ctrl=self.default_controller) + self.step("2b") if self.pics_guard(hasCoolingFeature): @@ -243,6 +292,9 @@ async def test_TC_TSTAT_2_2(self): # Test Harness Writes the limit of MaxCoolSetpointLimit to OccupiedCoolingSetpoint attribute await self.write_single_attribute(attribute_value=cluster.Attributes.OccupiedCoolingSetpoint(MaxCoolSetpointLimitValue), endpoint_id=endpoint) + if hasEventsFeature: + await self.check_setpoint_event(events_callback=events_callback, attribute=cluster.Attributes.OccupiedCoolingSetpoint, system_mode=cluster.Enums.SystemModeEnum.kCool, occupancy=cluster.Bitmaps.OccupancyBitmap.kOccupied, endpoint=endpoint, dev_ctrl=self.default_controller) + if self.pics_guard(hasAutoModeFeature): # Test Harness Writes If TSTAT.S.F05(AUTO) LowerLimit = Max(MinCoolSetpointLimit, (OccupiedHeatingSetpoint + MinSetpointDeadBand)) to OccupiedCoolingSetpoint attribute when Auto is enabled await self.write_single_attribute(attribute_value=cluster.Attributes.OccupiedCoolingSetpoint(max(MinCoolSetpointLimitValue, (OccupiedHeatingSetpointValue + (MinSetpointDeadBandValue)))), endpoint_id=endpoint) @@ -251,6 +303,10 @@ async def test_TC_TSTAT_2_2(self): # Test Harness Writes the limit of MinCoolSetpointLimit to OccupiedCoolingSetpoint attribute await self.write_single_attribute(attribute_value=cluster.Attributes.OccupiedCoolingSetpoint(MinCoolSetpointLimitValue), endpoint_id=endpoint) + self.step("2d") + if self.pics_guard(hasCoolingFeature) and await self.feature_guard(endpoint=endpoint, cluster=cluster, feature_int=cluster.Bitmaps.Feature.kEvents): + await self.check_setpoint_event(events_callback=events_callback, attribute=cluster.Attributes.OccupiedCoolingSetpoint, system_mode=cluster.Enums.SystemModeEnum.kCool, occupancy=cluster.Bitmaps.OccupancyBitmap.kOccupied, endpoint=endpoint, dev_ctrl=self.default_controller) + self.step("3a") if self.pics_guard(hasHeatingFeature): @@ -267,6 +323,9 @@ async def test_TC_TSTAT_2_2(self): val = await self.read_single_attribute_check_success(endpoint=endpoint, cluster=cluster, attribute=cluster.Attributes.OccupiedHeatingSetpoint) asserts.assert_equal(val, MinHeatSetpointLimitValue + ((MaxHeatSetpointLimitValue - MinHeatSetpointLimitValue) // 2)) + if hasEventsFeature: + await self.check_setpoint_event(events_callback=events_callback, attribute=cluster.Attributes.OccupiedHeatingSetpoint, system_mode=cluster.Enums.SystemModeEnum.kHeat, occupancy=cluster.Bitmaps.OccupancyBitmap.kOccupied, endpoint=endpoint) + self.step("3b") if self.pics_guard(hasHeatingFeature): @@ -284,6 +343,9 @@ async def test_TC_TSTAT_2_2(self): # Test Harness Writes the limit of MinHeatSetpointLimit to OccupiedHeatingSetpoint attribute await self.write_single_attribute(attribute_value=cluster.Attributes.OccupiedHeatingSetpoint(MinHeatSetpointLimitValue), endpoint_id=endpoint) + if hasEventsFeature: + await self.check_setpoint_event(events_callback=events_callback, attribute=cluster.Attributes.OccupiedHeatingSetpoint, system_mode=cluster.Enums.SystemModeEnum.kHeat, occupancy=cluster.Bitmaps.OccupancyBitmap.kOccupied, endpoint=endpoint) + if self.pics_guard(hasAutoModeFeature): # Test Harness Writes the limit of MaxHeatSetpointLimit to OccupiedHeatingSetpoint attribute upper_limit = min(MaxHeatSetpointLimitValue, (OccupiedCoolingSetpointValue - MinSetpointDeadBandValue)) @@ -293,6 +355,14 @@ async def test_TC_TSTAT_2_2(self): # Test Harness Writes the limit of MaxHeatSetpointLimit to OccupiedHeatingSetpoint attribute If TSTAT.S.F05 is true await self.write_single_attribute(attribute_value=cluster.Attributes.OccupiedHeatingSetpoint(MaxHeatSetpointLimitValue), endpoint_id=endpoint) + self.step("3d") + if self.pics_guard(hasHeatingFeature) and await self.feature_guard(endpoint=endpoint, cluster=cluster, feature_int=cluster.Bitmaps.Feature.kEvents): + if self.pics_guard(hasAutoModeFeature): + if hasEventsFeature: + # If we have auto mode, this will have also adjusted the cooling setpoint to preserve the deadband + await self.check_setpoint_event(events_callback=events_callback, attribute=cluster.Attributes.OccupiedCoolingSetpoint, system_mode=cluster.Enums.SystemModeEnum.kCool, occupancy=cluster.Bitmaps.OccupancyBitmap.kOccupied, endpoint=endpoint) + await self.check_setpoint_event(events_callback=events_callback, attribute=cluster.Attributes.OccupiedHeatingSetpoint, system_mode=cluster.Enums.SystemModeEnum.kHeat, occupancy=cluster.Bitmaps.OccupancyBitmap.kOccupied, endpoint=endpoint) + self.step("4a") if self.pics_guard(hasOccupancyFeature and hasCoolingFeature): @@ -333,6 +403,10 @@ async def test_TC_TSTAT_2_2(self): # Test Harness Writes the limit of MaxCoolSetpointLimit to UnoccupiedCoolingSetpoint attribute await self.write_single_attribute(attribute_value=cluster.Attributes.UnoccupiedCoolingSetpoint(MaxCoolSetpointLimitValue), endpoint_id=endpoint) + self.step("4d") + if self.pics_guard(hasOccupancyFeature and hasCoolingFeature) and await self.feature_guard(endpoint=endpoint, cluster=cluster, feature_int=cluster.Bitmaps.Feature.kEvents): + await self.check_setpoint_event(events_callback=events_callback, attribute=cluster.Attributes.UnoccupiedCoolingSetpoint, system_mode=cluster.Enums.SystemModeEnum.kCool, occupancy=0, endpoint=endpoint) + self.step("5a") if self.pics_guard(hasOccupancyFeature and hasHeatingFeature): @@ -369,10 +443,16 @@ async def test_TC_TSTAT_2_2(self): else: # Test Harness Writes the limit of MaxHeatSetpointLimit to UnoccupiedHeatingSetpoint attribute await self.write_single_attribute(attribute_value=cluster.Attributes.UnoccupiedHeatingSetpoint(MaxHeatSetpointLimitValue), endpoint_id=endpoint) + if hasEventsFeature: + await self.check_setpoint_event(events_callback=events_callback, attribute=cluster.Attributes.UnoccupiedHeatingSetpoint, system_mode=cluster.Enums.SystemModeEnum.kHeat, occupancy=0, endpoint=endpoint) # Test Harness Writes the limit of MinHeatSetpointLimit to UnoccupiedHeatingSetpoint attribute await self.write_single_attribute(attribute_value=cluster.Attributes.UnoccupiedHeatingSetpoint(MinHeatSetpointLimitValue), endpoint_id=endpoint) + self.step("5d") + if self.pics_guard(hasOccupancyFeature and hasHeatingFeature) and await self.feature_guard(endpoint=endpoint, cluster=cluster, feature_int=cluster.Bitmaps.Feature.kEvents): + await self.check_setpoint_event(events_callback=events_callback, attribute=cluster.Attributes.UnoccupiedHeatingSetpoint, system_mode=cluster.Enums.SystemModeEnum.kHeat, occupancy=0, endpoint=endpoint) + self.step("6a") if self.pics_guard(hasHeatingFeature and hasMinHeatSetpointLimitAttribute): @@ -684,6 +764,8 @@ async def test_TC_TSTAT_2_2(self): # Sets OccupiedHeatingSetpoint to default value await self.write_single_attribute(attribute_value=cluster.Attributes.OccupiedHeatingSetpoint(OccupiedHeatingSetpointValue), endpoint_id=endpoint) + self.flush_events(events_callback) + # Sends SetpointRaise Command Heat Only await self.send_single_cmd(cmd=Clusters.Objects.Thermostat.Commands.SetpointRaiseLower(mode=Clusters.Objects.Thermostat.Enums.SetpointRaiseLowerModeEnum.kHeat, amount=-30), endpoint=endpoint) @@ -691,12 +773,17 @@ async def test_TC_TSTAT_2_2(self): val = await self.read_single_attribute_check_success(endpoint=endpoint, cluster=cluster, attribute=cluster.Attributes.OccupiedHeatingSetpoint) asserts.assert_equal(val, OccupiedHeatingSetpointValue - 30 * 10) + if hasEventsFeature: + await self.check_setpoint_event(events_callback=events_callback, attribute=cluster.Attributes.OccupiedHeatingSetpoint, system_mode=cluster.Enums.SystemModeEnum.kHeat, occupancy=cluster.Bitmaps.OccupancyBitmap.kOccupied, endpoint=endpoint) + self.step("14") if self.pics_guard(hasHeatingFeature): # Sets OccupiedHeatingSetpoint to default value await self.write_single_attribute(attribute_value=cluster.Attributes.OccupiedHeatingSetpoint(OccupiedHeatingSetpointValue), endpoint_id=endpoint) + self.flush_events(events_callback) + # Test Harness Sends SetpointRaise Command Heat Only await self.send_single_cmd(cmd=Clusters.Objects.Thermostat.Commands.SetpointRaiseLower(mode=Clusters.Objects.Thermostat.Enums.SetpointRaiseLowerModeEnum.kHeat, amount=30), endpoint=endpoint) @@ -704,9 +791,15 @@ async def test_TC_TSTAT_2_2(self): val = await self.read_single_attribute_check_success(endpoint=endpoint, cluster=cluster, attribute=cluster.Attributes.OccupiedHeatingSetpoint) asserts.assert_equal(val, OccupiedHeatingSetpointValue + 30 * 10) + if hasEventsFeature: + await self.check_setpoint_event(events_callback=events_callback, attribute=cluster.Attributes.OccupiedHeatingSetpoint, system_mode=cluster.Enums.SystemModeEnum.kHeat, occupancy=cluster.Bitmaps.OccupancyBitmap.kOccupied, endpoint=endpoint) + self.step("15") if self.pics_guard(hasCoolingFeature): + + self.flush_events(events_callback) + # Test Harness Sends SetpointRaise Command Cool Only await self.send_single_cmd(cmd=Clusters.Objects.Thermostat.Commands.SetpointRaiseLower(mode=Clusters.Objects.Thermostat.Enums.SetpointRaiseLowerModeEnum.kCool, amount=-30), endpoint=endpoint) @@ -714,12 +807,18 @@ async def test_TC_TSTAT_2_2(self): val = await self.read_single_attribute_check_success(endpoint=endpoint, cluster=cluster, attribute=cluster.Attributes.OccupiedCoolingSetpoint) asserts.assert_equal(val, OccupiedCoolingSetpointValue - 30 * 10) + if hasEventsFeature: + await self.check_setpoint_event(events_callback=events_callback, attribute=cluster.Attributes.OccupiedHeatingSetpoint, system_mode=cluster.Enums.SystemModeEnum.kHeat, occupancy=cluster.Bitmaps.OccupancyBitmap.kOccupied, endpoint=endpoint) + await self.check_setpoint_event(events_callback=events_callback, attribute=cluster.Attributes.OccupiedCoolingSetpoint, system_mode=cluster.Enums.SystemModeEnum.kCool, occupancy=cluster.Bitmaps.OccupancyBitmap.kOccupied, endpoint=endpoint) + self.step("16") if self.pics_guard(hasCoolingFeature): # Sets OccupiedCoolingSetpoint to default value await self.write_single_attribute(attribute_value=cluster.Attributes.OccupiedCoolingSetpoint(OccupiedCoolingSetpointValue), endpoint_id=endpoint) + self.flush_events(events_callback) + # Test Harness Sends SetpointRaise Command Cool Only await self.send_single_cmd(cmd=Clusters.Objects.Thermostat.Commands.SetpointRaiseLower(mode=Clusters.Objects.Thermostat.Enums.SetpointRaiseLowerModeEnum.kCool, amount=30), endpoint=endpoint) @@ -727,6 +826,9 @@ async def test_TC_TSTAT_2_2(self): val = await self.read_single_attribute_check_success(endpoint=endpoint, cluster=cluster, attribute=cluster.Attributes.OccupiedCoolingSetpoint) asserts.assert_equal(val, OccupiedCoolingSetpointValue + 30 * 10) + if hasEventsFeature: + await self.check_setpoint_event(events_callback=events_callback, attribute=cluster.Attributes.OccupiedCoolingSetpoint, system_mode=cluster.Enums.SystemModeEnum.kCool, occupancy=cluster.Bitmaps.OccupancyBitmap.kOccupied, endpoint=endpoint) + self.step("17") if self.pics_guard(hasCoolingFeature): @@ -738,6 +840,8 @@ async def test_TC_TSTAT_2_2(self): await self.write_single_attribute(attribute_value=cluster.Attributes.OccupiedHeatingSetpoint(OccupiedHeatingSetpointValue), endpoint_id=endpoint) if self.pics_guard(hasHeatingFeature or hasCoolingFeature): + self.flush_events(events_callback) + # Test Harness Sends SetpointRaise Command Heat & Cool await self.send_single_cmd(cmd=Clusters.Objects.Thermostat.Commands.SetpointRaiseLower(mode=Clusters.Objects.Thermostat.Enums.SetpointRaiseLowerModeEnum.kBoth, amount=-30), endpoint=endpoint) @@ -751,6 +855,10 @@ async def test_TC_TSTAT_2_2(self): val = await self.read_single_attribute_check_success(endpoint=endpoint, cluster=cluster, attribute=cluster.Attributes.OccupiedHeatingSetpoint) asserts.assert_equal(val, OccupiedHeatingSetpointValue - 30 * 10) + if hasEventsFeature: + await self.check_setpoint_event(events_callback=events_callback, attribute=cluster.Attributes.OccupiedCoolingSetpoint, system_mode=cluster.Enums.SystemModeEnum.kCool, occupancy=cluster.Bitmaps.OccupancyBitmap.kOccupied, endpoint=endpoint) + await self.check_setpoint_event(events_callback=events_callback, attribute=cluster.Attributes.OccupiedHeatingSetpoint, system_mode=cluster.Enums.SystemModeEnum.kHeat, occupancy=cluster.Bitmaps.OccupancyBitmap.kOccupied, endpoint=endpoint) + self.step("18") if self.pics_guard(hasCoolingFeature): @@ -762,6 +870,8 @@ async def test_TC_TSTAT_2_2(self): await self.write_single_attribute(attribute_value=cluster.Attributes.OccupiedHeatingSetpoint(OccupiedHeatingSetpointValue), endpoint_id=endpoint) if self.pics_guard(hasHeatingFeature or hasCoolingFeature): + self.flush_events(events_callback) + # Test Harness Sends SetpointRaise Command Heat & Cool await self.send_single_cmd(cmd=Clusters.Objects.Thermostat.Commands.SetpointRaiseLower(mode=Clusters.Objects.Thermostat.Enums.SetpointRaiseLowerModeEnum.kBoth, amount=30), endpoint=endpoint) @@ -775,6 +885,10 @@ async def test_TC_TSTAT_2_2(self): val = await self.read_single_attribute_check_success(endpoint=endpoint, cluster=cluster, attribute=cluster.Attributes.OccupiedHeatingSetpoint) asserts.assert_equal(val, OccupiedHeatingSetpointValue + 30 * 10) + if hasEventsFeature: + await self.check_setpoint_event(events_callback=events_callback, attribute=cluster.Attributes.OccupiedCoolingSetpoint, system_mode=cluster.Enums.SystemModeEnum.kCool, occupancy=cluster.Bitmaps.OccupancyBitmap.kOccupied, endpoint=endpoint) + await self.check_setpoint_event(events_callback=events_callback, attribute=cluster.Attributes.OccupiedHeatingSetpoint, system_mode=cluster.Enums.SystemModeEnum.kHeat, occupancy=cluster.Bitmaps.OccupancyBitmap.kOccupied, endpoint=endpoint) + if self.pics_guard(hasCoolingFeature): # Restores OccupiedCoolingSetpoint to original value await self.write_single_attribute(attribute_value=cluster.Attributes.OccupiedCoolingSetpoint(OccupiedCoolingSetpointValue), endpoint_id=endpoint)