/** * * 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. */ #include "thermostat-server.h" #include 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; using namespace chip::app::Clusters::Thermostat::Structs; using namespace chip::app::Clusters::Globals::Structs; using namespace chip::Protocols::InteractionModel; namespace { /** * @brief Check if a preset is valid. * * @param[in] preset The preset to check. * * @return true If the preset is valid i.e the PresetHandle (if not null) fits within size constraints and the presetScenario enum * value is valid. Otherwise, return false. */ bool IsValidPresetEntry(const PresetStructWithOwnedMembers & preset) { // Check that the preset handle is not too long. if (!preset.GetPresetHandle().IsNull() && preset.GetPresetHandle().Value().size() > kPresetHandleSize) { return false; } // Ensure we have a valid PresetScenario. return (preset.GetPresetScenario() != PresetScenarioEnum::kUnknownEnumValue); } /** * @brief Checks if the preset is built-in * * @param[in] preset The preset to check. * * @return true If the preset is built-in, false otherwise. */ bool IsBuiltIn(const PresetStructWithOwnedMembers & preset) { return preset.GetBuiltIn().ValueOr(false); } /** * @brief Checks if the presets are matching i.e the presetHandles are the same. * * @param[in] preset The preset to check. * @param[in] presetToMatch The preset to match with. * * @return true If the presets match, false otherwise. If both preset handles are null, returns false */ bool PresetHandlesExistAndMatch(const PresetStructWithOwnedMembers & preset, const PresetStructWithOwnedMembers & presetToMatch) { return !preset.GetPresetHandle().IsNull() && !presetToMatch.GetPresetHandle().IsNull() && preset.GetPresetHandle().Value().data_equal(presetToMatch.GetPresetHandle().Value()); } /** * @brief Finds an entry in the pending presets list that matches a preset. * The presetHandle of the two presets must match. * * @param[in] delegate The delegate to use. * @param[in] presetToMatch The preset to match with. * * @return true if a matching entry was found in the pending presets list, false otherwise. */ bool MatchingPendingPresetExists(Delegate * delegate, const PresetStructWithOwnedMembers & presetToMatch) { VerifyOrReturnValue(delegate != nullptr, false); for (uint8_t i = 0; true; i++) { PresetStructWithOwnedMembers preset; CHIP_ERROR err = delegate->GetPendingPresetAtIndex(i, preset); if (err == CHIP_ERROR_PROVIDER_LIST_EXHAUSTED) { break; } if (err != CHIP_NO_ERROR) { ChipLogError(Zcl, "MatchingPendingPresetExists: GetPendingPresetAtIndex failed with error %" CHIP_ERROR_FORMAT, err.Format()); return false; } if (PresetHandlesExistAndMatch(preset, presetToMatch)) { return true; } } return false; } /** * @brief Finds and returns an entry in the Presets attribute list that matches * a preset, if such an entry exists. The presetToMatch must have a preset handle. * * @param[in] delegate The delegate to use. * @param[in] presetToMatch The preset to match with. * @param[out] matchingPreset The preset in the Presets attribute list that has the same PresetHandle as the presetToMatch. * * @return true if a matching entry was found in the presets attribute list, false otherwise. */ bool GetMatchingPresetInPresets(Delegate * delegate, const DataModel::Nullable & presetHandle, PresetStructWithOwnedMembers & matchingPreset) { VerifyOrReturnValue(delegate != nullptr, false); for (uint8_t i = 0; true; i++) { CHIP_ERROR err = delegate->GetPresetAtIndex(i, matchingPreset); if (err == CHIP_ERROR_PROVIDER_LIST_EXHAUSTED) { break; } if (err != CHIP_NO_ERROR) { ChipLogError(Zcl, "GetMatchingPresetInPresets: GetPresetAtIndex failed with error %" CHIP_ERROR_FORMAT, err.Format()); return false; } // Note: presets coming from our delegate always have a handle. if (presetHandle.Value().data_equal(matchingPreset.GetPresetHandle().Value())) { return true; } } return false; } /** * @brief Returns the length of the list of presets if the pending presets were to be applied. The size of the pending presets list * calculated, after all the constraint checks are done, is the new size of the updated Presets attribute since the pending * preset list is expected to have all existing presets with or without edits plus new presets. * This is called before changes are actually applied. * * @param[in] delegate The delegate to use. * * @return count of the updated Presets attribute if the pending presets were applied to it. Return 0 for error cases. */ uint8_t CountNumberOfPendingPresets(Delegate * delegate) { uint8_t numberOfPendingPresets = 0; VerifyOrReturnValue(delegate != nullptr, 0); for (uint8_t i = 0; true; i++) { PresetStructWithOwnedMembers pendingPreset; CHIP_ERROR err = delegate->GetPendingPresetAtIndex(i, pendingPreset); if (err == CHIP_ERROR_PROVIDER_LIST_EXHAUSTED) { break; } if (err != CHIP_NO_ERROR) { ChipLogError(Zcl, "CountNumberOfPendingPresets: GetPendingPresetAtIndex failed with error %" CHIP_ERROR_FORMAT, err.Format()); return 0; } numberOfPendingPresets++; } return numberOfPendingPresets; } /** * @brief Checks if the presetScenario is present in the PresetTypes attribute. * * @param[in] delegate The delegate to use. * @param[in] presetScenario The presetScenario to match with. * * @return true if the presetScenario is found, false otherwise. */ bool PresetScenarioExistsInPresetTypes(Delegate * delegate, PresetScenarioEnum presetScenario) { VerifyOrReturnValue(delegate != nullptr, false); for (uint8_t i = 0; true; i++) { PresetTypeStruct::Type presetType; auto err = delegate->GetPresetTypeAtIndex(i, presetType); if (err != CHIP_NO_ERROR) { return false; } if (presetType.presetScenario == presetScenario) { return true; } } return false; } /** * @brief Returns the count of preset entries in the pending presets list that have the matching presetHandle. * @param[in] delegate The delegate to use. * @param[in] presetHandleToMatch The preset handle to match. * * @return count of the number of presets found with the matching presetHandle. Returns 0 if no matching presets were found. */ uint8_t CountPresetsInPendingListWithPresetHandle(Delegate * delegate, const ByteSpan & presetHandleToMatch) { uint8_t count = 0; VerifyOrReturnValue(delegate != nullptr, count); for (uint8_t i = 0; true; i++) { PresetStructWithOwnedMembers preset; auto err = delegate->GetPendingPresetAtIndex(i, preset); if (err != CHIP_NO_ERROR) { return count; } DataModel::Nullable presetHandle = preset.GetPresetHandle(); if (!presetHandle.IsNull() && presetHandle.Value().data_equal(presetHandleToMatch)) { count++; } } return count; } /** * @brief Checks if the presetType for the given preset scenario supports name in the presetTypeFeatures bitmap. * * @param[in] delegate The delegate to use. * @param[in] presetScenario The presetScenario to match with. * * @return true if the presetType for the given preset scenario supports name, false otherwise. */ bool PresetTypeSupportsNames(Delegate * delegate, PresetScenarioEnum scenario) { VerifyOrReturnValue(delegate != nullptr, false); for (uint8_t i = 0; true; i++) { PresetTypeStruct::Type presetType; auto err = delegate->GetPresetTypeAtIndex(i, presetType); if (err != CHIP_NO_ERROR) { return false; } if (presetType.presetScenario == scenario) { return (presetType.presetTypeFeatures.Has(PresetTypeFeaturesBitmap::kSupportsNames)); } } return false; } /** * @brief Checks if the given preset handle is present in the presets attribute * @param[in] delegate The delegate to use. * @param[in] presetHandleToMatch The preset handle to match with. * * @return true if the given preset handle is present in the presets attribute list, false otherwise. */ bool IsPresetHandlePresentInPresets(Delegate * delegate, const ByteSpan & presetHandleToMatch) { VerifyOrReturnValue(delegate != nullptr, false); PresetStructWithOwnedMembers matchingPreset; for (uint8_t i = 0; true; i++) { CHIP_ERROR err = delegate->GetPresetAtIndex(i, matchingPreset); if (err == CHIP_ERROR_PROVIDER_LIST_EXHAUSTED) { return false; } if (err != CHIP_NO_ERROR) { ChipLogError(Zcl, "IsPresetHandlePresentInPresets: GetPresetAtIndex failed with error %" CHIP_ERROR_FORMAT, err.Format()); return false; } if (!matchingPreset.GetPresetHandle().IsNull() && matchingPreset.GetPresetHandle().Value().data_equal(presetHandleToMatch)) { return true; } } return false; } } // namespace namespace chip { namespace app { namespace Clusters { namespace Thermostat { extern ThermostatAttrAccess gThermostatAttrAccess; extern int16_t EnforceHeatingSetpointLimits(int16_t HeatingSetpoint, EndpointId endpoint); extern int16_t EnforceCoolingSetpointLimits(int16_t CoolingSetpoint, EndpointId endpoint); Status ThermostatAttrAccess::SetActivePreset(EndpointId endpoint, DataModel::Nullable presetHandle) { auto delegate = GetDelegate(endpoint); if (delegate == nullptr) { ChipLogError(Zcl, "Delegate is null"); return Status::InvalidInState; } // If the preset handle passed in the command is not present in the Presets attribute, return INVALID_COMMAND. if (!presetHandle.IsNull() && !IsPresetHandlePresentInPresets(delegate, presetHandle.Value())) { return Status::InvalidCommand; } CHIP_ERROR err = delegate->SetActivePresetHandle(presetHandle); if (err != CHIP_NO_ERROR) { ChipLogError(Zcl, "Failed to set ActivePresetHandle with error %" CHIP_ERROR_FORMAT, err.Format()); return StatusIB(err).mStatus; } return Status::Success; } CHIP_ERROR ThermostatAttrAccess::AppendPendingPreset(Thermostat::Delegate * delegate, const PresetStruct::Type & newPreset) { PresetStructWithOwnedMembers preset = newPreset; if (!IsValidPresetEntry(preset)) { return CHIP_IM_GLOBAL_STATUS(ConstraintError); } if (preset.GetPresetHandle().IsNull()) { if (IsBuiltIn(preset)) { return CHIP_IM_GLOBAL_STATUS(ConstraintError); } // Force to be false, if passed as null preset.SetBuiltIn(false); } else { // Per spec we need to check that: // (a) There is an existing non-pending preset with this handle. PresetStructWithOwnedMembers matchingPreset; if (!GetMatchingPresetInPresets(delegate, preset.GetPresetHandle().Value(), matchingPreset)) { return CHIP_IM_GLOBAL_STATUS(NotFound); } // (b) There is no existing pending preset with this handle. if (CountPresetsInPendingListWithPresetHandle(delegate, preset.GetPresetHandle().Value()) > 0) { return CHIP_IM_GLOBAL_STATUS(ConstraintError); } const auto & presetBuiltIn = preset.GetBuiltIn(); const auto & matchingPresetBuiltIn = matchingPreset.GetBuiltIn(); // (c)/(d) The built-in fields do not have a mismatch. if (presetBuiltIn.IsNull()) { if (matchingPresetBuiltIn.IsNull()) { // This really shouldn't happen; internal presets should alway have built-in set return CHIP_IM_GLOBAL_STATUS(InvalidInState); } preset.SetBuiltIn(matchingPresetBuiltIn.Value()); } else { if (matchingPresetBuiltIn.IsNull()) { // This really shouldn't happen; internal presets should alway have built-in set return CHIP_IM_GLOBAL_STATUS(InvalidInState); } if (presetBuiltIn.Value() != matchingPresetBuiltIn.Value()) { return CHIP_IM_GLOBAL_STATUS(ConstraintError); } } } if (!PresetScenarioExistsInPresetTypes(delegate, preset.GetPresetScenario())) { return CHIP_IM_GLOBAL_STATUS(ConstraintError); } if (preset.GetName().HasValue() && !PresetTypeSupportsNames(delegate, preset.GetPresetScenario())) { return CHIP_IM_GLOBAL_STATUS(ConstraintError); } // Before adding this preset to the pending presets, if the expected length of the pending presets' list // exceeds the total number of presets supported, return RESOURCE_EXHAUSTED. Note that the preset has not been appended yet. uint8_t numberOfPendingPresets = CountNumberOfPendingPresets(delegate); // We will be adding one more preset, so reject if the length is already at max. if (numberOfPendingPresets >= delegate->GetNumberOfPresets()) { return CHIP_IM_GLOBAL_STATUS(ResourceExhausted); } // TODO #34556 : Check if the number of presets for each presetScenario exceeds the max number of presets supported for that // scenario. We plan to support only one preset for each presetScenario for our use cases so defer this for re-evaluation. return delegate->AppendToPendingPresetList(preset); } Status ThermostatAttrAccess::PrecommitPresets(EndpointId endpoint) { auto delegate = GetDelegate(endpoint); if (delegate == nullptr) { ChipLogError(Zcl, "Delegate is null"); return Status::InvalidInState; } CHIP_ERROR err = CHIP_NO_ERROR; // For each preset in the presets attribute, check that the matching preset in the pending presets list does not // violate any spec constraints. for (uint8_t i = 0; true; i++) { PresetStructWithOwnedMembers preset; err = delegate->GetPresetAtIndex(i, preset); if (err == CHIP_ERROR_PROVIDER_LIST_EXHAUSTED) { break; } if (err != CHIP_NO_ERROR) { ChipLogError(Zcl, "emberAfThermostatClusterCommitPresetsSchedulesRequestCallback: GetPresetAtIndex failed with error " "%" CHIP_ERROR_FORMAT, err.Format()); return Status::InvalidInState; } bool found = MatchingPendingPresetExists(delegate, preset); // If a built in preset in the Presets attribute list is removed and not found in the pending presets list, return // CONSTRAINT_ERROR. if (IsBuiltIn(preset) && !found) { return Status::ConstraintError; } } // If there is an ActivePresetHandle set, find the preset in the pending presets list that matches the ActivePresetHandle // attribute. If a preset is not found with the same presetHandle, return INVALID_IN_STATE. If there is no ActivePresetHandle // attribute set, continue with other checks. uint8_t buffer[kPresetHandleSize]; MutableByteSpan activePresetHandleSpan(buffer); auto activePresetHandle = DataModel::MakeNullable(activePresetHandleSpan); err = delegate->GetActivePresetHandle(activePresetHandle); if (err != CHIP_NO_ERROR) { return Status::InvalidInState; } if (!activePresetHandle.IsNull()) { uint8_t count = CountPresetsInPendingListWithPresetHandle(delegate, activePresetHandle.Value()); if (count == 0) { return Status::InvalidInState; } } // For each preset in the pending presets list, check that the preset does not violate any spec constraints. for (uint8_t i = 0; true; i++) { PresetStructWithOwnedMembers pendingPreset; err = delegate->GetPendingPresetAtIndex(i, pendingPreset); if (err == CHIP_ERROR_PROVIDER_LIST_EXHAUSTED) { break; } if (err != CHIP_NO_ERROR) { ChipLogError(Zcl, "emberAfThermostatClusterCommitPresetsSchedulesRequestCallback: GetPendingPresetAtIndex failed with error " "%" CHIP_ERROR_FORMAT, err.Format()); return Status::InvalidInState; } // Enforce the Setpoint Limits for both the cooling and heating setpoints in the pending preset. // TODO: This code does not work, because it's modifying our temporary copy. Optional coolingSetpointValue = pendingPreset.GetCoolingSetpoint(); if (coolingSetpointValue.HasValue()) { pendingPreset.SetCoolingSetpoint(MakeOptional(EnforceCoolingSetpointLimits(coolingSetpointValue.Value(), endpoint))); } Optional heatingSetpointValue = pendingPreset.GetHeatingSetpoint(); if (heatingSetpointValue.HasValue()) { pendingPreset.SetHeatingSetpoint(MakeOptional(EnforceHeatingSetpointLimits(heatingSetpointValue.Value(), endpoint))); } } return Status::Success; } bool emberAfThermostatClusterSetActivePresetRequestCallback(CommandHandler * commandObj, const ConcreteCommandPath & commandPath, const Commands::SetActivePresetRequest::DecodableType & commandData) { auto status = gThermostatAttrAccess.SetActivePreset(commandPath.mEndpointId, commandData.presetHandle); commandObj->AddStatus(commandPath, status); return true; } } // namespace Thermostat } // namespace Clusters } // namespace app } // namespace chip bool emberAfThermostatClusterSetActivePresetRequestCallback(CommandHandler * commandObj, const ConcreteCommandPath & commandPath, const Commands::SetActivePresetRequest::DecodableType & commandData) { return Thermostat::emberAfThermostatClusterSetActivePresetRequestCallback(commandObj, commandPath, commandData); }