blob: d94b839e3959071b3e56e9c506fcfc0034365bc4 [file] [log] [blame]
/**
* Copyright (c) 2020-2026 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/groups-server/GroupsCluster.h>
#include <app/clusters/scenes-server/Constants.h>
#include <app/server-cluster/AttributeListBuilder.h>
#include <clusters/GroupKeyManagement/Ids.h>
#include <clusters/Groups/Attributes.h>
#include <clusters/Groups/Commands.h>
#include <clusters/Groups/Metadata.h>
#include <clusters/Groups/Structs.h>
#include <tracing/macros.h>
using namespace chip::app::Clusters::Groups;
using chip::Credentials::GroupDataProvider;
using chip::Protocols::InteractionModel::Status;
// The macro LogErrorOnFailure generally logs file:line which makes the macro larger. We use here a smaller
// macro (i.e. no line & file info) since the underlying error would have the original cause and logs would contain
// what call is being processed. This is to save about 100 bytes or so of flash.
#define LogIfFailure(expr) \
do \
{ \
CHIP_ERROR __err = (expr); \
if (__err != CHIP_NO_ERROR) \
{ \
ChipLogError(Zcl, "Error: %" CHIP_ERROR_FORMAT, __err.Format()); \
} \
} while (false)
namespace chip::app::Clusters {
namespace {
constexpr AttributePathParams kGroupKeyGroupTableAttributePath{ kRootEndpointId, GroupKeyManagement::Id,
GroupKeyManagement::Attributes::GroupTable::Id };
void NotifyGroupTableChanged(ServerClusterContext * context)
{
// TODO: This seems a bit coupled: we are notifying in this cluster that ANOTHER cluster
// has changed. We should support only one cluster or another really...
VerifyOrReturn(context != nullptr);
context->interactionContext.dataModelChangeListener.MarkDirty(kGroupKeyGroupTableAttributePath);
}
class AutoReleaseIterator
{
public:
AutoReleaseIterator(GroupDataProvider & provider, FabricIndex fabricIndex) : mIterator(provider.IterateGroupKeys(fabricIndex))
{}
~AutoReleaseIterator()
{
if (mIterator != nullptr)
{
mIterator->Release();
}
}
bool Valid() const { return mIterator != nullptr; }
GroupDataProvider::GroupKeyIterator * operator->() { return mIterator; }
private:
GroupDataProvider::GroupKeyIterator * mIterator;
};
/**
* @brief Checks if there are key set associated with the given GroupId
*/
bool KeyExists(GroupDataProvider & provider, FabricIndex fabricIndex, GroupId groupId)
{
AutoReleaseIterator it(provider, fabricIndex);
VerifyOrReturnValue(it.Valid(), false);
GroupDataProvider::GroupKey key;
while (it->Next(key))
{
Credentials::GroupDataProvider::KeySet keys;
if ((key.group_id == groupId) && (provider.GetKeySet(fabricIndex, key.keyset_id, keys) == CHIP_NO_ERROR))
{
return true;
}
}
return false;
}
struct GroupMembershipResponse
{
// A null capacity means that it is unknown if any further groups MAY be added.
const DataModel::Nullable<uint8_t> kCapacityUnknown;
// Use GetCommandId instead of commandId directly to avoid naming conflict with CommandIdentification in ExecutionOfACommand
static constexpr CommandId GetCommandId() { return Commands::GetGroupMembershipResponse::Id; }
static constexpr ClusterId GetClusterId() { return Groups::Id; }
GroupMembershipResponse(const Commands::GetGroupMembership::DecodableType & data, EndpointId endpoint,
GroupDataProvider::EndpointIterator * iter) :
mCommandData(data),
mEndpoint(endpoint), mIterator(iter)
{}
const Commands::GetGroupMembership::DecodableType & mCommandData;
EndpointId mEndpoint = kInvalidEndpointId;
GroupDataProvider::EndpointIterator * mIterator = nullptr;
CHIP_ERROR Encode(TLV::TLVWriter & writer, TLV::Tag tag) const
{
TLV::TLVType outer;
ReturnErrorOnFailure(writer.StartContainer(tag, TLV::kTLVType_Structure, outer));
ReturnErrorOnFailure(
DataModel::Encode(writer, TLV::ContextTag(Commands::GetGroupMembershipResponse::Fields::kCapacity), kCapacityUnknown));
{
TLV::TLVType type;
ReturnErrorOnFailure(writer.StartContainer(TLV::ContextTag(Commands::GetGroupMembershipResponse::Fields::kGroupList),
TLV::kTLVType_Array, type));
{
GroupDataProvider::GroupEndpoint mapping;
size_t requestedCount = 0;
ReturnErrorOnFailure(mCommandData.groupList.ComputeSize(&requestedCount));
auto accept_group = [&](GroupId groupId) {
// 1.3.6.3.1. If the GroupList field is empty, the entity SHALL respond with all group identifiers of which the
// entity is a member.
VerifyOrReturnValue(0 != requestedCount, true);
auto iter = mCommandData.groupList.begin();
while (iter.Next())
{
VerifyOrReturnValue(groupId != iter.GetValue(), true);
}
return false;
};
while (mIterator && mIterator->Next(mapping))
{
if (mapping.endpoint_id != mEndpoint)
{
continue;
}
if (!accept_group(mapping.group_id))
{
continue;
}
ReturnErrorOnFailure(DataModel::Encode(writer, TLV::AnonymousTag(), mapping.group_id));
ChipLogDetail(Zcl, " 0x%02x", mapping.group_id);
}
ChipLogDetail(Zcl, "]");
}
ReturnErrorOnFailure(writer.EndContainer(type));
}
ReturnErrorOnFailure(writer.EndContainer(outer));
return CHIP_NO_ERROR;
}
};
} // namespace
CHIP_ERROR GroupsCluster::Attributes(const ConcreteClusterPath & path, ReadOnlyBufferBuilder<DataModel::AttributeEntry> & builder)
{
AttributeListBuilder listBuilder(builder);
return listBuilder.Append(Attributes::kMandatoryMetadata, {});
}
CHIP_ERROR GroupsCluster::AcceptedCommands(const ConcreteClusterPath & path,
ReadOnlyBufferBuilder<DataModel::AcceptedCommandEntry> & builder)
{
using namespace Commands;
static constexpr DataModel::AcceptedCommandEntry kAcceptedCommands[] = {
AddGroup::kMetadataEntry, //
ViewGroup::kMetadataEntry, //
GetGroupMembership::kMetadataEntry, //
RemoveGroup::kMetadataEntry, //
RemoveAllGroups::kMetadataEntry, //
AddGroupIfIdentifying::kMetadataEntry, //
};
return builder.ReferenceExisting(kAcceptedCommands);
}
CHIP_ERROR GroupsCluster::GeneratedCommands(const ConcreteClusterPath & path, ReadOnlyBufferBuilder<CommandId> & builder)
{
using namespace Commands;
static constexpr CommandId kGeneratedCommands[] = {
AddGroupResponse::Id, //
ViewGroupResponse::Id, //
GetGroupMembershipResponse::Id, //
RemoveGroupResponse::Id, //
};
return builder.ReferenceExisting(kGeneratedCommands);
}
DataModel::ActionReturnStatus GroupsCluster::ReadAttribute(const DataModel::ReadAttributeRequest & request,
AttributeValueEncoder & encoder)
{
using namespace Attributes;
switch (request.path.mAttributeId)
{
case ClusterRevision::Id:
return encoder.Encode(kRevision);
case FeatureMap::Id:
// Group names is hardcoded (feature is M conformance in the spec)
return encoder.Encode(Feature::kGroupNames);
case NameSupport::Id:
// According to the spec, kGroupNames must be set (M conformance in the spec)
return encoder.Encode(NameSupportBitmap::kGroupNames);
default:
return Status::UnsupportedAttribute;
}
}
std::optional<DataModel::ActionReturnStatus>
GroupsCluster::InvokeCommand(const DataModel::InvokeRequest & request, TLV::TLVReader & input_arguments, CommandHandler * handler)
{
using namespace Commands;
const FabricIndex fabricIndex = request.GetAccessingFabricIndex();
switch (request.path.mCommandId)
{
case AddGroup::Id: {
MATTER_TRACE_SCOPE("AddGroup", "Groups");
AddGroup::DecodableType request_data;
ReturnErrorOnFailure(request_data.Decode(input_arguments, fabricIndex));
Groups::Commands::AddGroupResponse::Type response;
response.groupID = request_data.groupID;
response.status = to_underlying(AddGroup(request_data.groupID, request_data.groupName, fabricIndex));
handler->AddResponse(request.path, response);
return std::nullopt;
}
case ViewGroup::Id: {
MATTER_TRACE_SCOPE("ViewGroup", "Groups");
ViewGroup::DecodableType request_data;
ReturnErrorOnFailure(request_data.Decode(input_arguments, fabricIndex));
const GroupId groupId = request_data.groupID;
Credentials::GroupDataProvider::GroupInfo info;
const Status status = [&]() {
VerifyOrReturnError(IsValidGroupId(groupId), Status::ConstraintError);
VerifyOrReturnError(mGroupDataProvider.HasEndpoint(fabricIndex, groupId, mPath.mEndpointId), Status::NotFound);
if (mGroupDataProvider.GetGroupInfo(fabricIndex, groupId, info) != CHIP_NO_ERROR)
{
return Status::NotFound;
}
return Status::Success;
}();
if (status != Status::Success)
{
info.name[0] = 0;
}
handler->AddResponse(request.path,
ViewGroupResponse::Type{
.status = to_underlying(status),
.groupID = request_data.groupID,
.groupName = CharSpan(info.name, strnlen(info.name, GroupDataProvider::GroupInfo::kGroupNameMax)),
});
return std::nullopt;
}
case GetGroupMembership::Id: {
MATTER_TRACE_SCOPE("GetGroupMembership", "Groups");
GetGroupMembership::DecodableType request_data;
ReturnErrorOnFailure(request_data.Decode(input_arguments, fabricIndex));
GroupDataProvider::EndpointIterator * iter = mGroupDataProvider.IterateEndpoints(fabricIndex);
VerifyOrReturnError(nullptr != iter, Status::Failure);
handler->AddResponse(request.path, GroupMembershipResponse(request_data, mPath.mEndpointId, iter));
iter->Release();
return std::nullopt;
}
case RemoveGroup::Id: {
MATTER_TRACE_SCOPE("RemoveGroup", "Groups");
RemoveGroup::DecodableType request_data;
ReturnErrorOnFailure(request_data.Decode(input_arguments, fabricIndex));
const GroupId groupId = request_data.groupID;
handler->AddResponse(
request.path,
Groups::Commands::RemoveGroupResponse::Type{
.status = to_underlying([&]() {
VerifyOrReturnError(IsValidGroupId(groupId), Status::ConstraintError);
VerifyOrReturnError(mGroupDataProvider.HasEndpoint(fabricIndex, groupId, mPath.mEndpointId), Status::NotFound);
if (CHIP_ERROR err = mGroupDataProvider.RemoveEndpoint(fabricIndex, groupId, mPath.mEndpointId);
err != CHIP_NO_ERROR)
{
ChipLogDetail(Zcl, "ERR: Failed to remove mapping (end:%d, group:0x%x), err:%" CHIP_ERROR_FORMAT,
mPath.mEndpointId, groupId, err.Format());
return Status::NotFound;
}
if (mScenesIntegration != nullptr)
{
// If a group is removed the scenes associated with that group SHOULD be removed.
LogIfFailure(mScenesIntegration->GroupWillBeRemoved(fabricIndex, groupId));
}
NotifyGroupTableChanged(mContext);
return Status::Success;
}()),
.groupID = request_data.groupID,
});
return std::nullopt;
}
case RemoveAllGroups::Id: {
MATTER_TRACE_SCOPE("RemoveAllGroups", "Groups");
if (mScenesIntegration != nullptr)
{
GroupDataProvider::EndpointIterator * iter = mGroupDataProvider.IterateEndpoints(fabricIndex);
GroupDataProvider::GroupEndpoint mapping;
VerifyOrReturnError(nullptr != iter, Status::Failure);
while (iter->Next(mapping))
{
if (mPath.mEndpointId == mapping.endpoint_id)
{
LogIfFailure(mScenesIntegration->GroupWillBeRemoved(fabricIndex, mapping.group_id));
}
}
iter->Release();
LogIfFailure(mScenesIntegration->GroupWillBeRemoved(fabricIndex, scenes::kGlobalSceneGroupId));
}
LogIfFailure(mGroupDataProvider.RemoveEndpoint(fabricIndex, mPath.mEndpointId));
NotifyGroupTableChanged(mContext);
return Status::Success;
}
case AddGroupIfIdentifying::Id: {
MATTER_TRACE_SCOPE("AddGroupIfIdentifying", "Groups");
AddGroupIfIdentifying::DecodableType request_data;
ReturnErrorOnFailure(request_data.Decode(input_arguments, fabricIndex));
// skip with success if we are not identifying
VerifyOrReturnValue((mIdentifyIntegration != nullptr) && mIdentifyIntegration->IsIdentifying(), Status::Success);
// AddGroupIfIdentifying is response `Y` in the spec: we return the status (not a structure, as opposed to AddGroup)
return AddGroup(request_data.groupID, request_data.groupName, fabricIndex);
}
default:
return Status::UnsupportedCommand;
}
}
Status GroupsCluster::AddGroup(GroupId groupID, CharSpan groupName, FabricIndex fabricIndex)
{
VerifyOrReturnError(IsValidGroupId(groupID), Status::ConstraintError);
VerifyOrReturnError(groupName.size() <= GroupDataProvider::GroupInfo::kGroupNameMax, Status::ConstraintError);
VerifyOrReturnError(KeyExists(mGroupDataProvider, fabricIndex, groupID), Status::UnsupportedAccess);
// Add a new entry to the GroupTable
if (CHIP_ERROR err = mGroupDataProvider.SetGroupInfo(fabricIndex, GroupDataProvider::GroupInfo(groupID, groupName));
err != CHIP_NO_ERROR)
{
ChipLogDetail(Zcl, "ERR: Failed to add mapping (end:%d, group:0x%x), err:%" CHIP_ERROR_FORMAT, mPath.mEndpointId, groupID,
err.Format());
return Status::ResourceExhausted;
}
if (CHIP_ERROR err = mGroupDataProvider.AddEndpoint(fabricIndex, groupID, mPath.mEndpointId); err != CHIP_NO_ERROR)
{
ChipLogDetail(Zcl, "ERR: Failed to add mapping (end:%d, group:0x%x), err:%" CHIP_ERROR_FORMAT, mPath.mEndpointId, groupID,
err.Format());
// best-effort undo the mapping we added above, to try to be a bit more atomic.
LogIfFailure(mGroupDataProvider.RemoveGroupInfo(fabricIndex, groupID));
return Status::ResourceExhausted;
}
NotifyGroupTableChanged(mContext);
return Status::Success;
}
} // namespace chip::app::Clusters