blob: e20cefae889b7f5afbb32e9eaf6b8d4875de42a3 [file] [log] [blame]
/**
*
* Copyright (c) 2020 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.
*/
/**
*
* Copyright (c) 2020 Silicon Labs
*
* 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 Routines for the Groups Server plugin, the
*server implementation of the Groups cluster.
*******************************************************************************
******************************************************************************/
// *******************************************************************
// * groups-server.c
// *
// *
// * Copyright 2010 by Ember Corporation. All rights reserved. *80*
// *******************************************************************
#include "groups-server.h"
#include <app-common/zap-generated/attributes/Accessors.h>
#include <app-common/zap-generated/cluster-objects.h>
#include <app/CommandHandler.h>
#include <app/ConcreteCommandPath.h>
#include <app/util/af.h>
#include <app/util/binding-table.h>
#ifdef EMBER_AF_PLUGIN_SCENES
#include <app/clusters/scenes/scenes.h>
#endif // EMBER_AF_PLUGIN_SCENES
using namespace chip;
using namespace chip::app::Clusters;
using namespace chip::app::Clusters::Groups;
static bool isGroupPresent(EndpointId endpoint, GroupId groupId);
static bool bindingGroupMatch(EndpointId endpoint, GroupId groupId, EmberBindingTableEntry * entry);
static uint8_t findGroupIndex(EndpointId endpoint, GroupId groupId);
void emberAfGroupsClusterServerInitCallback(EndpointId endpoint)
{
// The high bit of Name Support indicates whether group names are supported.
// Group names are not supported by this plugin.
EmberAfStatus status;
uint8_t nameSupport = (uint8_t) emberAfPluginGroupsServerGroupNamesSupportedCallback(endpoint);
status = Attributes::NameSupport::Set(endpoint, nameSupport);
if (status != EMBER_ZCL_STATUS_SUCCESS)
{
emberAfGroupsClusterPrintln("ERR: writing name support %x", status);
}
}
// --------------------------
// Internal functions used to maintain the group table within the context
// of the binding table.
//
// In the binding:
// The first two bytes of the identifier is set to the groupId
// The local endpoint is set to the endpoint that is mapped to this group
// --------------------------
static EmberAfStatus addEntryToGroupTable(EndpointId endpoint, GroupId groupId, const CharSpan & groupName)
{
uint8_t i;
// Check for duplicates.
if (isGroupPresent(endpoint, groupId))
{
// Even if the group already exists, tell the application about the name,
// so it can cope with renames.
emberAfPluginGroupsServerSetGroupNameCallback(endpoint, groupId, groupName);
return EMBER_ZCL_STATUS_DUPLICATE_EXISTS;
}
// Look for an empty binding slot.
for (i = 0; i < EMBER_BINDING_TABLE_SIZE; i++)
{
EmberBindingTableEntry binding;
if (emberGetBinding(i, &binding) == EMBER_SUCCESS && binding.type == EMBER_UNUSED_BINDING)
{
EmberStatus status;
binding.type = EMBER_MULTICAST_BINDING;
binding.groupId = groupId;
binding.local = endpoint;
status = emberSetBinding(i, &binding);
if (status == EMBER_SUCCESS)
{
// Set the group name, if supported
emberAfPluginGroupsServerSetGroupNameCallback(endpoint, groupId, groupName);
return EMBER_ZCL_STATUS_SUCCESS;
}
else
{
emberAfGroupsClusterPrintln("ERR: Failed to create binding (0x%x)", status);
}
}
}
emberAfGroupsClusterPrintln("ERR: Binding table is full");
return EMBER_ZCL_STATUS_INSUFFICIENT_SPACE;
}
static EmberAfStatus removeEntryFromGroupTable(EndpointId endpoint, GroupId groupId)
{
if (isGroupPresent(endpoint, groupId))
{
uint8_t bindingIndex = findGroupIndex(endpoint, groupId);
EmberStatus status = emberDeleteBinding(bindingIndex);
if (status == EMBER_SUCCESS)
{
emberAfPluginGroupsServerSetGroupNameCallback(endpoint, groupId, CharSpan());
return EMBER_ZCL_STATUS_SUCCESS;
}
else
{
emberAfGroupsClusterPrintln("ERR: Failed to delete binding (0x%x)", status);
return EMBER_ZCL_STATUS_FAILURE;
}
}
return EMBER_ZCL_STATUS_NOT_FOUND;
}
bool emberAfGroupsClusterAddGroupCallback(app::CommandHandler * commandObj, const app::ConcreteCommandPath & commandPath,
const Commands::AddGroup::DecodableType & commandData)
{
auto & groupId = commandData.groupId;
auto & groupName = commandData.groupName;
EmberAfStatus status;
CHIP_ERROR err = CHIP_NO_ERROR;
emberAfGroupsClusterPrintln("RX: AddGroup 0x%2x, \"%.*s\"", groupId, static_cast<int>(groupName.size()), groupName.data());
status = addEntryToGroupTable(emberAfCurrentEndpoint(), groupId, groupName);
// For all networks, Add Group commands are only responded to when
// they are addressed to a single device.
if (emberAfCurrentCommand()->type != EMBER_INCOMING_UNICAST && emberAfCurrentCommand()->type != EMBER_INCOMING_UNICAST_REPLY)
{
return true;
}
{
app::CommandPathParams cmdParams = { emberAfCurrentEndpoint(), /* group id */ 0, Groups::Id, Commands::AddGroupResponse::Id,
(app::CommandPathFlags::kEndpointIdValid) };
TLV::TLVWriter * writer = nullptr;
SuccessOrExit(err = commandObj->PrepareCommand(cmdParams));
VerifyOrExit((writer = commandObj->GetCommandDataIBTLVWriter()) != nullptr, err = CHIP_ERROR_INCORRECT_STATE);
SuccessOrExit(err = writer->Put(TLV::ContextTag(0), status));
SuccessOrExit(err = writer->Put(TLV::ContextTag(1), groupId));
SuccessOrExit(err = commandObj->FinishCommand());
}
exit:
if (err != CHIP_NO_ERROR)
{
ChipLogError(Zcl, "Failed to encode response command.");
}
return true;
}
bool emberAfGroupsClusterViewGroupCallback(app::CommandHandler * commandObj, const app::ConcreteCommandPath & commandPath,
const Commands::ViewGroup::DecodableType & commandData)
{
auto & groupId = commandData.groupId;
EmberAfStatus status = EMBER_ZCL_STATUS_NOT_FOUND;
CHIP_ERROR err = CHIP_NO_ERROR;
uint8_t groupName[ZCL_GROUPS_CLUSTER_MAXIMUM_NAME_LENGTH + 1] = { 0 };
// Get the group name, if supported
emberAfPluginGroupsServerGetGroupNameCallback(emberAfCurrentEndpoint(), groupId, groupName);
emberAfGroupsClusterPrintln("RX: ViewGroup 0x%2x", groupId);
// For all networks, View Group commands can only be addressed to a
// single device.
if (emberAfCurrentCommand()->type != EMBER_INCOMING_UNICAST && emberAfCurrentCommand()->type != EMBER_INCOMING_UNICAST_REPLY)
{
return true;
}
if (isGroupPresent(emberAfCurrentEndpoint(), groupId))
{
status = EMBER_ZCL_STATUS_SUCCESS;
}
{
app::CommandPathParams cmdParams = { emberAfCurrentEndpoint(), /* group id */ 0, Groups::Id,
Commands::ViewGroupResponse::Id, (app::CommandPathFlags::kEndpointIdValid) };
TLV::TLVWriter * writer = nullptr;
SuccessOrExit(err = commandObj->PrepareCommand(cmdParams));
VerifyOrExit((writer = commandObj->GetCommandDataIBTLVWriter()) != nullptr, err = CHIP_ERROR_INCORRECT_STATE);
SuccessOrExit(err = writer->Put(TLV::ContextTag(0), status));
SuccessOrExit(err = writer->Put(TLV::ContextTag(1), groupId));
SuccessOrExit(err = writer->PutString(TLV::ContextTag(2), reinterpret_cast<char *>(&groupName[1]), groupName[0]));
SuccessOrExit(err = commandObj->FinishCommand());
}
exit:
if (err != CHIP_NO_ERROR)
{
ChipLogError(Zcl, "Failed to encode response command.");
}
return true;
}
bool emberAfGroupsClusterGetGroupMembershipCallback(app::CommandHandler * commandObj, const app::ConcreteCommandPath & commandPath,
const Commands::GetGroupMembership::DecodableType & commandData)
{
auto & groupCount = commandData.groupCount;
auto & groupList = commandData.groupList;
EmberStatus status = EMBER_ZCL_STATUS_FAILURE;
uint8_t i, j;
uint8_t count = 0;
uint8_t list[EMBER_BINDING_TABLE_SIZE << 1];
uint8_t listLen = 0;
CHIP_ERROR err = CHIP_NO_ERROR;
emberAfGroupsClusterPrint("RX: GetGroupMembership [");
// For all networks, Get Group Membership commands may be sent either
// unicast or broadcast (removing the ZLL-specific limitation to unicast).
// When Group Count is zero, respond with a list of all active groups.
// Otherwise, respond with a list of matches.
// TODO: https://github.com/project-chip/connectedhomeip/issues/10335
if (groupCount == 0)
{
for (i = 0; i < EMBER_BINDING_TABLE_SIZE; i++)
{
EmberBindingTableEntry entry;
status = emberGetBinding(i, &entry);
if ((status == EMBER_SUCCESS) && (entry.type == EMBER_MULTICAST_BINDING) && (entry.local == emberAfCurrentEndpoint()))
{
list[listLen] = EMBER_LOW_BYTE(entry.groupId);
list[listLen + 1] = EMBER_HIGH_BYTE(entry.groupId);
listLen = static_cast<uint8_t>(listLen + 2);
count++;
}
}
}
else
{
auto iter = groupList.begin();
while (iter.Next())
{
GroupId groupId = iter.GetValue();
emberAfGroupsClusterPrint(" 0x%02" PRIx16, groupId);
for (j = 0; j < EMBER_BINDING_TABLE_SIZE; j++)
{
EmberBindingTableEntry entry;
status = emberGetBinding(j, &entry);
if ((status == EMBER_SUCCESS) && (entry.type == EMBER_MULTICAST_BINDING))
{
if (entry.local == emberAfCurrentEndpoint() && entry.groupId == groupId)
{
list[listLen] = EMBER_LOW_BYTE(groupId);
list[listLen + 1] = EMBER_HIGH_BYTE(groupId);
listLen = static_cast<uint8_t>(listLen + 2);
count++;
}
}
}
}
err = iter.GetStatus();
}
emberAfGroupsClusterPrintln("]");
if (err != CHIP_NO_ERROR)
{
status = emberAfSendImmediateDefaultResponse(EMBER_ZCL_STATUS_MALFORMED_COMMAND);
err = CHIP_NO_ERROR;
goto exit;
}
// Only send a response if the Group Count was zero or if one or more active
// groups matched. Otherwise, a Default Response is sent.
if (groupCount == 0 || count != 0)
{
// A capacity of 0xFF means that it is unknown if any further groups may be
// added. Each group requires a binding and, because the binding table is
// used for other purposes besides groups, we can't be sure what the
// capacity will be in the future.
{
app::CommandPathParams cmdParams = { emberAfCurrentEndpoint(), /* group id */ 0, Groups::Id,
Commands::GetGroupMembershipResponse::Id,
(app::CommandPathFlags::kEndpointIdValid) };
TLV::TLVWriter * writer = nullptr;
SuccessOrExit(err = commandObj->PrepareCommand(cmdParams));
VerifyOrExit((writer = commandObj->GetCommandDataIBTLVWriter()) != nullptr, err = CHIP_ERROR_INCORRECT_STATE);
SuccessOrExit(err = writer->Put(TLV::ContextTag(0), static_cast<uint8_t>(0xff)));
SuccessOrExit(err = writer->Put(TLV::ContextTag(1), count));
SuccessOrExit(err = writer->PutBytes(TLV::ContextTag(2), list, listLen));
SuccessOrExit(err = commandObj->FinishCommand());
}
}
else
{
status = emberAfSendImmediateDefaultResponse(EMBER_ZCL_STATUS_NOT_FOUND);
}
if (EMBER_SUCCESS != status)
{
emberAfGroupsClusterPrintln("Groups: failed to send %s: 0x%x",
(groupCount == 0 || count != 0) ? "get_group_membership response" : "default_response", status);
}
exit:
if (err != CHIP_NO_ERROR)
{
ChipLogError(Zcl, "Failed to encode response command.");
}
return true;
}
bool emberAfGroupsClusterRemoveGroupCallback(app::CommandHandler * commandObj, const app::ConcreteCommandPath & commandPath,
const Commands::RemoveGroup::DecodableType & commandData)
{
auto & groupId = commandData.groupId;
EmberAfStatus status;
CHIP_ERROR err = CHIP_NO_ERROR;
emberAfGroupsClusterPrintln("RX: RemoveGroup 0x%2x", groupId);
status = removeEntryFromGroupTable(emberAfCurrentEndpoint(), groupId);
// For all networks, Remove Group commands are only responded to when
// they are addressed to a single device.
if (emberAfCurrentCommand()->type != EMBER_INCOMING_UNICAST && emberAfCurrentCommand()->type != EMBER_INCOMING_UNICAST_REPLY)
{
return true;
}
// EMAPPFWKV2-1414: if we remove a group, we should remove any scene
// associated with it. ZCL6: 3.6.2.3.5: "Note that if a group is
// removed the scenes associated with that group SHOULD be removed."
emberAfScenesClusterRemoveScenesInGroupCallback(emberAfCurrentEndpoint(), groupId);
{
app::CommandPathParams cmdParams = { emberAfCurrentEndpoint(), /* group id */ 0, Groups::Id,
Commands::RemoveGroupResponse::Id, (app::CommandPathFlags::kEndpointIdValid) };
TLV::TLVWriter * writer = nullptr;
SuccessOrExit(err = commandObj->PrepareCommand(cmdParams));
VerifyOrExit((writer = commandObj->GetCommandDataIBTLVWriter()) != nullptr, err = CHIP_ERROR_INCORRECT_STATE);
SuccessOrExit(err = writer->Put(TLV::ContextTag(0), status));
SuccessOrExit(err = writer->Put(TLV::ContextTag(1), groupId));
SuccessOrExit(err = commandObj->FinishCommand());
}
exit:
if (err != CHIP_NO_ERROR)
{
ChipLogError(Zcl, "Failed to encode response command.");
}
return true;
}
bool emberAfGroupsClusterRemoveAllGroupsCallback(app::CommandHandler * commandObj, const app::ConcreteCommandPath & commandPath,
const Commands::RemoveAllGroups::DecodableType & commandData)
{
EmberStatus sendStatus = EMBER_SUCCESS;
uint8_t i;
EndpointId endpoint = emberAfCurrentEndpoint();
bool success = true;
emberAfGroupsClusterPrintln("RX: RemoveAllGroups");
for (i = 0; i < EMBER_BINDING_TABLE_SIZE; i++)
{
EmberBindingTableEntry binding;
if (emberGetBinding(i, &binding) == EMBER_SUCCESS)
{
if (binding.type == EMBER_MULTICAST_BINDING && endpoint == binding.local)
{
EmberStatus status = emberDeleteBinding(i);
if (status != EMBER_SUCCESS)
{
success = false;
emberAfGroupsClusterPrintln("ERR: Failed to delete binding (0x%x)", status);
}
else
{
GroupId groupId = binding.groupId;
emberAfPluginGroupsServerSetGroupNameCallback(endpoint, groupId, CharSpan());
success = true && success;
// EMAPPFWKV2-1414: if we remove a group, we should remove any scene
// associated with it. ZCL6: 3.6.2.3.5: "Note that if a group is
// removed the scenes associated with that group SHOULD be removed."
emberAfScenesClusterRemoveScenesInGroupCallback(endpoint, groupId);
}
}
}
}
emberAfScenesClusterRemoveScenesInGroupCallback(emberAfCurrentEndpoint(), ZCL_SCENES_GLOBAL_SCENE_GROUP_ID);
sendStatus = emberAfSendImmediateDefaultResponse(success ? EMBER_ZCL_STATUS_SUCCESS : EMBER_ZCL_STATUS_FAILURE);
if (EMBER_SUCCESS != sendStatus)
{
emberAfGroupsClusterPrintln("Groups: failed to send %s: 0x%x", "default_response", sendStatus);
}
return true;
}
bool emberAfGroupsClusterAddGroupIfIdentifyingCallback(app::CommandHandler * commandObj,
const app::ConcreteCommandPath & commandPath,
const Commands::AddGroupIfIdentifying::DecodableType & commandData)
{
auto & groupId = commandData.groupId;
auto & groupName = commandData.groupName;
EmberAfStatus status;
EmberStatus sendStatus = EMBER_SUCCESS;
emberAfGroupsClusterPrintln("RX: AddGroupIfIdentifying 0x%2x, \"%.*s\"", groupId, static_cast<int>(groupName.size()),
groupName.data());
if (!emberAfIsDeviceIdentifying(emberAfCurrentEndpoint()))
{
// If not identifying, ignore add group -> success; not a failure.
status = EMBER_ZCL_STATUS_SUCCESS;
}
else
{
status = addEntryToGroupTable(emberAfCurrentEndpoint(), groupId, groupName);
}
sendStatus = emberAfSendImmediateDefaultResponse(status);
if (EMBER_SUCCESS != sendStatus)
{
emberAfGroupsClusterPrintln("Groups: failed to send %s: 0x%x", "default_response", sendStatus);
}
return true;
}
bool emberAfGroupsClusterEndpointInGroupCallback(EndpointId endpoint, GroupId groupId)
{
return isGroupPresent(endpoint, groupId);
}
void emberAfGroupsClusterClearGroupTableCallback(EndpointId endpoint)
{
uint8_t i, networkIndex = 0 /* emberGetCurrentNetwork() */;
for (i = 0; i < EMBER_BINDING_TABLE_SIZE; i++)
{
EmberBindingTableEntry binding;
if (emberGetBinding(i, &binding) == EMBER_SUCCESS && binding.type == EMBER_MULTICAST_BINDING &&
(endpoint == binding.local || (endpoint == EMBER_BROADCAST_ENDPOINT && networkIndex == binding.networkIndex)))
{
EmberStatus status = emberDeleteBinding(i);
if (status != EMBER_SUCCESS)
{
emberAfGroupsClusterPrintln("ERR: Failed to delete binding (0x%x)", status);
}
}
}
}
static bool isGroupPresent(EndpointId endpoint, GroupId groupId)
{
uint8_t i;
for (i = 0; i < EMBER_BINDING_TABLE_SIZE; i++)
{
EmberBindingTableEntry binding;
if (emberGetBinding(i, &binding) == EMBER_SUCCESS)
{
if (bindingGroupMatch(endpoint, groupId, &binding))
{
return true;
}
}
}
return false;
}
static bool bindingGroupMatch(EndpointId endpoint, GroupId groupId, EmberBindingTableEntry * entry)
{
return (entry->type == EMBER_MULTICAST_BINDING && entry->groupId == groupId && entry->local == endpoint);
}
static uint8_t findGroupIndex(EndpointId endpoint, GroupId groupId)
{
EmberStatus status;
uint8_t i;
for (i = 0; i < EMBER_BINDING_TABLE_SIZE; i++)
{
EmberBindingTableEntry entry;
status = emberGetBinding(i, &entry);
if ((status == EMBER_SUCCESS) && bindingGroupMatch(endpoint, groupId, &entry))
{
return i;
}
}
return EMBER_AF_GROUP_TABLE_NULL_INDEX;
}
void emberAfPluginGroupsServerGetGroupNameCallback(EndpointId endpoint, GroupId groupId, uint8_t * groupName) {}
bool emberAfPluginGroupsServerGroupNamesSupportedCallback(EndpointId endpoint)
{
return false;
}
void emberAfPluginGroupsServerSetGroupNameCallback(EndpointId endpoint, GroupId groupId, const CharSpan & groupName) {}
void MatterGroupsPluginServerInitCallback() {}