From 161b256b40c55ec1af4beb9d72a53536717f0551 Mon Sep 17 00:00:00 2001 From: William Date: Wed, 31 Jul 2024 03:58:19 +0100 Subject: [PATCH] Implementation of service area server (#33991) * Add definitions for common HomeLocationStruct and types - see connectedhomeip-spec PR 8933 * Add definition for Service Area Cluster - see connectedhomeip-spec PR 8937 * Add semantic tag definitions needed for Service Area Cluster - see connectedhomeip-spec PR 8937 * Update Service Area Cluster definition of LocationInfoStruct - see connectedhomeip-spec PR 8937 * Rename SurfaceTag to FloorSurfaceTag - spec PR 8937 commit 431739b3e3996d0ef74a36b2a6b93ac0d3db9c45 * Fix Carport typo, remove IsGroundFloor field (spec change) * Update home location definitions per spec PR 8933 commit f04958166412d5b7eff4d3443273f47f12f22baf 2024-05-23 * update generated files * Update Service Area Cluster definitions per spec PR 8937 up to commit 6bf3762eb1ee733c642e79074744d1185b82a89c 2024-05-24 * update generated files * Initial implementations of service-area-cluster per spec PR 8937 up to commit 6bf3762eb1ee733c642e79074744d1185b82a89c 2024-05-24 * Regenerated zap files after merge. * Moved the AreaTypeTag namespace definition to namespaces.xml. * Moved the HomeLoc global struct into global-sturcts.xml. * Updated the AreaTypeTag namespace to match the latest spec definition. * Updated the AreaTypeTag namespace to match the latest spec definition. * Updated the matterlint rules with the changes in the xml files. * Reverted changes to the rvc zap file. * Addded global xml files to relevant lists of xmls. * Added the Position, Landmark and Floor Surface Namespaces. * Removed namespace tag definitions from the service area cluters XML. * Regenerated zap files. * Rewrite service-area-server and delegate to avoid disallowed c++ containers * Regenerated zap files. * Regenerated zap files. * Explicitly set the conformance. * Fixed typos in the service-area clusters XML from review. * Used a more feature rich method for defining the cluster's features. * Reordered data-type definitions to match the order in the spec. * Regenerated zap files. * Updated the service area type names following fixes to the XMLs. * Updated the rvc-example type names following fixes to the XMLs. * Simplified some of the logic in the service-area cluster objects. * Added an InEqual method to the LocationSturctureWrapper and simplified the Instance::IsUniqueSupportedLocation impl. * Removed the handle volotile methos as the delegate should be responsibel for updating these values as they change. * Apply editorial suggestions from code review. Includes a minor bug fix. Co-authored-by: Kiel Oleson Co-authored-by: Petru Lauric <81822411+plauric@users.noreply.github.com> * Replaced Location Location in the logging text with Service Area. * Implemented some renaming and editorial changes from review. Moved some service area methods from public to private. * Added the ServicArea to the controller init.py * Moved the HandleSupportedLocationsUpdated method form the instance to the server. * Moved delegate implementations to the .cpp * Removed calling the HandleSupportedLocationsUpdate when adding a new supported location as the restrictions imposed on the SelectedLocations, CurrentLocation and Progress attributs are maintained. * Removed calling the HandleSupportedLocationsUpdate when adding a new supported map as the restrictions imposed on the SupprotedLocations attribut are maintained. * Renamed a delegate method and updated it's documentation. * Reverted a change to the darwin file for apple engineers to make. * Removed the optional cluster id prameter from the service area Instance constructor. * Removed unneeded check. * Added a way to get the Service Area instance from the delegate. Added a virtual Init method to the Service area delegate. * Moved the rvc service area delegate impl out of the rvc device class and into it's own class. * Comment chages to the service area delegate in the rvc example app. * Added github issue to a comment. * Restyled by whitespace * Restyled by clang-format * Restyled by gn * Restyled by isort * zap regen after pull. * Updated the Service Area code documentation following a review comment. Move the IsProgressElement to the delegate and added a default implementation. * Restyled by whitespace * Restyled by clang-format * Renamed DoseNameMatch to IsNameEqual. * Replaced the multiple bool inputs of the IsEqual method with a BitMask. * Removed the use of ret_value to improve readability. * Removed redundant ; * Removed redundant use of the namespaces. * Moved the empty impl of MatterServiceAreaPluginServerInitCallback to util. * Simplified some method returns. * Updated a log level. * Removed extra logs while reading attributes. * Removed else after if return. * Fixed sizeof-array-decay warning. * Cast all std::vector.size() returns in the RVC service area delegate to uint32_t. The service area server limits the size of these lists to 255. * Change the way the Delegate and Instance classes are stopped from being copyabel. * Fixed a bug that was causing the clang-tidy CI test to seg fault. * Restyled by clang-format * Renamed namespaces.xml to semantic-tag-namespace-enums.xml. * Missed some rename locations. * Regenerated zap files after sync with upstream. * Fixed typos from review. * Fixed the termination of the Location and Map structure wrappers' name buffers. * - Replaced the use of `char *` with `MutableCharSpan`. - Removed the use of the VerifyOrExit macro. - Removed `IsSupportedMapChangeAllowed` check in the `AddSupportedMap` method as adding maps does not compromise any of the other attributes. * Restyled by clang-format * Refactored out the use of ret_value. * Removed an unused method in the rvc-app * Refactored rvc-app to use static casts. * Fixed missing ; * Restyled by clang-format * Rplaced the use of fromCharSpan with _span in the rvc-app. * Removed unneeded casting. * Removed unnecessary null terminations in the service area struct wrappers. * Fixed an error in the docs. * Restyled by clang-format * Moved the responsibility of checking for duplicates in the selected locations command attribute to the server. * Restyled by clang-format * Removed unneccessary memset. * Updated some docs. * Removed unneeded Raw() call. * Added attribute ID to reading log. * Removed the use of __func__ * Fixed incorrect command handling statuses. * Fixed some code documentation. * Fixed bug in handling of errorStatusTexts from the delegate. * Rnamed dummy vars as ignored. * Fixed the printing of map name. * Replaced use of Nullable with NullNullable. * Added a log that was commented out. * Added non-list service area attributes to the list of attributeAccessInterfaceAttributes. * Fixed bug relateing to getting a null value. * Improved the readability of the logic for determining if a change to the service area's estimated end time attribute should be reported. * Do not report CurrentLocation change if it does not change. * Simplified logic returns Co-authored-by: Andrei Litvin * Restyled by whitespace * Restyled by clang-format * Restyled by prettier-json * Changed the way the Commond Handler Interface is registered following the merge. * Made all delegate methods public. Removed the Instance class from being a friend of the Delegated class. * Restyled by clang-format * Reduced the socope of variabels to improve readability. * Regenerated zap files after sync with upstream. * Fixed name change of LocationDescriptorStruct * Restyled by clang-format * Added FeatureMap to the list of attributeAccessInterfaceAttributes. * Do not consider list of 0 as error. * Fixed attribute ID printing. * Apply suggestions from code review Co-authored-by: Boris Zbarsky * Generated zap code after merge. * Fixed the logic of the ReportEstimatedEndTimeChange method. --------- Co-authored-by: jfierke@irobot.com Co-authored-by: Kiel Oleson Co-authored-by: Petru Lauric <81822411+plauric@users.noreply.github.com> Co-authored-by: Restyled.io Co-authored-by: Andrei Litvin Co-authored-by: Boris Zbarsky --- examples/rvc-app/linux/BUILD.gn | 1 + .../rvc-app/rvc-common/include/rvc-device.h | 9 +- .../include/rvc-service-area-delegate.h | 124 ++ examples/rvc-app/rvc-common/rvc-app.matter | 310 +++++ examples/rvc-app/rvc-common/rvc-app.zap | 236 ++++ .../rvc-app/rvc-common/src/rvc-device.cpp | 1 + .../src/rvc-service-area-delegate.cpp | 412 ++++++ src/app/chip_data_model.gni | 8 + .../basic-information/basic-information.cpp | 2 +- .../service-area-cluster-objects.h | 358 ++++++ .../service-area-delegate.cpp | 90 ++ .../service-area-delegate.h | 384 ++++++ .../service-area-server.cpp | 1131 +++++++++++++++++ .../service-area-server/service-area-server.h | 357 ++++++ src/app/common/templates/config-data.yaml | 1 + src/app/util/util.cpp | 1 + .../zcl/zcl-with-test-extensions.json | 3 +- src/app/zap-templates/zcl/zcl.json | 3 +- src/app/zap_cluster_list.json | 2 + .../python/chip/clusters/__init__.py | 6 +- .../zap-generated/attributes/Accessors.cpp | 222 ---- .../zap-generated/attributes/Accessors.h | 28 - .../app-common/zap-generated/callback.h | 12 - 23 files changed, 3432 insertions(+), 269 deletions(-) create mode 100644 examples/rvc-app/rvc-common/include/rvc-service-area-delegate.h create mode 100644 examples/rvc-app/rvc-common/src/rvc-service-area-delegate.cpp create mode 100644 src/app/clusters/service-area-server/service-area-cluster-objects.h create mode 100644 src/app/clusters/service-area-server/service-area-delegate.cpp create mode 100644 src/app/clusters/service-area-server/service-area-delegate.h create mode 100644 src/app/clusters/service-area-server/service-area-server.cpp create mode 100644 src/app/clusters/service-area-server/service-area-server.h diff --git a/examples/rvc-app/linux/BUILD.gn b/examples/rvc-app/linux/BUILD.gn index 2db48c20b27357..0adb4e9c9ae8e9 100644 --- a/examples/rvc-app/linux/BUILD.gn +++ b/examples/rvc-app/linux/BUILD.gn @@ -27,6 +27,7 @@ executable("chip-rvc-app") { "${chip_root}/examples/rvc-app/rvc-common/src/rvc-device.cpp", "${chip_root}/examples/rvc-app/rvc-common/src/rvc-mode-delegates.cpp", "${chip_root}/examples/rvc-app/rvc-common/src/rvc-operational-state-delegate.cpp", + "${chip_root}/examples/rvc-app/rvc-common/src/rvc-service-area-delegate.cpp", "RvcAppCommandDelegate.cpp", "include/CHIPProjectAppConfig.h", "main.cpp", diff --git a/examples/rvc-app/rvc-common/include/rvc-device.h b/examples/rvc-app/rvc-common/include/rvc-device.h index f6a33a6199dd23..092ded91f76272 100644 --- a/examples/rvc-app/rvc-common/include/rvc-device.h +++ b/examples/rvc-app/rvc-common/include/rvc-device.h @@ -2,8 +2,11 @@ #include "rvc-mode-delegates.h" #include "rvc-operational-state-delegate.h" +#include "rvc-service-area-delegate.h" #include #include +#include +#include #include @@ -23,6 +26,9 @@ class RvcDevice RvcOperationalState::RvcOperationalStateDelegate mOperationalStateDelegate; RvcOperationalState::Instance mOperationalStateInstance; + ServiceArea::RvcServiceAreaDelegate mServiceAreaDelegate; + ServiceArea::Instance mServiceAreaInstance; + bool mDocked = false; bool mCharging = false; @@ -37,7 +43,8 @@ class RvcDevice explicit RvcDevice(EndpointId aRvcClustersEndpoint) : mRunModeDelegate(), mRunModeInstance(&mRunModeDelegate, aRvcClustersEndpoint, RvcRunMode::Id, 0), mCleanModeDelegate(), mCleanModeInstance(&mCleanModeDelegate, aRvcClustersEndpoint, RvcCleanMode::Id, 0), mOperationalStateDelegate(), - mOperationalStateInstance(&mOperationalStateDelegate, aRvcClustersEndpoint) + mOperationalStateInstance(&mOperationalStateDelegate, aRvcClustersEndpoint), mServiceAreaDelegate(), + mServiceAreaInstance(&mServiceAreaDelegate, aRvcClustersEndpoint, BitMask(0)) { // set the current-mode at start-up mRunModeInstance.UpdateCurrentMode(RvcRunMode::ModeIdle); diff --git a/examples/rvc-app/rvc-common/include/rvc-service-area-delegate.h b/examples/rvc-app/rvc-common/include/rvc-service-area-delegate.h new file mode 100644 index 00000000000000..e170d2ffeb35ce --- /dev/null +++ b/examples/rvc-app/rvc-common/include/rvc-service-area-delegate.h @@ -0,0 +1,124 @@ +/* + * + * Copyright (c) 2024 Project CHIP Authors + * All rights reserved. + * + * 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. + */ + +#pragma once + +#include +#include +#include +#include + +namespace chip { +namespace app { +namespace Clusters { + +class RvcDevice; + +namespace ServiceArea { + +class RvcServiceAreaDelegate : public Delegate +{ +private: + // containers for array attributes. + std::vector mSupportedLocations; + std::vector mSupportedMaps; + std::vector mSelectedLocations; + std::vector mProgressList; + +public: + CHIP_ERROR Init() override; + + // command support + bool IsSetSelectedLocationsAllowed(MutableCharSpan statusText) override; + + bool IsValidSelectLocationsSet(const ServiceArea::Commands::SelectLocations::DecodableType & req, + ServiceArea::SelectLocationsStatus & locationStatus, MutableCharSpan statusText) override; + + bool HandleSkipCurrentLocation(MutableCharSpan skipStatusText) override; + + //************************************************************************* + // Supported Locations accessors + + bool IsSupportedLocationsChangeAllowed() override; + + uint32_t GetNumberOfSupportedLocations() override; + + bool GetSupportedLocationByIndex(uint32_t listIndex, ServiceArea::LocationStructureWrapper & supportedLocation) override; + + bool GetSupportedLocationById(uint32_t aLocationId, uint32_t & listIndex, + ServiceArea::LocationStructureWrapper & supportedLocation) override; + + bool AddSupportedLocation(const ServiceArea::LocationStructureWrapper & newLocation, uint32_t & listIndex) override; + + bool ModifySupportedLocation(uint32_t listIndex, const ServiceArea::LocationStructureWrapper & modifiedLocation) override; + + bool ClearSupportedLocations() override; + + //************************************************************************* + // Supported Maps accessors + + bool IsSupportedMapChangeAllowed() override; + + uint32_t GetNumberOfSupportedMaps() override; + + bool GetSupportedMapByIndex(uint32_t listIndex, ServiceArea::MapStructureWrapper & supportedMap) override; + + bool GetSupportedMapById(uint8_t aMapId, uint32_t & listIndex, ServiceArea::MapStructureWrapper & supportedMap) override; + + bool AddSupportedMap(const ServiceArea::MapStructureWrapper & newMap, uint32_t & listIndex) override; + + bool ModifySupportedMap(uint32_t listIndex, const ServiceArea::MapStructureWrapper & newMap) override; + + bool ClearSupportedMaps() override; + + //************************************************************************* + // Selected Locations accessors + + uint32_t GetNumberOfSelectedLocations() override; + + bool GetSelectedLocationByIndex(uint32_t listIndex, uint32_t & selectedLocation) override; + + // IsSelectedLocation() no override + + bool AddSelectedLocation(uint32_t aLocationId, uint32_t & listIndex) override; + + bool ClearSelectedLocations() override; + + //************************************************************************* + // Progress accessors + + uint32_t GetNumberOfProgressElements() override; + + bool GetProgressElementByIndex(uint32_t listIndex, ServiceArea::Structs::ProgressStruct::Type & aProgressElement) override; + + bool GetProgressElementById(uint32_t aLocationId, uint32_t & listIndex, + ServiceArea::Structs::ProgressStruct::Type & aProgressElement) override; + + bool AddProgressElement(const ServiceArea::Structs::ProgressStruct::Type & newProgressElement, uint32_t & listIndex) override; + + bool ModifyProgressElement(uint32_t listIndex, + const ServiceArea::Structs::ProgressStruct::Type & modifiedProgressElement) override; + + bool ClearProgress() override; +}; + +} // namespace ServiceArea + +} // namespace Clusters +} // namespace app +} // namespace chip diff --git a/examples/rvc-app/rvc-common/rvc-app.matter b/examples/rvc-app/rvc-common/rvc-app.matter index c22e5758ba17c1..cd377ef709b2b7 100644 --- a/examples/rvc-app/rvc-common/rvc-app.matter +++ b/examples/rvc-app/rvc-common/rvc-app.matter @@ -1208,6 +1208,296 @@ cluster RvcOperationalState = 97 { command GoHome(): OperationalCommandResponse = 128; } +/** The Service Area cluster provides an interface for controlling the locations where a device should operate, and for querying the current location. */ +provisional cluster ServiceArea = 336 { + revision 1; // NOTE: Default/not specifically set + + enum AreaTypeTag : enum8 { + kAisle = 0; + kAttic = 1; + kBackDoor = 2; + kBackYard = 3; + kBalcony = 4; + kBallroom = 5; + kBathroom = 6; + kBedroom = 7; + kBorder = 8; + kBoxroom = 9; + kBreakfastRoom = 10; + kCarport = 11; + kCellar = 12; + kCloakroom = 13; + kCloset = 14; + kConservatory = 15; + kCorridor = 16; + kCraftRoom = 17; + kCupboard = 18; + kDeck = 19; + kDen = 20; + kDining = 21; + kDrawingRoom = 22; + kDressingRoom = 23; + kDriveway = 24; + kElevator = 25; + kEnsuite = 26; + kEntrance = 27; + kEntryway = 28; + kFamilyRoom = 29; + kFoyer = 30; + kFrontDoor = 31; + kFrontYard = 32; + kGameRoom = 33; + kGarage = 34; + kGarageDoor = 35; + kGarden = 36; + kGardenDoor = 37; + kGuestBathroom = 38; + kGuestBedroom = 39; + kGuestRestroom = 40; + kGuestRoom = 41; + kGym = 42; + kHallway = 43; + kHearthRoom = 44; + kKidsRoom = 45; + kKidsBedroom = 46; + kKitchen = 47; + kLarder = 48; + kLaundryRoom = 49; + kLawn = 50; + kLibrary = 51; + kLivingRoom = 52; + kLounge = 53; + kMediaTVRoom = 54; + kMudRoom = 55; + kMusicRoom = 56; + kNursery = 57; + kOffice = 58; + kOutdoorKitchen = 59; + kOutside = 60; + kPantry = 61; + kParkingLot = 62; + kParlor = 63; + kPatio = 64; + kPlayRoom = 65; + kPoolRoom = 66; + kPorch = 67; + kPrimaryBathroom = 68; + kPrimaryBedroom = 69; + kRamp = 70; + kReceptionRoom = 71; + kRecreationRoom = 72; + kRestroom = 73; + kRoof = 74; + kSauna = 75; + kScullery = 76; + kSewingRoom = 77; + kShed = 78; + kSideDoor = 79; + kSideYard = 80; + kSittingRoom = 81; + kSnug = 82; + kSpa = 83; + kStaircase = 84; + kSteamRoom = 85; + kStorageRoom = 86; + kStudio = 87; + kStudy = 88; + kSunRoom = 89; + kSwimmingPool = 90; + kTerrace = 91; + kUtilityRoom = 92; + kWard = 93; + kWorkshop = 94; + } + + enum FloorSurfaceTag : enum8 { + kCarpet = 0; + kCeramic = 1; + kConcrete = 2; + kCork = 3; + kDeepCarpet = 4; + kDirt = 5; + kEngineeredWood = 6; + kGlass = 7; + kGrass = 8; + kHardwood = 9; + kLaminate = 10; + kLinoleum = 11; + kMat = 12; + kMetal = 13; + kPlastic = 14; + kPolishedConcrete = 15; + kRubber = 16; + kRug = 17; + kSand = 18; + kStone = 19; + kTatami = 20; + kTerrazzo = 21; + kTile = 22; + kVinyl = 23; + } + + enum LandmarkTag : enum8 { + kAirConditioner = 0; + kAirPurifier = 1; + kBackDoor = 2; + kBarStool = 3; + kBathMat = 4; + kBathtub = 5; + kBed = 6; + kBookshelf = 7; + kChair = 8; + kChristmasTree = 9; + kCoatRack = 10; + kCoffeeTable = 11; + kCookingRange = 12; + kCouch = 13; + kCountertop = 14; + kCradle = 15; + kCrib = 16; + kDesk = 17; + kDiningTable = 18; + kDishwasher = 19; + kDoor = 20; + kDresser = 21; + kLaundryDryer = 22; + kFan = 23; + kFireplace = 24; + kFreezer = 25; + kFrontDoor = 26; + kHighChair = 27; + kKitchenIsland = 28; + kLamp = 29; + kLitterBox = 30; + kMirror = 31; + kNightstand = 32; + kOven = 33; + kPetBed = 34; + kPetBowl = 35; + kPetCrate = 36; + kRefrigerator = 37; + kScratchingPost = 38; + kShoeRack = 39; + kShower = 40; + kSideDoor = 41; + kSink = 42; + kSofa = 43; + kStove = 44; + kTable = 45; + kToilet = 46; + kTrashCan = 47; + kLaundryWasher = 48; + kWindow = 49; + kWineCooler = 50; + } + + enum OperationalStatusEnum : enum8 { + kPending = 0; + kOperating = 1; + kSkipped = 2; + kCompleted = 3; + } + + enum PositionTag : enum8 { + kLeft = 0; + kRight = 1; + kTop = 2; + kBottom = 3; + kMiddle = 4; + kRow = 5; + kColumn = 6; + kUnder = 7; + kNextTo = 8; + kAround = 9; + kOn = 10; + kAbove = 11; + kFrontOf = 12; + kBehind = 13; + } + + enum SelectLocationsStatus : enum8 { + kSuccess = 0; + kUnsupportedLocation = 1; + kDuplicatedLocations = 2; + kInvalidInMode = 3; + kInvalidSet = 4; + } + + enum SkipCurrentLocationStatus : enum8 { + kSuccess = 0; + kInvalidLocationList = 1; + kInvalidInMode = 2; + } + + bitmap Feature : bitmap32 { + kListOrder = 0x1; + kSelectWhileRunning = 0x2; + } + + struct LocationDescriptorStruct { + char_string<128> locationName = 0; + nullable int16s floorNumber = 1; + nullable AreaTypeTag areaType = 2; + } + + struct LocationInfoStruct { + nullable LocationDescriptorStruct locationInfo = 0; + nullable LandmarkTag landmarkTag = 1; + nullable PositionTag positionTag = 2; + nullable FloorSurfaceTag surfaceTag = 3; + } + + struct LocationStruct { + int32u locationID = 0; + nullable int8u mapID = 1; + LocationInfoStruct locationInfo = 2; + } + + struct MapStruct { + int8u mapID = 0; + char_string<64> name = 1; + } + + struct ProgressStruct { + int32u locationID = 0; + OperationalStatusEnum status = 1; + optional nullable elapsed_s totalOperationalTime = 2; + optional nullable elapsed_s estimatedTime = 3; + } + + readonly attribute LocationStruct supportedLocations[] = 0; + readonly attribute nullable MapStruct supportedMaps[] = 1; + readonly attribute nullable int32u selectedLocations[] = 2; + readonly attribute optional nullable int32u currentLocation = 3; + readonly attribute optional nullable epoch_s estimatedEndTime = 4; + readonly attribute optional nullable ProgressStruct progress[] = 5; + readonly attribute command_id generatedCommandList[] = 65528; + readonly attribute command_id acceptedCommandList[] = 65529; + readonly attribute event_id eventList[] = 65530; + readonly attribute attrib_id attributeList[] = 65531; + readonly attribute bitmap32 featureMap = 65532; + readonly attribute int16u clusterRevision = 65533; + + request struct SelectLocationsRequest { + nullable int32u newLocations[] = 0; + } + + response struct SelectLocationsResponse = 1 { + SelectLocationsStatus status = 0; + optional char_string<256> statusText = 1; + } + + response struct SkipCurrentLocationResponse = 3 { + SkipCurrentLocationStatus status = 0; + optional char_string<256> statusText = 1; + } + + /** Command used to select a set of device locations, where the device is to operate */ + command SelectLocations(SelectLocationsRequest): SelectLocationsResponse = 0; + /** This command is used to skip the current location where the device operates. */ + command SkipCurrentLocation(): SkipCurrentLocationResponse = 2; +} + endpoint 0 { device type ma_rootdevice = 22, version 1; @@ -1463,6 +1753,26 @@ endpoint 1 { handle command OperationalCommandResponse; handle command GoHome; } + + server cluster ServiceArea { + callback attribute supportedLocations; + callback attribute supportedMaps; + callback attribute selectedLocations; + callback attribute currentLocation; + callback attribute estimatedEndTime; + callback attribute progress; + callback attribute generatedCommandList; + callback attribute acceptedCommandList; + callback attribute eventList; + callback attribute attributeList; + callback attribute featureMap; + ram attribute clusterRevision default = 1; + + handle command SelectLocations; + handle command SelectLocationsResponse; + handle command SkipCurrentLocation; + handle command SkipCurrentLocationResponse; + } } diff --git a/examples/rvc-app/rvc-common/rvc-app.zap b/examples/rvc-app/rvc-common/rvc-app.zap index 7afc8b7d3b819b..cb3ea7cc0d24c4 100644 --- a/examples/rvc-app/rvc-common/rvc-app.zap +++ b/examples/rvc-app/rvc-common/rvc-app.zap @@ -2877,6 +2877,242 @@ "included": 1 } ] + }, + { + "name": "Service Area", + "code": 336, + "mfgCode": null, + "define": "SERVICE_AREA_CLUSTER", + "side": "server", + "enabled": 1, + "commands": [ + { + "name": "SelectLocations", + "code": 0, + "mfgCode": null, + "source": "client", + "isIncoming": 1, + "isEnabled": 1 + }, + { + "name": "SelectLocationsResponse", + "code": 1, + "mfgCode": null, + "source": "server", + "isIncoming": 0, + "isEnabled": 1 + }, + { + "name": "SkipCurrent", + "code": 2, + "mfgCode": null, + "source": "client", + "isIncoming": 1, + "isEnabled": 1 + }, + { + "name": "SkipCurrentResponse", + "code": 3, + "mfgCode": null, + "source": "server", + "isIncoming": 0, + "isEnabled": 1 + } + ], + "attributes": [ + { + "name": "SupportedLocations", + "code": 0, + "mfgCode": null, + "side": "server", + "type": "array", + "included": 1, + "storageOption": "External", + "singleton": 0, + "bounded": 0, + "defaultValue": null, + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, + { + "name": "SupportedMaps", + "code": 1, + "mfgCode": null, + "side": "server", + "type": "array", + "included": 1, + "storageOption": "External", + "singleton": 0, + "bounded": 0, + "defaultValue": null, + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, + { + "name": "SelectedLocations", + "code": 2, + "mfgCode": null, + "side": "server", + "type": "array", + "included": 1, + "storageOption": "External", + "singleton": 0, + "bounded": 0, + "defaultValue": null, + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, + { + "name": "CurrentLocation", + "code": 3, + "mfgCode": null, + "side": "server", + "type": "int32u", + "included": 1, + "storageOption": "External", + "singleton": 0, + "bounded": 0, + "defaultValue": "", + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, + { + "name": "EstimatedEndTime", + "code": 4, + "mfgCode": null, + "side": "server", + "type": "epoch_s", + "included": 1, + "storageOption": "External", + "singleton": 0, + "bounded": 0, + "defaultValue": "", + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, + { + "name": "Progress", + "code": 5, + "mfgCode": null, + "side": "server", + "type": "array", + "included": 1, + "storageOption": "External", + "singleton": 0, + "bounded": 0, + "defaultValue": null, + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, + { + "name": "GeneratedCommandList", + "code": 65528, + "mfgCode": null, + "side": "server", + "type": "array", + "included": 1, + "storageOption": "External", + "singleton": 0, + "bounded": 0, + "defaultValue": null, + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, + { + "name": "AcceptedCommandList", + "code": 65529, + "mfgCode": null, + "side": "server", + "type": "array", + "included": 1, + "storageOption": "External", + "singleton": 0, + "bounded": 0, + "defaultValue": null, + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, + { + "name": "EventList", + "code": 65530, + "mfgCode": null, + "side": "server", + "type": "array", + "included": 1, + "storageOption": "External", + "singleton": 0, + "bounded": 0, + "defaultValue": null, + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, + { + "name": "AttributeList", + "code": 65531, + "mfgCode": null, + "side": "server", + "type": "array", + "included": 1, + "storageOption": "External", + "singleton": 0, + "bounded": 0, + "defaultValue": null, + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, + { + "name": "FeatureMap", + "code": 65532, + "mfgCode": null, + "side": "server", + "type": "bitmap32", + "included": 1, + "storageOption": "External", + "singleton": 0, + "bounded": 0, + "defaultValue": "0", + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + }, + { + "name": "ClusterRevision", + "code": 65533, + "mfgCode": null, + "side": "server", + "type": "int16u", + "included": 1, + "storageOption": "RAM", + "singleton": 0, + "bounded": 0, + "defaultValue": "1", + "reportable": 1, + "minInterval": 1, + "maxInterval": 65534, + "reportableChange": 0 + } + ] } ] } diff --git a/examples/rvc-app/rvc-common/src/rvc-device.cpp b/examples/rvc-app/rvc-common/src/rvc-device.cpp index 63da53c0e0ee53..e018e0929301c7 100644 --- a/examples/rvc-app/rvc-common/src/rvc-device.cpp +++ b/examples/rvc-app/rvc-common/src/rvc-device.cpp @@ -6,6 +6,7 @@ using namespace chip::app::Clusters; void RvcDevice::Init() { + mServiceAreaInstance.Init(); mRunModeInstance.Init(); mCleanModeInstance.Init(); mOperationalStateInstance.Init(); diff --git a/examples/rvc-app/rvc-common/src/rvc-service-area-delegate.cpp b/examples/rvc-app/rvc-common/src/rvc-service-area-delegate.cpp new file mode 100644 index 00000000000000..4c66b35b43fe60 --- /dev/null +++ b/examples/rvc-app/rvc-common/src/rvc-service-area-delegate.cpp @@ -0,0 +1,412 @@ +/* + * + * Copyright (c) 2024 Project CHIP Authors + * All rights reserved. + * + * 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 +#include + +using namespace chip; +using namespace chip::app::Clusters; +using namespace chip::app::Clusters::ServiceArea; + +CHIP_ERROR RvcServiceAreaDelegate::Init() +{ + // hardcoded fill of SUPPORTED MAPS for prototyping + uint8_t supportedMapId_XX = 3; + uint8_t supportedMapId_YY = 245; + + GetInstance()->AddSupportedMap(supportedMapId_XX, "My Map XX"_span); + GetInstance()->AddSupportedMap(supportedMapId_YY, "My Map YY"_span); + + // hardcoded fill of SUPPORTED LOCATIONS for prototyping + uint32_t supportedLocationId_A = 7; + uint32_t supportedLocationId_B = 1234567; + uint32_t supportedLocationId_C = 10050; + uint32_t supportedLocationId_D = 0x88888888; + + // Location A has name, floor number, uses map XX + GetInstance()->AddSupportedLocation( + supportedLocationId_A, DataModel::Nullable(supportedMapId_XX), "My Location A"_span, + DataModel::Nullable(4), DataModel::Nullable(), + DataModel::Nullable(), DataModel::Nullable(), + DataModel::Nullable()); + + // Location B has name, uses map XX + GetInstance()->AddSupportedLocation( + supportedLocationId_B, DataModel::Nullable(supportedMapId_XX), "My Location B"_span, + DataModel::Nullable(), DataModel::Nullable(), + DataModel::Nullable(), DataModel::Nullable(), + DataModel::Nullable()); + + // Location C has full SemData, no name, Map YY + GetInstance()->AddSupportedLocation(supportedLocationId_C, DataModel::Nullable(supportedMapId_YY), CharSpan(), + DataModel::Nullable(-1), + DataModel::Nullable(ServiceArea::AreaTypeTag::kPlayRoom), + DataModel::Nullable(ServiceArea::LandmarkTag::kBackDoor), + DataModel::Nullable(ServiceArea::PositionTag::kNextTo), + DataModel::Nullable(ServiceArea::FloorSurfaceTag::kConcrete)); + + // Location D has null values for all HomeLocationStruct fields, Map YY + GetInstance()->AddSupportedLocation(supportedLocationId_D, DataModel::Nullable(supportedMapId_YY), + "My Location D"_span, DataModel::Nullable(), + DataModel::Nullable(), + DataModel::Nullable(ServiceArea::LandmarkTag::kCouch), + DataModel::Nullable(ServiceArea::PositionTag::kNextTo), + DataModel::Nullable(ServiceArea::FloorSurfaceTag::kHardwood)); + + GetInstance()->SetCurrentLocation(supportedLocationId_C); + + return CHIP_NO_ERROR; +} + +//************************************************************************* +// command support + +bool RvcServiceAreaDelegate::IsSetSelectedLocationsAllowed(MutableCharSpan statusText) +{ + // TODO IMPLEMENT + return true; +}; + +bool RvcServiceAreaDelegate::IsValidSelectLocationsSet(const Commands::SelectLocations::DecodableType & req, + SelectLocationsStatus & locationStatus, MutableCharSpan statusText) +{ + // TODO IMPLEMENT + return true; +}; + +bool RvcServiceAreaDelegate::HandleSkipCurrentLocation(MutableCharSpan skipStatusText) +{ + // TODO IMPLEMENT + return true; +}; + +//************************************************************************* +// Supported Locations accessors + +bool RvcServiceAreaDelegate::IsSupportedLocationsChangeAllowed() +{ + // TODO IMPLEMENT + return true; +} + +uint32_t RvcServiceAreaDelegate::GetNumberOfSupportedLocations() +{ + return static_cast(mSupportedLocations.size()); +} + +bool RvcServiceAreaDelegate::GetSupportedLocationByIndex(uint32_t listIndex, LocationStructureWrapper & aSupportedLocation) +{ + if (listIndex < mSupportedLocations.size()) + { + aSupportedLocation = mSupportedLocations[listIndex]; + return true; + } + + return false; +}; + +bool RvcServiceAreaDelegate::GetSupportedLocationById(uint32_t aLocationId, uint32_t & listIndex, + LocationStructureWrapper & aSupportedLocation) +{ + // We do not need to reimplement this method as it's already done by the SDK. + // We are reimplementing this method, still using linear search, but with some optimization on the SDK implementation + // since we have direct access to the list. + listIndex = 0; + + while (listIndex < mSupportedLocations.size()) + { + if (mSupportedLocations[listIndex].locationID == aLocationId) + { + aSupportedLocation = mSupportedLocations[listIndex]; + return true; + } + + ++listIndex; + } + + return false; +}; + +bool RvcServiceAreaDelegate::AddSupportedLocation(const LocationStructureWrapper & newLocation, uint32_t & listIndex) +{ + // The server instance (caller) is responsible for ensuring that there are no duplicate location IDs, list size not exceeded, + // etc. + + // Double-check list size to ensure there no memory issues. + if (mSupportedLocations.size() < kMaxNumSupportedLocations) + { + // not sorting list, number of locations normally expected to be small, max 255 + mSupportedLocations.push_back(newLocation); + listIndex = static_cast(mSupportedMaps.size()) - 1; // new element is last in list + return true; + } + + ChipLogError(Zcl, "AddSupportedLocation %u - supported locations list is already at maximum size %u", newLocation.locationID, + static_cast(kMaxNumSupportedLocations)); + + return false; +} + +bool RvcServiceAreaDelegate::ModifySupportedLocation(uint32_t listIndex, const LocationStructureWrapper & modifiedLocation) +{ + // The server instance (caller) is responsible for ensuring that there are no duplicate location IDs, list size not exceeded, + // etc. + + // Double-check that locationID's match. + if (modifiedLocation.locationID != mSupportedLocations[listIndex].locationID) + { + ChipLogError(Zcl, "ModifySupportedLocation - locationID's do not match, new locationID %u, existing locationID %u", + modifiedLocation.locationID, mSupportedLocations[listIndex].locationID); + return false; + } + + // checks passed, update the attribute + mSupportedLocations[listIndex] = modifiedLocation; + return true; +} + +bool RvcServiceAreaDelegate::ClearSupportedLocations() +{ + if (!mSupportedLocations.empty()) + { + mSupportedLocations.clear(); + return true; + } + + return false; +} + +//************************************************************************* +// Supported Maps accessors + +bool RvcServiceAreaDelegate::IsSupportedMapChangeAllowed() +{ + // TODO IMPLEMENT + return true; +} + +uint32_t RvcServiceAreaDelegate::GetNumberOfSupportedMaps() +{ + return static_cast(mSupportedMaps.size()); +} + +bool RvcServiceAreaDelegate::GetSupportedMapByIndex(uint32_t listIndex, MapStructureWrapper & aSupportedMap) +{ + if (listIndex < mSupportedMaps.size()) + { + aSupportedMap = mSupportedMaps[listIndex]; + return true; + } + + return false; +}; + +bool RvcServiceAreaDelegate::GetSupportedMapById(uint8_t aMapId, uint32_t & listIndex, MapStructureWrapper & aSupportedMap) +{ + // We do not need to reimplement this method as it's already done by the SDK. + // We are reimplementing this method, still using linear search, but with some optimization on the SDK implementation + // since we have direct access to the list. + listIndex = 0; + + while (listIndex < mSupportedMaps.size()) + { + if (mSupportedMaps[listIndex].mapID == aMapId) + { + aSupportedMap = mSupportedMaps[listIndex]; + return true; + } + + ++listIndex; + } + + return false; +}; + +bool RvcServiceAreaDelegate::AddSupportedMap(const MapStructureWrapper & newMap, uint32_t & listIndex) +{ + // The server instance (caller) is responsible for ensuring that there are no duplicate location IDs, list size not exceeded, + // etc. + + // Double-check list size to ensure there no memory issues. + if (mSupportedMaps.size() < kMaxNumSupportedMaps) + { + // not sorting list, number of locations normally expected to be small, max 255 + mSupportedMaps.push_back(newMap); + listIndex = static_cast(mSupportedMaps.size()) - 1; // new element is last in list + return true; + } + ChipLogError(Zcl, "AddSupportedMap %u - supported maps list is already at maximum size %u", newMap.mapID, + static_cast(kMaxNumSupportedMaps)); + + return false; +} + +bool RvcServiceAreaDelegate::ModifySupportedMap(uint32_t listIndex, const MapStructureWrapper & modifiedMap) +{ + // The server instance (caller) is responsible for ensuring that there are no duplicate location IDs, list size not exceeded, + // etc. + + // Double-check that mapID's match. + if (modifiedMap.mapID != mSupportedMaps[listIndex].mapID) + { + ChipLogError(Zcl, "ModifySupportedMap - mapID's do not match, new mapID %u, existing mapID %u", modifiedMap.mapID, + mSupportedMaps[listIndex].mapID); + return false; + } + + // save modified map + mSupportedMaps[listIndex] = modifiedMap; + return true; +} + +bool RvcServiceAreaDelegate::ClearSupportedMaps() +{ + if (!mSupportedMaps.empty()) + { + mSupportedMaps.clear(); + return true; + } + + return false; +} + +//************************************************************************* +// Selected Locations accessors + +uint32_t RvcServiceAreaDelegate::GetNumberOfSelectedLocations() +{ + return static_cast(mSelectedLocations.size()); +} + +bool RvcServiceAreaDelegate::GetSelectedLocationByIndex(uint32_t listIndex, uint32_t & aSelectedLocation) +{ + if (listIndex < mSelectedLocations.size()) + { + aSelectedLocation = mSelectedLocations[listIndex]; + return true; + } + + return false; +}; + +bool RvcServiceAreaDelegate::AddSelectedLocation(uint32_t aLocationId, uint32_t & listIndex) +{ + // The server instance (caller) is responsible for ensuring that there are no duplicate location IDs, list size not exceeded, + // etc. + + // Double-check list size to ensure there no memory issues. + if (mSelectedLocations.size() < kMaxNumSelectedLocations) + { + // not sorting list, number of locations normally expected to be small, max 255 + mSelectedLocations.push_back(aLocationId); + listIndex = static_cast(mSelectedLocations.size()) - 1; // new element is last in list + return true; + } + ChipLogError(Zcl, "AddSelectedLocation %u - selected locations list is already at maximum size %u", aLocationId, + static_cast(kMaxNumSelectedLocations)); + + return false; +} + +bool RvcServiceAreaDelegate::ClearSelectedLocations() +{ + if (!mSelectedLocations.empty()) + { + mSelectedLocations.clear(); + return true; + } + + return false; +} + +//************************************************************************* +// Progress List accessors + +uint32_t RvcServiceAreaDelegate::GetNumberOfProgressElements() +{ + return static_cast(mProgressList.size()); +} + +bool RvcServiceAreaDelegate::GetProgressElementByIndex(uint32_t listIndex, Structs::ProgressStruct::Type & aProgressElement) +{ + if (listIndex < mProgressList.size()) + { + aProgressElement = mProgressList[listIndex]; + return true; + } + + return false; +}; + +bool RvcServiceAreaDelegate::GetProgressElementById(uint32_t aLocationId, uint32_t & listIndex, + Structs::ProgressStruct::Type & aProgressElement) +{ + // We do not need to reimplement this method as it's already done by the SDK. + // We are reimplementing this method, still using linear search, but with some optimization on the SDK implementation + // since we have direct access to the list. + listIndex = 0; + + while (listIndex < mProgressList.size()) + { + if (mProgressList[listIndex].locationID == aLocationId) + { + aProgressElement = mProgressList[listIndex]; + return true; + } + + ++listIndex; + } + + return false; +}; + +bool RvcServiceAreaDelegate::AddProgressElement(const Structs::ProgressStruct::Type & newProgressElement, uint32_t & listIndex) +{ + // The server instance (caller) is responsible for ensuring that there are no duplicate location IDs, list size not exceeded, + // etc. + + // Double-check list size to ensure there no memory issues. + if (mProgressList.size() < kMaxNumProgressElements) + { + // not sorting list, number of locations normally expected to be small, max 255 + mProgressList.push_back(newProgressElement); + listIndex = static_cast(mProgressList.size()) - 1; // new element is last in list + return true; + } + ChipLogError(Zcl, "AddProgressElement %u -progress list is already at maximum size %u", newProgressElement.locationID, + static_cast(kMaxNumProgressElements)); + + return false; +} + +bool RvcServiceAreaDelegate::ModifyProgressElement(uint32_t listIndex, + const Structs::ProgressStruct::Type & modifiedProgressElement) +{ + // TODO IMPLEMENT + return false; +} + +bool RvcServiceAreaDelegate::ClearProgress() +{ + if (!mProgressList.empty()) + { + mProgressList.clear(); + return true; + } + + return false; +} diff --git a/src/app/chip_data_model.gni b/src/app/chip_data_model.gni index ae7bb84948b3be..ce65fe42331acb 100644 --- a/src/app/chip_data_model.gni +++ b/src/app/chip_data_model.gni @@ -398,6 +398,14 @@ template("chip_data_model") { "${_app_root}/clusters/${cluster}/thread-network-diagnostics-provider.cpp", "${_app_root}/clusters/${cluster}/thread-network-diagnostics-provider.h", ] + } else if (cluster == "service-area-server") { + sources += [ + "${_app_root}/clusters/${cluster}/${cluster}.cpp", + "${_app_root}/clusters/${cluster}/${cluster}.h", + "${_app_root}/clusters/${cluster}/service-area-cluster-objects.h", + "${_app_root}/clusters/${cluster}/service-area-delegate.cpp", + "${_app_root}/clusters/${cluster}/service-area-delegate.h", + ] } else if (cluster == "thread-border-router-management-server") { sources += [ "${_app_root}/clusters/${cluster}/thread-border-router-management-server.cpp", diff --git a/src/app/clusters/basic-information/basic-information.cpp b/src/app/clusters/basic-information/basic-information.cpp index b550026b5f1cd7..bd45a1035164b5 100644 --- a/src/app/clusters/basic-information/basic-information.cpp +++ b/src/app/clusters/basic-information/basic-information.cpp @@ -342,7 +342,7 @@ CHIP_ERROR BasicAttrAccess::Write(const ConcreteDataAttributePath & aPath, Attri switch (aPath.mAttributeId) { - case Location::Id: { + case Attributes::Location::Id: { CHIP_ERROR err = WriteLocation(aDecoder); return err; diff --git a/src/app/clusters/service-area-server/service-area-cluster-objects.h b/src/app/clusters/service-area-server/service-area-cluster-objects.h new file mode 100644 index 00000000000000..e46704796f74c3 --- /dev/null +++ b/src/app/clusters/service-area-server/service-area-cluster-objects.h @@ -0,0 +1,358 @@ +/* + * + * Copyright (c) 2024 Project CHIP Authors + * All rights reserved. + * + * 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. + */ + +#pragma once + +#include +#include + +namespace chip { +namespace app { +namespace Clusters { +namespace ServiceArea { + +// These limits are defined in the spec. +inline constexpr size_t kLocationNameMaxSize = 128u; +inline constexpr size_t kMapNameMaxSize = 64u; + +/** + * This class is used to wrap the LocationStruct object and provide a more user-friendly interface for the data. + * It provides a way to store the location name in a buffer, and provides a way to compare the location name with a given string. + */ +struct LocationStructureWrapper : public chip::app::Clusters::ServiceArea::Structs::LocationStruct::Type +{ + LocationStructureWrapper() + { + Set(0, 0, CharSpan(), DataModel::Nullable(), DataModel::Nullable(), + DataModel::Nullable(), DataModel::Nullable(), DataModel::Nullable()); + } + + /** + * @brief This is a full constructor that initializes the location object with the given values. All values are deep copied. + * @param[in] aLocationId The unique identifier of this location. + * @param[in] aMapId The identifier of the supported map associated with this location. + * @param[in] aLocationName A human readable name for this location (empty string if not used). + * @param[in] aFloorNumber The floor level of this location - use negative values for below ground. + * @param[in] aAreaTypeTag A common namespace Area tag - indicates an association of the location with an indoor or outdoor area + * of a home. + * @param[in] aLandmarkTag A common namespace Landmark tag - indicates an association of the location with a home landmark. + * @param[in] aPositionTag A common namespace Position tag - indicates the position of the location with respect to the + * landmark. + * @param[in] aSurfaceTag A common namespace Floor Surface tag - indicates an association of the location with a surface type. + * + * @note Requirements regarding what combinations of fields and values are valid are not checked by this class. + * @note If aLocationName is larger than kLocationNameMaxSize, it will be truncated. + * @note If aLocationName is an empty string and aFloorNumber and aAreaTypeTag are null, locationInfo will be set to null. + */ + LocationStructureWrapper(uint32_t aLocationId, const DataModel::Nullable & aMapId, const CharSpan & aLocationName, + const DataModel::Nullable & aFloorNumber, + const DataModel::Nullable & aAreaTypeTag, + const DataModel::Nullable & aLandmarkTag, + const DataModel::Nullable & aPositionTag, + const DataModel::Nullable & aSurfaceTag) + { + Set(aLocationId, aMapId, aLocationName, aFloorNumber, aAreaTypeTag, aLandmarkTag, aPositionTag, aSurfaceTag); + } + + /** + * @brief This is a copy constructor that initializes the location object with the values from another location object. All + * values are deep copied. + * @param[in] aOther The location object to copy. + * + * @note If the locationName is empty string and aFloorNumber and aAreaTypeTag are null, locationInfo will be set to null. + */ + LocationStructureWrapper(const LocationStructureWrapper & aOther) { *this = aOther; } + + /** + * @brief This is an assignment operator that initializes the location object with the values from another location object. All + * values are deep copied. + * @param[in] aOther The location object to copy. + * + * @note If the locationName is empty string and aFloorNumber and aAreaTypeTag are null, locationInfo will be set to null. + */ + LocationStructureWrapper & operator=(const LocationStructureWrapper & aOther) + { + if (aOther.locationInfo.locationInfo.IsNull()) + { + Set(aOther.locationID, aOther.mapID, CharSpan(), NullOptional, NullOptional, aOther.locationInfo.landmarkTag, + aOther.locationInfo.positionTag, aOther.locationInfo.surfaceTag); + } + else + { + Set(aOther.locationID, aOther.mapID, aOther.locationInfo.locationInfo.Value().locationName, + aOther.locationInfo.locationInfo.Value().floorNumber, aOther.locationInfo.locationInfo.Value().areaType, + aOther.locationInfo.landmarkTag, aOther.locationInfo.positionTag, aOther.locationInfo.surfaceTag); + } + + return *this; + } + + /** + * @brief Set all fields of the location object. All values are deep copied. + * @param[in] aLocationId The unique identifier of this location. + * @param[in] aMapId The identifier of the supported map associated with this location. + * @param[in] aLocationName A human readable name for this location (empty string if not used). + * @param[in] aFloorNumber The floor level of this location - use negative values for below ground. + * @param[in] aAreaTypeTag A common namespace Area tag - indicates an association of the location with an indoor or outdoor area + * of a home. + * @param[in] aLandmarkTag A common namespace Landmark tag - indicates an association of the location with a home landmark. + * @param[in] aPositionTag A common namespace Position tag - indicates the position of the location with respect to the + * landmark. + * @param[in] aSurfaceTag A common namespace Floor Surface tag - indicates an association of the location with a surface type. + * + * @note Requirements regarding what combinations of fields and values are valid are not checked by this class. + * @note If aLocationName is larger than kLocationNameMaxSize, it will be truncated. + * @note If aLocationName is an empty string and aFloorNumber and aAreaTypeTag are null, locationInfo will be set to null. + */ + void Set(uint32_t aLocationId, const DataModel::Nullable & aMapId, const CharSpan & aLocationName, + const DataModel::Nullable & aFloorNumber, const DataModel::Nullable & aAreaType, + const DataModel::Nullable & aLandmarkTag, const DataModel::Nullable & aPositionTag, + const DataModel::Nullable & aSurfaceTag) + { + locationID = aLocationId; + mapID = aMapId; + + // If there is at least one non-null value for locationInfo, add it to the location structure. + if ((!aLocationName.empty()) || (!aFloorNumber.IsNull()) || (!aAreaType.IsNull())) + { + // Create a home location info structure and fill it in except for the location name. This is done below. + locationInfo.locationInfo.SetNonNull(Structs::LocationDescriptorStruct::Type()); + + locationInfo.locationInfo.Value().floorNumber = aFloorNumber; + locationInfo.locationInfo.Value().areaType = aAreaType; + } + else + { + locationInfo.locationInfo.SetNull(); + } + + locationInfo.landmarkTag = aLandmarkTag; + locationInfo.positionTag = aPositionTag; + locationInfo.surfaceTag = aSurfaceTag; + + // this assumes locationInfo structure was created above, if appropriate + if (!locationInfo.locationInfo.IsNull()) + { + if (aLocationName.empty()) + { + locationInfo.locationInfo.Value().locationName = CharSpan(mLocationNameBuffer, 0); + } + else if (aLocationName.size() > sizeof(mLocationNameBuffer)) + { + // Save the truncated name that fits into available size. + memcpy(mLocationNameBuffer, aLocationName.data(), sizeof(mLocationNameBuffer)); + locationInfo.locationInfo.Value().locationName = CharSpan(mLocationNameBuffer, sizeof(mLocationNameBuffer)); + } + else + { + // Save full name. + memcpy(mLocationNameBuffer, aLocationName.data(), aLocationName.size()); + locationInfo.locationInfo.Value().locationName = CharSpan(mLocationNameBuffer, aLocationName.size()); + } + } + } + + /** + * @brief Compare the location's name with the given text. + * @param[in] aLocationName The name to compare. + * @return true if the location structure's name field matches aLocationName. + * False otherwise, including if the location structure's HomeLocation structure is null. + */ + bool IsNameEqual(const CharSpan & aLocationName) const + { + if (!locationInfo.locationInfo.IsNull()) + { + return locationInfo.locationInfo.Value().locationName.data_equal(aLocationName); + } + + return false; + } + + /** + * This is used for configuring the IsEqual method. + * If kIgnoreLocationId is set, the location IDs are ignored when checking for equality. + * If kIgnoreMapId is set, the map IDs are ignored when checking for equality. + */ + enum class IsEqualConfig : uint8_t + { + kIgnoreLocationId = 0x1, + kIgnoreMapId = 0x2, + }; + + /** + * @brief Checks if the given LocationStructureWrapper is equal to this one. + * @param aOther The location to compare with. + * @param aConfig Set if the location IDs and/or the map IDs should be ignored when checking for equality. + * @return True if both locations are equal. False otherwise. + */ + bool IsEqual(const LocationStructureWrapper & aOther, BitMask aConfig) const + { + if (!aConfig.Has(IsEqualConfig::kIgnoreLocationId) && (locationID != aOther.locationID)) + { + return false; + } + + if (!aConfig.Has(IsEqualConfig::kIgnoreMapId) && (mapID != aOther.mapID)) + { + return false; + } + + if (locationInfo.locationInfo.IsNull() != aOther.locationInfo.locationInfo.IsNull()) + { + return false; + } + + if (!locationInfo.locationInfo.IsNull()) + { + + if (!IsNameEqual(aOther.locationInfo.locationInfo.Value().locationName)) + { + return false; + } + + if (locationInfo.locationInfo.Value().floorNumber != aOther.locationInfo.locationInfo.Value().floorNumber) + { + return false; + } + + if (locationInfo.locationInfo.Value().areaType != aOther.locationInfo.locationInfo.Value().areaType) + { + return false; + } + } + + if (locationInfo.landmarkTag != aOther.locationInfo.landmarkTag) + { + return false; + } + + if (locationInfo.positionTag != aOther.locationInfo.positionTag) + { + return false; + } + + if (locationInfo.surfaceTag != aOther.locationInfo.surfaceTag) + { + return false; + } + + return true; + } + + /** + * @return The location name. + */ + CharSpan GetName() + { + if (locationInfo.locationInfo.IsNull()) + { + return { mLocationNameBuffer, 0 }; + } + + return locationInfo.locationInfo.Value().locationName; + } + +private: + char mLocationNameBuffer[kLocationNameMaxSize] = { 0 }; +}; + +/** + * This class wraps the MapStruct object and provides a more user-friendly interface for the data. + */ +struct MapStructureWrapper : public chip::app::Clusters::ServiceArea::Structs::MapStruct::Type +{ + MapStructureWrapper() { Set(0, CharSpan()); } + + /** + * @brief This is a full constructor that initializes the map object with the given values. All values are deep copied. + * @param[in] aMapId The identifier of this map. + * @param[in] aMapName A human readable name (should not be empty string). + * + * @note Requirements regarding what combinations of fields and values are 'valid' are not checked by this class. + * @note If aMapName is larger than kMapNameMaxSize, it will be truncated. + */ + MapStructureWrapper(uint8_t aMapId, const CharSpan & aMapName) { Set(aMapId, aMapName); } + + /** + * @brief This is a copy constructor that initializes the map object with the values from another map object. All values are + * deep copied. + * @param[in] aOther The map object to copy. + */ + MapStructureWrapper(const MapStructureWrapper & aOther) { *this = aOther; } + + /** + * @brief This is an assignment operator that initializes the map object with the values from another map object. All values are + * deep copied. + * @param[in] aOther The map object to copy. + */ + MapStructureWrapper & operator=(const MapStructureWrapper & aOther) + { + Set(aOther.mapID, aOther.name); + return *this; + } + + /** + * @brief Set all fields of the map object. All values are deep copied. + * @param[in] aMapId The identifier of this map. + * @param[in] aMapName A human readable name (should not be empty string). + * + * @note Requirements regarding what combinations of fields and values are 'valid' are not checked by this class. + * @note if aMapName is larger than kMapNameMaxSize, it will be truncated. + */ + void Set(uint8_t aMapId, const CharSpan & aMapName) + { + mapID = aMapId; + + if (aMapName.empty()) + { + name = CharSpan(mMapNameBuffer, 0); + } + else if (aMapName.size() > sizeof(mMapNameBuffer)) + { + // Save the truncated name that fits into available size. + memcpy(mMapNameBuffer, aMapName.data(), sizeof(mMapNameBuffer)); + name = CharSpan(mMapNameBuffer, sizeof(mMapNameBuffer)); + } + else + { + // Save full name. + memcpy(mMapNameBuffer, aMapName.data(), aMapName.size()); + name = CharSpan(mMapNameBuffer, aMapName.size()); + } + } + + /** + * @brief Compare the map's name with given text. + * @param[in] aMapName The name to compare. + * @return true if the map structure's name field matches aMapName. + */ + bool IsNameEqual(const CharSpan & aMapName) const { return name.data_equal(aMapName); } + + /** + * @return The map name. + */ + CharSpan GetName() const { return name; } + +private: + char mMapNameBuffer[kMapNameMaxSize] = { 0 }; +}; + +} // namespace ServiceArea +} // namespace Clusters +} // namespace app +} // namespace chip diff --git a/src/app/clusters/service-area-server/service-area-delegate.cpp b/src/app/clusters/service-area-server/service-area-delegate.cpp new file mode 100644 index 00000000000000..1be7d257336c94 --- /dev/null +++ b/src/app/clusters/service-area-server/service-area-delegate.cpp @@ -0,0 +1,90 @@ +#include "service-area-delegate.h" +#include "service-area-server.h" + +using namespace chip::app::Clusters::ServiceArea; + +bool Delegate::GetSupportedLocationById(uint32_t aLocationId, uint32_t & listIndex, LocationStructureWrapper & aSupportedLocation) +{ + listIndex = 0; + + // simple linear iteration to find the location with the desired locationID. + while (GetSupportedLocationByIndex(listIndex, aSupportedLocation)) + { + if (aSupportedLocation.locationID == aLocationId) + { + return true; + } + + ++listIndex; + } + + return false; +} + +void Delegate::HandleSupportedLocationsUpdated() +{ + mInstance->ClearSelectedLocations(); + mInstance->SetCurrentLocation(DataModel::NullNullable); + mInstance->ClearProgress(); +} + +bool Delegate::GetSupportedMapById(uint8_t aMapId, uint32_t & listIndex, MapStructureWrapper & aSupportedMap) +{ + listIndex = 0; + + while (GetSupportedMapByIndex(listIndex, aSupportedMap)) + { + if (aSupportedMap.mapID == aMapId) + { + return true; + } + + ++listIndex; + } + + return false; +} + +bool Delegate::IsSelectedLocation(uint32_t aLocationId) +{ + uint32_t listIndex = 0; + uint32_t selectedLocation; + + while (GetSelectedLocationByIndex(listIndex, selectedLocation)) + { + if (selectedLocation == aLocationId) + { + return true; + } + + ++listIndex; + } + + return false; +} + +bool Delegate::GetProgressElementById(uint32_t aLocationId, uint32_t & listIndex, Structs::ProgressStruct::Type & aProgressElement) +{ + listIndex = 0; + + // simple linear iteration to find the progress element with the desired locationID. + while (GetProgressElementByIndex(listIndex, aProgressElement)) + { + if (aProgressElement.locationID == aLocationId) + { + return true; + } + + ++listIndex; + } + + return false; +} + +bool Delegate::IsProgressElement(uint32_t aLocationId) +{ + uint32_t index; + Structs::ProgressStruct::Type progressElement; + + return GetProgressElementById(aLocationId, index, progressElement); +} diff --git a/src/app/clusters/service-area-server/service-area-delegate.h b/src/app/clusters/service-area-server/service-area-delegate.h new file mode 100644 index 00000000000000..baf50d071b3404 --- /dev/null +++ b/src/app/clusters/service-area-server/service-area-delegate.h @@ -0,0 +1,384 @@ +/* + * + * Copyright (c) 2024 Project CHIP Authors + * All rights reserved. + * + * 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. + */ + +#pragma once + +#include "service-area-cluster-objects.h" + +namespace chip { +namespace app { +namespace Clusters { +namespace ServiceArea { + +class Instance; + +// ***************************************************************************** +// cluster constraints + +constexpr size_t kMaxNumSupportedLocations = 255; +constexpr size_t kMaxNumSupportedMaps = 255; +constexpr size_t kMaxNumSelectedLocations = 255; +constexpr size_t kMaxNumProgressElements = 255; + +constexpr size_t kMaxSizeStatusText = 256; + +/** + * ServiceArea::Delegate Defines methods for implementing application-specific + * logic for the Service Area Cluster. + */ +class Delegate +{ +public: + Delegate() = default; + virtual ~Delegate() = default; + + /** + * Stop this class objects from being copied. + */ + Delegate(const Delegate &) = delete; + Delegate & operator=(const Delegate &) = delete; + + /** + * @brief This method will be called during the ServiceArea server initialization after the Instance information has been + * validated and the Instance has been registered. This can be used to initialise app logic. + */ + virtual CHIP_ERROR Init() { return CHIP_NO_ERROR; }; + + //************************************************************************* + // Command handling support + + /** + * @brief Can the selected locations be set by the client in the current operating mode? + * @param[out] statusText text describing why selected locations cannot be set (if return is false). + * @return true if the current device state allows selected locations to be set by user. + * + * @note The statusText field SHOULD indicate why the request is not allowed, given the current mode + * of the device, which may involve other clusters. + */ + virtual bool IsSetSelectedLocationsAllowed(MutableCharSpan statusText) = 0; + + /** + * Given a set of locations to be set to the SelectedLocations attribute, this method should check that + * the set of locations as a whole is valid and reachable by the device. + * If the set of locations is invalid, the locationStatus should be set to InvalidSet and + * the statusText SHALL include a vendor-defined error description. + * + * The caller of this method will ensure that there are no duplicates is the list + * and that all the locations in the set are valid supported locations. + * + * @param[in] req List of new selected locations. + * @param[out] locationStatus Success if all checks pass, error code if failure. + * @param[out] statusText text describing failure (see description above), size kMaxSizeStatusText. + * @return true if success. + * + * @note If the SelectLocations command is allowed when the device is operating and the selected locations change to none, the + * device must stop. + */ + virtual bool IsValidSelectLocationsSet(const Commands::SelectLocations::DecodableType & req, + SelectLocationsStatus & locationStatus, MutableCharSpan statusText) = 0; + + /** + * @brief The server instance ensures that the SelectedLocations and CurrentLocation attributes are not null before + * calling this method. + * @param[out] skipStatusText text describing why current location cannot be skipped. + * @return true if command is successful, false if the received skip request cannot be handled due to the current mode of the + * device. + * + * @note skipStatusText must be filled out by the function on failure. + * + * @note If the device successfully accepts the request and the ListOrder feature is set to 1: + * The server SHALL stop operating at the current location. + * The server SHALL attempt to operate at the remaining locations on the SelectedLocations attribute list, starting with + * the next entry. If the end of the SelectedLocations attribute list is reached, the server SHALL stop operating. + * + * @note If the device successfully accepts the request and the ListOrder feature is set to 0: + * The server SHALL stop operating at the current location. + * The server SHALL attempt to operate at the locations on the SelectedLocations attribute list where operating has not + * been completed, using a vendor defined order. If the server has completed operating at all locations on the SelectedLocations + * attribute list, the server SHALL stop operating. + * + * @note If the Status field is set to InvalidLocationList, the StatusText field SHALL be an empty string. + * If the Status field is not set to Success, or InvalidLocationList, the StatusText field SHALL include a vendor defined + * error description which can be used to explain the error to the user. For example, if the Status field is set to + * InvalidInMode, the StatusText field SHOULD indicate why the request is not allowed, given the current mode of the device, + * which may involve other clusters. + */ + virtual bool HandleSkipCurrentLocation(MutableCharSpan skipStatusText) + { + // device support of this command is optional + CopyCharSpanToMutableCharSpan("Skip Current Location command not supported by device"_span, skipStatusText); + return false; + } + + //************************************************************************* + // Supported Locations accessors + + /** + * @return true if the current device state allows the SupportedLocations attribute to be updated. + * + * @note The SupportedLocations attribute list changes (adding or deleting entries, + * changing their MapID fields, changing the LocationID fields, or nulling the entire list) + * SHOULD NOT be allowed while the device is operating, to reduce the impact on the clients, + * and the potential confusion for the users. + * + * @note The device implementation MAY allow supported location changes while operating if the device + * repopulates the SupportedMaps, SupportedLocations, CurrentLocation, and Progress attributes with + * data matching the constraints listed in the requirements for each attribute. + */ + virtual bool IsSupportedLocationsChangeAllowed() = 0; + + virtual uint32_t GetNumberOfSupportedLocations() = 0; + + /** + * @brief Get a supported location using the position in the list. + * @param[in] listIndex the position in the list. + * @param[out] aSupportedLocation copy of the location contents, if found. + * @return true if location found, false otherwise. + */ + virtual bool GetSupportedLocationByIndex(uint32_t listIndex, LocationStructureWrapper & aSupportedLocation) = 0; + + /** + * @brief Get a supported location that matches a locationID. + * @param[in] aLocationId the locationID to search for. + * @param[out] listIndex the location's index in the list, if found. + * @param[out] aSupportedLocation copy of the location contents, if found. + * @return true if location found, false otherwise. + * + * @note may be overloaded in device implementation for optimization, if desired. + */ + virtual bool GetSupportedLocationById(uint32_t aLocationId, uint32_t & listIndex, + LocationStructureWrapper & aSupportedLocation); + + /** + * This method is called by the server instance to add a new location to the list. + * The server instance will ensure that the newLocation is a valid, unique location. + * @param [in] newLocation new location to add. + * @param [out] listIndex filled with the list index for the new location, if successful. + * @return true if successful, false otherwise. + + * @note this function SHOULD double check that the added location won't exceed the maximum list size. + */ + virtual bool AddSupportedLocation(const LocationStructureWrapper & newLocation, uint32_t & listIndex) = 0; + + /** + * This method is called by the server instance to modify an existing location in the list. + * The server instance will ensure that the modifiedLocation is a valid, unique location. + * @param[in] listIndex The index of the location being modified. + * @param[in] modifiedLocation A location with the modified contents. + * @return true if successful, false otherwise. + * + * @note this function SHOULD double check that newLocation's locationID matches the object at listIndex. + */ + virtual bool ModifySupportedLocation(uint32_t listIndex, const LocationStructureWrapper & modifiedLocation) = 0; + + /** + * @return true if supported locations was not already null, false otherwise. + */ + virtual bool ClearSupportedLocations() = 0; + + /** + * @brief Ensure that when the Supported locations is modified, the required restrictions for the SelectedLocations, + * CurrentLocation, and Progress attributes are maintained. + * + * This method will be called by the SDK whenever the adherence to the restrictions for these attributes cannot be guaranteed. + * For example, if there are deletions in the SupportedMops or SupportedLocations attributes, or if there are changes to their + * IDs. This method will no be called if the changes made to the SupportedMops or SupportedLocations attributes, ensure that the + * restrictions are adhered. For example, if there are additions or the modifications do not involve changing IDs in the + * SupportedMops or SupportedLocations attributes. + * + * The default implementation will set the SelectedLocations, CurrentLocation, and Progress attributes to null. + * + * The user is free the redefine this method as their device may have more information on what has changed and may be able to + * maintain the restrictions on these attributes by selectively editing them. + */ + virtual void HandleSupportedLocationsUpdated(); + + //************************************************************************* + // Supported Maps accessors + + /** + * @return true if the current device state allows the SupportedMaps attribute to be updated. + * + * @note The SupportedMaps attribute list changes (adding or deleting entries, + * changing their MapID fields, or nulling the entire list) + * SHOULD NOT be allowed while the device is operating, to reduce the impact on the clients, + * and the potential confusion for the users. + * + * @note The device implementation MAY allow supported maps changes while operating if the device + * repopulates the SupportedLocations, CurrentLocation, and Progress attributes with + * data matching the constraints listed in the requirements for each attribute. + */ + virtual bool IsSupportedMapChangeAllowed() = 0; + + virtual uint32_t GetNumberOfSupportedMaps() = 0; + + /** + * @brief Get a supported map using the position in the list. + * @param[in] listIndex the position in the list. + * @param[out] aSupportedMap copy of the map contents, if found. + * @return true if a supported map is found, false otherwise. + */ + virtual bool GetSupportedMapByIndex(uint32_t listIndex, MapStructureWrapper & aSupportedMap) = 0; + + /** + * @brief Get a supported map that matches a mapID. + * @param[in] aMapId the mapID to search for. + * @param[out] listIndex the map's index in the list, if found. + * @param[out] aSupportedMap copy of the location contents, if found. + * @return true if a supported map is found, false otherwise. + * + * @note may be overloaded in device implementation for optimization, if desired. + */ + virtual bool GetSupportedMapById(uint8_t aMapId, uint32_t & listIndex, MapStructureWrapper & aSupportedMap); + + /** + * This method is called by the server instance to add a new map to the list. + * The server instance will ensure that the newMap is a valid, unique map. + * @param[in] newMap The new map to add. + * @param[out] listIndex filled with the list index of the new map, if successful. + * @return true if successful, false otherwise. + * + * @note this function SHOULD double check that the added map won't exceed the maximum list size + */ + virtual bool AddSupportedMap(const MapStructureWrapper & newMap, uint32_t & listIndex) = 0; + + /** + * This method is called by the server instance to modify an existing map in the list. + * The server instance will ensure that the modifiedMap is a valid, unique map. + * @param[in] listIndexThe index of the map being modified. + * @param[in] modifiedMapA map with the modified contents. + * @return true if successful, false otherwise. + * + * @note this function SHOULD double check that modifiedMap's mapID matches the object at listIndex. + */ + virtual bool ModifySupportedMap(uint32_t listIndex, const MapStructureWrapper & modifiedMap) = 0; + + /** + * @return true if supported maps was not already null, false otherwise. + */ + virtual bool ClearSupportedMaps() = 0; + + //************************************************************************* + // Selected Locations accessors + + virtual uint32_t GetNumberOfSelectedLocations() = 0; + + /** + * @brief Get a selected location using the position in the list. + * @param[in] listIndex the position in the list. + * @param[out] selectedLocation the selected location value, if found. + * @return true if a selected location is found, false otherwise. + */ + virtual bool GetSelectedLocationByIndex(uint32_t listIndex, uint32_t & selectedLocation) = 0; + + /** + * @return true if the aLocationId locationID is found in the SelectedLocations list, false otherwise. + * + * @note may be overloaded in device implementation for optimization, if desired. + */ + virtual bool IsSelectedLocation(uint32_t aLocationId); + + /** + * This method is called by the server instance to add a new selected location to the list. + * The server instance will ensure that the aLocationId references a SUPPORTED location, and is unique within selected + * locations. + * @param[in] aLocationId The new locationID to add. + * @param[out] listIndex filled with the list index of the new location, if successful. + * @return true if successful, false otherwise. + * + * @note this function SHOULD double check that the added location won't exceed the maximum list size. + */ + virtual bool AddSelectedLocation(uint32_t aLocationId, uint32_t & listIndex) = 0; + + /** + * @return true if selected locations was not already null, false otherwise. + */ + virtual bool ClearSelectedLocations() = 0; + + //************************************************************************* + // Progress accessors + + virtual uint32_t GetNumberOfProgressElements() = 0; + + /** + * @brief Get a progress element using the position in the list. + * @param[in] listIndex the position in the list. + * @param[out] aProgressElement copy of the progress element contents, if found. + * @return true if a progress element is found, false otherwise. + */ + virtual bool GetProgressElementByIndex(uint32_t listIndex, Structs::ProgressStruct::Type & aProgressElement) = 0; + + /** + * @brief Get a progress element that matches a locationID. + * @param[in] aLocationId the locationID to search for. + * @param[out] listIndex the location's index in the list, if found. + * @param[out] aProgressElement copy of the progress element contents, if found. + * @return true if a progress element is found, false otherwise. + * + * @note may be overloaded in device implementation for optimization, if desired. + */ + virtual bool GetProgressElementById(uint32_t aLocationId, uint32_t & listIndex, + Structs::ProgressStruct::Type & aProgressElement); + + /** + * @brief Is the progress element in the progress list? + * @param[in] aLocationId location id of the progress element. + * @return true if the progress element identified by Id is in the progress list. + */ + virtual bool IsProgressElement(uint32_t aLocationId); + + /** + * This method is called by the server instance to add a new progress element to the list. + * The server instance will ensure that the newProgressElement is a valid, unique progress element. + * @param[in] newProgressElement The new element to add. + * @param[out] listIndex is filled with the list index for the new element, if successful. + * @return true if successful, false otherwise. + * + * @note this function SHOULD double check that the added element won't exceed the maximum list size. + */ + virtual bool AddProgressElement(const Structs::ProgressStruct::Type & newProgressElement, uint32_t & listIndex) = 0; + + /** + * This method is called by the server instance to modify an existing progress element in the list. + * The server instance will ensure that the modifiedProgressElement is a valid and unique progress element. + * @param[in] listIndex The list index of the progress element being modified. + * @param[in] modifiedProgressElement modified element's contents. + * @return true if successful, false otherwise. + * + * @note this function SHOULD double check that modifiedProgressElement's locationID matches the object at listIndex + */ + virtual bool ModifyProgressElement(uint32_t listIndex, const Structs::ProgressStruct::Type & modifiedProgressElement) = 0; + + /** + * @return true if progress list was not already null, false otherwise. + */ + virtual bool ClearProgress() = 0; + + Instance * GetInstance() { return mInstance; } + + void SetInstance(Instance * aInstance) { mInstance = aInstance; } + +private: + Instance * mInstance = nullptr; +}; + +} // namespace ServiceArea +} // namespace Clusters +} // namespace app +} // namespace chip diff --git a/src/app/clusters/service-area-server/service-area-server.cpp b/src/app/clusters/service-area-server/service-area-server.cpp new file mode 100644 index 00000000000000..97775fb24f67c0 --- /dev/null +++ b/src/app/clusters/service-area-server/service-area-server.cpp @@ -0,0 +1,1131 @@ +/** + * + * Copyright (c) 2023 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 "service-area-server.h" +#include "service-area-delegate.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using Status = chip::Protocols::InteractionModel::Status; + +namespace chip { +namespace app { +namespace Clusters { +namespace ServiceArea { + +// **************************************************************************** +// Service Area Server Instance + +Instance::Instance(Delegate * aDelegate, EndpointId aEndpointId, BitMask aFeature) : + AttributeAccessInterface(MakeOptional(aEndpointId), Id), CommandHandlerInterface(MakeOptional(aEndpointId), Id), + mDelegate(aDelegate), mEndpointId(aEndpointId), mClusterId(Id), mFeature(aFeature) +{ + ChipLogProgress(Zcl, "Service Area: Instance constructor"); + mDelegate->SetInstance(this); +} + +Instance::~Instance() +{ + CommandHandlerInterfaceRegistry::UnregisterCommandHandler(this); + unregisterAttributeAccessOverride(this); +} + +CHIP_ERROR Instance::Init() +{ + ChipLogProgress(Zcl, "Service Area: INIT"); + + // Check if the cluster has been selected in zap + VerifyOrReturnError(emberAfContainsServer(mEndpointId, Id), CHIP_ERROR_INVALID_ARGUMENT, + ChipLogError(Zcl, "Service Area: The cluster with Id %lu was not enabled in zap.", long(Id))); + + ReturnErrorOnFailure(CommandHandlerInterfaceRegistry::RegisterCommandHandler(this)); + + VerifyOrReturnError(registerAttributeAccessOverride(this), CHIP_ERROR_INCORRECT_STATE); + + return mDelegate->Init(); +} + +//************************************************************************* +// core functions + +CHIP_ERROR Instance::Read(const ConcreteReadAttributePath & aPath, AttributeValueEncoder & aEncoder) +{ + ChipLogDetail(Zcl, "Service Area: Reading attribute with ID " ChipLogFormatMEI, ChipLogValueMEI(aPath.mAttributeId)); + + switch (aPath.mAttributeId) + { + + case Attributes::SupportedLocations::Id: + return ReadSupportedLocations(aEncoder); + + case Attributes::SupportedMaps::Id: + return ReadSupportedMaps(aEncoder); + + case Attributes::SelectedLocations::Id: + return ReadSelectedLocations(aEncoder); + + case Attributes::CurrentLocation::Id: + return aEncoder.Encode(GetCurrentLocation()); + + case Attributes::EstimatedEndTime::Id: + return aEncoder.Encode(GetEstimatedEndTime()); + + case Attributes::Progress::Id: + return ReadProgress(aEncoder); + + case Attributes::FeatureMap::Id: + return aEncoder.Encode(mFeature); + + default: + ChipLogProgress(Zcl, "Service Area: Unsupported attribute with ID " ChipLogFormatMEI, ChipLogValueMEI(aPath.mAttributeId)); + } + + return CHIP_NO_ERROR; +} + +void Instance::InvokeCommand(HandlerContext & handlerContext) +{ + switch (handlerContext.mRequestPath.mCommandId) + { + case Commands::SelectLocations::Id: + return CommandHandlerInterface::HandleCommand( + handlerContext, [this](HandlerContext & ctx, const auto & req) { HandleSelectLocationsCmd(ctx, req); }); + + case Commands::SkipCurrentLocation::Id: + return CommandHandlerInterface::HandleCommand( + handlerContext, [this](HandlerContext & ctx, const auto & req) { HandleSkipCurrentLocationCmd(ctx); }); + } +} + +//************************************************************************* +// attribute readers + +CHIP_ERROR Instance::ReadSupportedLocations(AttributeValueEncoder & aEncoder) +{ + if (mDelegate->GetNumberOfSupportedLocations() == 0) + { + return aEncoder.EncodeNull(); + } + + return aEncoder.EncodeList([this](const auto & encoder) -> CHIP_ERROR { + uint8_t locationIndex = 0; + LocationStructureWrapper supportedLocation; + + while (mDelegate->GetSupportedLocationByIndex(locationIndex++, supportedLocation)) + { + ReturnErrorOnFailure(encoder.Encode(supportedLocation)); + } + return CHIP_NO_ERROR; + }); +} + +CHIP_ERROR Instance::ReadSupportedMaps(AttributeValueEncoder & aEncoder) +{ + if (mDelegate->GetNumberOfSupportedMaps() == 0) + { + return aEncoder.EncodeNull(); + } + + return aEncoder.EncodeList([this](const auto & encoder) -> CHIP_ERROR { + uint32_t mapIndex = 0; + MapStructureWrapper supportedMap; + + while (mDelegate->GetSupportedMapByIndex(mapIndex++, supportedMap)) + { + ReturnErrorOnFailure(encoder.Encode(supportedMap)); + } + return CHIP_NO_ERROR; + }); +} + +CHIP_ERROR Instance::ReadSelectedLocations(AttributeValueEncoder & aEncoder) +{ + if (mDelegate->GetNumberOfSelectedLocations() == 0) + { + return aEncoder.EncodeNull(); + } + + return aEncoder.EncodeList([this](const auto & encoder) -> CHIP_ERROR { + uint32_t locationIndex = 0; + uint32_t selectedLocation; + + while (mDelegate->GetSelectedLocationByIndex(locationIndex++, selectedLocation)) + { + ReturnErrorOnFailure(encoder.Encode(selectedLocation)); + } + return CHIP_NO_ERROR; + }); +} + +CHIP_ERROR Instance::ReadProgress(AttributeValueEncoder & aEncoder) +{ + if (mDelegate->GetNumberOfProgressElements() == 0) + { + return aEncoder.EncodeNull(); + } + + return aEncoder.EncodeList([this](const auto & encoder) -> CHIP_ERROR { + uint32_t locationIndex = 0; + Structs::ProgressStruct::Type progressElement; + + while (mDelegate->GetProgressElementByIndex(locationIndex++, progressElement)) + { + ReturnErrorOnFailure(encoder.Encode(progressElement)); + } + return CHIP_NO_ERROR; + }); +} + +//************************************************************************* +// command handlers + +void Instance::HandleSelectLocationsCmd(HandlerContext & ctx, const Commands::SelectLocations::DecodableType & req) +{ + ChipLogDetail(Zcl, "Service Area: HandleSelectLocationsCmd"); + + // On receipt of this command the device SHALL respond with a SelectLocationsResponse command. + auto exitResponse = [ctx](SelectLocationsStatus status, CharSpan statusText) { + Commands::SelectLocationsResponse::Type response{ + .status = status, + .statusText = Optional(statusText), + }; + ctx.mCommandHandler.AddResponse(ctx.mRequestPath, response); + }; + + size_t numberOfLocations = 0; + // Get the number of Selected Locations in the command parameter and check that it is valid. + if (!req.newLocations.IsNull()) + { + if (CHIP_NO_ERROR != req.newLocations.Value().ComputeSize(&numberOfLocations)) + { + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::InvalidCommand); + return; + } + + // If the device determines that it can't operate at all locations from the list, + // the SelectLocationsResponse command's Status field SHALL indicate InvalidSet. + if (numberOfLocations > kMaxNumSelectedLocations) + { + exitResponse(SelectLocationsStatus::kInvalidSet, "invalid number of locations"_span); + return; + } + } + + // if number of selected locations in parameter matches number in attribute - the locations *might* be the same + bool matchesCurrentSelectedLocations = (numberOfLocations == mDelegate->GetNumberOfSelectedLocations()); + + if (!req.newLocations.IsNull()) + { + // do as much parameter validation as we can + { + uint32_t ignoredIndex = 0; + uint32_t oldSelectedLocation; + uint32_t i = 0; + auto iLocationIter = req.newLocations.Value().begin(); + while (iLocationIter.Next()) + { + uint32_t aSelectedLocation = iLocationIter.GetValue(); + + // each item in this list SHALL match the LocationID field of an entry on the SupportedLocations attribute's list + // If the Status field is set to UnsupportedLocation, the StatusText field SHALL be an empty string. + if (!IsSupportedLocation(aSelectedLocation)) + { + exitResponse(SelectLocationsStatus::kUnsupportedLocation, ""_span); + return; + } + + // Checking for duplicate locations. + uint32_t j = 0; + auto jLocationIter = req.newLocations.Value().begin(); + while (j < i) + { + jLocationIter + .Next(); // Since j < i and i is valid, we can safely call Next() without checking the return value. + if (jLocationIter.GetValue() == aSelectedLocation) + { + exitResponse(SelectLocationsStatus::kDuplicatedLocations, ""_span); + return; + } + j += 1; + } + + // check to see if parameter list and attribute still match + if (matchesCurrentSelectedLocations) + { + if (!mDelegate->GetSelectedLocationByIndex(ignoredIndex, oldSelectedLocation) || + (aSelectedLocation != oldSelectedLocation)) + { + matchesCurrentSelectedLocations = false; + } + } + + i += 1; + } + + // after iterating with Next through DecodableType - check for failure + if (CHIP_NO_ERROR != iLocationIter.GetStatus()) + { + ctx.mCommandHandler.AddStatus(ctx.mRequestPath, Status::InvalidCommand); + return; + } + } + } + + // If the NewLocations field is the same as the value of the SelectedLocations attribute + // the SelectLocationsResponse command SHALL have the Status field set to Success and + // the StatusText field MAY be supplied with a human-readable string or include an empty string. + if (matchesCurrentSelectedLocations) + { + exitResponse(SelectLocationsStatus::kSuccess, ""_span); + return; + } + + char delegateStatusBuffer[kMaxSizeStatusText]; + MutableCharSpan delegateStatusText(delegateStatusBuffer); + + // If the current state of the device doesn't allow for the locations to be selected, + // the SelectLocationsResponse command SHALL have the Status field set to InvalidInMode. + // if the Status field is set to InvalidInMode, the StatusText field SHOULD indicate why the request is not allowed, + // given the current mode of the device, which may involve other clusters. + // (note - locationStatusText to be filled out by delegated function for if return value is false) + if (!mDelegate->IsSetSelectedLocationsAllowed(delegateStatusText)) + { + exitResponse(SelectLocationsStatus::kInvalidInMode, delegateStatusText); + return; + } + + // Reset in case the delegate accidentally modified this string. + delegateStatusText = MutableCharSpan(delegateStatusBuffer); + + // ask the device to handle SelectLocations Command + // (note - locationStatusText to be filled out by delegated function for kInvalidInMode and InvalidSet) + auto locationStatus = SelectLocationsStatus::kSuccess; + if (!mDelegate->IsValidSelectLocationsSet(req, locationStatus, delegateStatusText)) + { + exitResponse(locationStatus, delegateStatusText); + return; + } + + { + // If the device successfully accepts the request, the server will attempt to operate at the location(s) + // indicated by the entries of the NewLocation field, when requested to operate, + // the SelectLocationsResponse command SHALL have the Status field set to Success, + // and the SelectedLocations attribute SHALL be set to the value of the NewLocations field. + mDelegate->ClearSelectedLocations(); + + if (!req.newLocations.IsNull()) + { + auto locationIter = req.newLocations.Value().begin(); + uint32_t ignored; + while (locationIter.Next()) + { + mDelegate->AddSelectedLocation(locationIter.GetValue(), ignored); + } + } + + NotifySelectedLocationsChanged(); + } + + exitResponse(SelectLocationsStatus::kSuccess, ""_span); +} + +void Instance::HandleSkipCurrentLocationCmd(HandlerContext & ctx) +{ + ChipLogDetail(Zcl, "Service Area: HandleSkipCurrentLocation"); + + // On receipt of this command the device SHALL respond with a SkipCurrentLocationResponse command. + auto exitResponse = [ctx](SkipCurrentLocationStatus status, CharSpan statusText) { + Commands::SkipCurrentLocationResponse::Type response{ + .status = status, + .statusText = Optional(statusText), + }; + ctx.mCommandHandler.AddResponse(ctx.mRequestPath, response); + }; + + // If the SelectedLocations attribute is null, the response status should be set to InvalidLocationList. + // If the Status field is set to InvalidLocationList, the StatusText field SHALL be an empty string. + if (mDelegate->GetNumberOfSelectedLocations() == 0) + { + ChipLogError(Zcl, "Selected Locations attribute is null"); + exitResponse(SkipCurrentLocationStatus::kInvalidLocationList, ""_span); + return; + } + + // If the CurrentLocation attribute is null, the status should be set to InvalidInMode. + // If the Status field is not set to Success, or InvalidLocationList, the StatusText field SHALL include a vendor defined error + // description. + if (mCurrentLocation.IsNull()) + { + exitResponse(SkipCurrentLocationStatus::kInvalidInMode, "Current Location attribute is null"_span); + return; + } + + // have the device attempt to skip + // If the Status field is not set to Success, or InvalidLocationList, the StatusText field SHALL include a vendor defined error + // description. InvalidInMode | The received request cannot be handled due to the current mode of the device. (skipStatusText to + // be filled out by delegated function on failure.) + char skipStatusBuffer[kMaxSizeStatusText]; + MutableCharSpan skipStatusText(skipStatusBuffer); + + if (!mDelegate->HandleSkipCurrentLocation(skipStatusText)) + { + exitResponse(SkipCurrentLocationStatus::kInvalidInMode, skipStatusText); + return; + } +} + +//************************************************************************* +// attribute notifications + +void Instance::NotifySupportedLocationsChanged() +{ + MatterReportingAttributeChangeCallback(mEndpointId, mClusterId, Attributes::SupportedLocations::Id); +} + +void Instance::NotifySupportedMapsChanged() +{ + MatterReportingAttributeChangeCallback(mEndpointId, mClusterId, Attributes::SupportedMaps::Id); +} + +void Instance::NotifySelectedLocationsChanged() +{ + MatterReportingAttributeChangeCallback(mEndpointId, mClusterId, Attributes::SelectedLocations::Id); +} + +void Instance::NotifyCurrentLocationChanged() +{ + MatterReportingAttributeChangeCallback(mEndpointId, mClusterId, Attributes::CurrentLocation::Id); +} + +void Instance::NotifyEstimatedEndTimeChanged() +{ + MatterReportingAttributeChangeCallback(mEndpointId, mClusterId, Attributes::EstimatedEndTime::Id); +} + +void Instance::NotifyProgressChanged() +{ + MatterReportingAttributeChangeCallback(mEndpointId, mClusterId, Attributes::Progress::Id); +} + +// **************************************************************************** +// Supported Locations manipulators + +bool Instance::IsSupportedLocation(uint32_t aLocationId) +{ + uint32_t ignoredIndex; + LocationStructureWrapper ignoredLocation; + + return mDelegate->GetSupportedLocationById(aLocationId, ignoredIndex, ignoredLocation); +} + +bool Instance::IsValidSupportedLocation(const LocationStructureWrapper & aLocation) +{ + // If the HomeLocationInfo field is null, the LandmarkTag field SHALL NOT be null. + // If the LandmarkTag field is null, the HomeLocationInfo field SHALL NOT be null. + if (aLocation.locationInfo.locationInfo.IsNull() && aLocation.locationInfo.landmarkTag.IsNull()) + { + ChipLogDetail(Zcl, "IsValidAsSupportedLocation %u - must have locationInfo and/or LandmarkTag", aLocation.locationID); + return false; + } + + // If HomeLocationInfo is not null, and its LocationName field is an empty string, at least one of the following SHALL NOT + // be null: HomeLocationInfo's FloorNumber field, HomeLocationInfo's AreaType field, the LandmarkTag field + if (!aLocation.locationInfo.locationInfo.IsNull()) + { + if (aLocation.locationInfo.locationInfo.Value().locationName.empty() && + aLocation.locationInfo.locationInfo.Value().floorNumber.IsNull() && + aLocation.locationInfo.locationInfo.Value().areaType.IsNull() && aLocation.locationInfo.landmarkTag.IsNull()) + { + ChipLogDetail( + Zcl, "IsValidAsSupportedLocation %u - LocationName is empty string, FloorNumber, AreaType, LandmarkTag are null", + aLocation.locationID); + return false; + } + } + + // If the LandmarkTag field is null, the PositionTag field SHALL be null. + if (aLocation.locationInfo.landmarkTag.IsNull() && !aLocation.locationInfo.positionTag.IsNull()) + { + ChipLogDetail(Zcl, "IsValidAsSupportedLocation %u - PositionTag with no LandmarkTag", aLocation.locationID); + return false; + } + + if (mDelegate->GetNumberOfSupportedMaps() == 0) + { + // If the SupportedMaps attribute is null, mapid SHALL be null. + if (!aLocation.mapID.IsNull()) + { + ChipLogDetail(Zcl, "IsValidSupportedLocation %u - map Id %u is not in empty supported map list", aLocation.locationID, + aLocation.mapID.Value()); + return false; + } + } + else + { + // If the SupportedMaps attribute is not null, mapID SHALL be the ID of an entry from the SupportedMaps attribute. + if (!IsSupportedMap(aLocation.mapID.Value())) + { + ChipLogError(Zcl, "IsValidSupportedLocation %u - map Id %u is not in supported map list", aLocation.locationID, + aLocation.mapID.Value()); + return false; + } + } + + return true; +} + +bool Instance::IsUniqueSupportedLocation(const LocationStructureWrapper & aLocation, bool ignoreLocationId) +{ + BitMask config; + + if (ignoreLocationId) + { + config.Set(LocationStructureWrapper::IsEqualConfig::kIgnoreLocationId); + } + + // If the SupportedMaps attribute is not null, each entry in this list SHALL have a unique value for the combination of the + // MapID and LocationInfo fields. If the SupportedMaps attribute is null, each entry in this list SHALL have a unique value for + // the LocationInfo field. + if (mDelegate->GetNumberOfSupportedMaps() == 0) + { + config.Set(LocationStructureWrapper::IsEqualConfig::kIgnoreMapId); + } + + uint8_t locationIndex = 0; + LocationStructureWrapper entry; + while (mDelegate->GetSupportedLocationByIndex(locationIndex++, entry)) + { + if (aLocation.IsEqual(entry, config)) + { + return false; + } + } + + return true; +} + +bool Instance::ReportEstimatedEndTimeChange(const DataModel::Nullable & aEstimatedEndTime) +{ + if (mEstimatedEndTime == aEstimatedEndTime) + { + return false; + } + + // The value of this attribute SHALL only be reported in the following cases: + // - when it changes from null. + if (mEstimatedEndTime.IsNull()) + { + return true; + } + + // - when it changes from 0 + if (mEstimatedEndTime.Value() == 0) + { + return true; + } + + if (aEstimatedEndTime.IsNull()) + { + return false; + } + + // From this point we know that mEstimatedEndTime and aEstimatedEndTime are not null and not the same. + + // - when it changes to 0 + if (aEstimatedEndTime.Value() == 0) + { + return true; + } + + // - when it decreases + return (aEstimatedEndTime.Value() < mEstimatedEndTime.Value()); +} + +bool Instance::AddSupportedLocation(uint32_t aLocationId, const DataModel::Nullable & aMapId, + const CharSpan & aLocationName, const DataModel::Nullable & aFloorNumber, + const DataModel::Nullable & aAreaType, + const DataModel::Nullable & aLandmarkTag, + const DataModel::Nullable & aPositionTag, + const DataModel::Nullable & aSurfaceTag) +{ + // Create location object for validation. + LocationStructureWrapper aNewLocation(aLocationId, aMapId, aLocationName, aFloorNumber, aAreaType, aLandmarkTag, aPositionTag, + aSurfaceTag); + + // Does device mode allow this attribute to be updated? + if (!mDelegate->IsSupportedLocationsChangeAllowed()) + { + return false; + } + + // Check there is space for the entry. + if (mDelegate->GetNumberOfSupportedLocations() >= kMaxNumSupportedLocations) + { + ChipLogError(Zcl, "AddSupportedLocation %u - too many entries", aLocationId); + return false; + } + + // Verify cluster requirements concerning valid fields and field relationships. + if (!IsValidSupportedLocation(aNewLocation)) + { + ChipLogError(Zcl, "AddSupportedLocation %u - not a valid location object", aNewLocation.locationID); + return false; + } + + // Each entry in Supported Locations SHALL have a unique value for the ID field. + // If the SupportedMaps attribute is not null, each entry in this list SHALL have a unique value for the combination of the + // MapID and LocationInfo fields. If the SupportedMaps attribute is null, each entry in this list SHALL have a unique value for + // the LocationInfo field. + if (!IsUniqueSupportedLocation(aNewLocation, false)) + { + ChipLogError(Zcl, "AddSupportedLocation %u - not a unique location object", aNewLocation.locationID); + return false; + } + + // Add the SupportedLocation to the SupportedLocations attribute. + uint32_t ignoredIndex; + if (!mDelegate->AddSupportedLocation(aNewLocation, ignoredIndex)) + { + return false; + } + + NotifySupportedLocationsChanged(); + return true; +} + +bool Instance::ModifySupportedLocation(uint32_t aLocationId, const DataModel::Nullable & aMapId, + const CharSpan & aLocationName, const DataModel::Nullable & aFloorNumber, + const DataModel::Nullable & aAreaType, + const DataModel::Nullable & aLandmarkTag, + const DataModel::Nullable & aPositionTag, + const DataModel::Nullable & aSurfaceTag) +{ + bool mapIDChanged = false; + uint32_t listIndex; + + // get existing supported location to modify + LocationStructureWrapper supportedLocation; + if (!mDelegate->GetSupportedLocationById(aLocationId, listIndex, supportedLocation)) + { + ChipLogError(Zcl, "ModifySupportedLocation %u - not a supported locationID", aLocationId); + return false; + } + + { + // check for mapID change + if ((aMapId.IsNull() != supportedLocation.mapID.IsNull()) || + (!aMapId.IsNull() && !supportedLocation.mapID.IsNull() && (aMapId.Value() != supportedLocation.mapID.Value()))) + { + // does device mode allow this attribute to be updated? + if (!mDelegate->IsSupportedLocationsChangeAllowed()) + { + return false; + } + mapIDChanged = true; + } + + // create new location object for validation + LocationStructureWrapper aNewLocation(aLocationId, aMapId, aLocationName, aFloorNumber, aAreaType, aLandmarkTag, + aPositionTag, aSurfaceTag); + + // verify cluster requirements concerning valid fields and field relationships + if (!IsValidSupportedLocation(aNewLocation)) + { + ChipLogError(Zcl, "ModifySupportedLocation %u - not a valid location object", aNewLocation.locationID); + return false; + } + + // Updated location description must not match another existing location description. + // We ignore comparing the location ID as one of the locations will match this one. + if (!IsUniqueSupportedLocation(aNewLocation, true)) + { + ChipLogError(Zcl, "ModifySupportedLocation %u - not a unique location object", aNewLocation.locationID); + return false; + } + + // Replace the supported location with the modified location. + if (!mDelegate->ModifySupportedLocation(listIndex, aNewLocation)) + { + return false; + } + } + + if (mapIDChanged) + { + mDelegate->HandleSupportedLocationsUpdated(); + } + + NotifySupportedLocationsChanged(); + return true; +} + +bool Instance::ClearSupportedLocations() +{ + // does device mode allow this attribute to be updated? + if (!mDelegate->IsSupportedLocationsChangeAllowed()) + { + return false; + } + + if (mDelegate->ClearSupportedLocations()) + { + mDelegate->HandleSupportedLocationsUpdated(); + NotifySupportedLocationsChanged(); + return true; + } + + return false; +} + +//************************************************************************* +// Supported Maps manipulators + +bool Instance::IsSupportedMap(uint8_t aMapId) +{ + uint32_t ignoredIndex; + MapStructureWrapper ignoredMap; + + return mDelegate->GetSupportedMapById(aMapId, ignoredIndex, ignoredMap); +} + +bool Instance::AddSupportedMap(uint8_t aMapId, const CharSpan & aMapName) +{ + // check max# of list entries + if (mDelegate->GetNumberOfSupportedMaps() >= kMaxNumSupportedMaps) + { + ChipLogError(Zcl, "AddSupportedMap %u - maximum number of entries", aMapId); + return false; + } + + // Map name SHALL include readable text that describes the map name (cannot be empty string). + if (aMapName.empty()) + { + ChipLogError(Zcl, "AddSupportedMap %u - Name must not be empty string", aMapId); + return false; + } + + // Each entry in this list SHALL have a unique value for the Name field. + uint8_t mapIndex = 0; + MapStructureWrapper entry; + + while (mDelegate->GetSupportedMapByIndex(mapIndex++, entry)) + { + // the name cannot be the same as an existing map + if (entry.IsNameEqual(aMapName)) + { + ChipLogError(Zcl, "AddSupportedMap %u - A map already exists with same name '%.*s'", aMapId, + static_cast(entry.GetName().size()), entry.GetName().data()); + return false; + } + + // Each entry in this list SHALL have a unique value for the MapID field. + if (aMapId == entry.mapID) + { + ChipLogError(Zcl, "AddSupportedMap - non-unique Id %u", aMapId); + return false; + } + } + + { + // add to supported maps attribute + MapStructureWrapper newMap(aMapId, aMapName); + uint32_t ignoredIndex; + if (!mDelegate->AddSupportedMap(newMap, ignoredIndex)) + { + return false; + } + } + + // map successfully added + NotifySupportedMapsChanged(); + return true; +} + +bool Instance::RenameSupportedMap(uint8_t aMapId, const CharSpan & newMapName) +{ + uint32_t modifiedIndex; + MapStructureWrapper modifiedMap; + + // get existing entry + if (!mDelegate->GetSupportedMapById(aMapId, modifiedIndex, modifiedMap)) + { + ChipLogError(Zcl, "RenameSupportedMap Id %u - map does not exist", aMapId); + return false; + } + + // Map name SHALL include readable text that describes the map's name. It cannot be empty string. + if (newMapName.empty()) + { + ChipLogError(Zcl, "RenameSupportedMap %u - Name must not be empty string", aMapId); + return false; + } + + // update the local copy of the map + modifiedMap.Set(modifiedMap.mapID, newMapName); + + // Each entry in this list SHALL have a unique value for the Name field. + uint32_t loopIndex = 0; + MapStructureWrapper entry; + + while (mDelegate->GetSupportedMapByIndex(loopIndex, entry)) + { + if (modifiedIndex == loopIndex) + { + continue; // don't check local modified map against its own list entry + } + + if (entry.IsNameEqual(newMapName)) + { + ChipLogError(Zcl, "RenameSupportedMap %u - map already exists with same name '%.*s'", aMapId, + static_cast(entry.GetName().size()), entry.GetName().data()); + return false; + } + + ++loopIndex; + } + + if (!mDelegate->ModifySupportedMap(modifiedIndex, modifiedMap)) + { + return false; + } + + // map successfully renamed + NotifySupportedMapsChanged(); + return true; +} + +bool Instance::ClearSupportedMaps() +{ + // does device mode allow this attribute to be updated? + if (!mDelegate->IsSupportedMapChangeAllowed()) + { + return false; + } + + if (mDelegate->ClearSupportedMaps()) + { + ClearSupportedLocations(); + NotifySupportedMapsChanged(); + return true; + } + + return false; +} + +//************************************************************************* +// Selected Locations manipulators + +bool Instance::AddSelectedLocation(uint32_t & aSelectedLocation) +{ + // check max# of list entries + if (mDelegate->GetNumberOfSelectedLocations() >= kMaxNumSelectedLocations) + { + ChipLogError(Zcl, "AddSelectedLocation %u - maximum number of entries", aSelectedLocation); + return false; + } + + // each item in this list SHALL match the LocationID field of an entry on the SupportedLocations attribute's list + if (!IsSupportedLocation(aSelectedLocation)) + { + ChipLogError(Zcl, "AddSelectedLocation %u - not a supported location", aSelectedLocation); + return false; + } + + // each entry in this list SHALL have a unique value + if (mDelegate->IsSelectedLocation(aSelectedLocation)) + { + ChipLogError(Zcl, "AddSelectedLocation %u - duplicated location", aSelectedLocation); + return false; + } + + // Does device mode allow modification of selected locations? + char locationStatusBuffer[kMaxSizeStatusText]; + MutableCharSpan locationStatusText(locationStatusBuffer); + + if (!mDelegate->IsSetSelectedLocationsAllowed(locationStatusText)) + { + ChipLogError(Zcl, "AddSelectedLocation %u - %.*s", aSelectedLocation, static_cast(locationStatusText.size()), + locationStatusText.data()); + return false; + } + + uint32_t ignoredIndex; + return mDelegate->AddSelectedLocation(aSelectedLocation, ignoredIndex); +} + +bool Instance::ClearSelectedLocations() +{ + if (mDelegate->ClearSelectedLocations()) + { + NotifySelectedLocationsChanged(); + return true; + } + + return false; +} + +//************************************************************************* +// Current Location manipulators + +DataModel::Nullable Instance::GetCurrentLocation() +{ + return mCurrentLocation; +} + +bool Instance::SetCurrentLocation(const DataModel::Nullable & aCurrentLocation) +{ + // If not null, the value of this attribute SHALL match the LocationID field of an entry on the SupportedLocations attribute's + // list. + if ((!aCurrentLocation.IsNull()) && (!IsSupportedLocation(aCurrentLocation.Value()))) + { + ChipLogError(Zcl, "SetCurrentLocation %u - location is not supported", aCurrentLocation.Value()); + return false; + } + + bool notifyChange = mCurrentLocation != aCurrentLocation; + + mCurrentLocation = aCurrentLocation; + if (notifyChange) + { + NotifyCurrentLocationChanged(); + } + + // EstimatedEndTime SHALL be null if the CurrentLocation attribute is null. + if (mCurrentLocation.IsNull()) + { + SetEstimatedEndTime(DataModel::NullNullable); + } + + return true; +} + +//************************************************************************* +// Estimated End Time manipulators + +DataModel::Nullable Instance::GetEstimatedEndTime() +{ + return mEstimatedEndTime; +} + +bool Instance::SetEstimatedEndTime(const DataModel::Nullable & aEstimatedEndTime) +{ + // EstimatedEndTime SHALL be null if the CurrentLocation attribute is null. + if (mCurrentLocation.IsNull() && !aEstimatedEndTime.IsNull()) + { + ChipLogError(Zcl, "SetEstimatedEndTime - must be null if Current Location is null"); + return false; + } + + bool notifyChange = ReportEstimatedEndTimeChange(aEstimatedEndTime); + + mEstimatedEndTime = aEstimatedEndTime; + + if (notifyChange) + { + NotifyEstimatedEndTimeChanged(); + } + + // success + return true; +} + +//************************************************************************* +// Progress list manipulators + +bool Instance::AddPendingProgressElement(uint32_t aLocationId) +{ + // create progress element + Structs::ProgressStruct::Type inactiveProgress = { aLocationId, OperationalStatusEnum::kPending }; + + // check max# of list entries + if (mDelegate->GetNumberOfProgressElements() >= kMaxNumProgressElements) + { + ChipLogError(Zcl, "AddPendingProgressElement - maximum number of entries"); + return false; + } + + // For each entry in this list, the LocationID field SHALL match an entry on the SupportedLocations attribute's list. + if (!IsSupportedLocation(aLocationId)) + { + ChipLogError(Zcl, "AddPendingProgressElement - not a supported location %u", aLocationId); + return false; + } + + // Each entry in this list SHALL have a unique value for the LocationID field. + if (mDelegate->IsProgressElement(aLocationId)) + { + ChipLogError(Zcl, "AddPendingProgressElement - progress element already exists for location %u", aLocationId); + return false; + } + + uint32_t ignoredIndex; + + if (!mDelegate->AddProgressElement(inactiveProgress, ignoredIndex)) + { + return false; + } + + NotifyProgressChanged(); + return true; +} + +bool Instance::SetProgressStatus(uint32_t aLocationId, OperationalStatusEnum opStatus) +{ + uint32_t listIndex; + Structs::ProgressStruct::Type progressElement; + + if (!mDelegate->GetProgressElementById(aLocationId, listIndex, progressElement)) + { + ChipLogError(Zcl, "SetProgressStatus - progress element does not exist for location %u", aLocationId); + return false; + } + + // If the status value is not changing, there in no need to modify the existing element. + if (progressElement.status == opStatus) + { + return true; + } + + // set the progress status in the local copy + progressElement.status = opStatus; + + // TotalOperationalTime SHALL be null if the Status field is not set to Completed or Skipped. + if ((opStatus != OperationalStatusEnum::kCompleted) && (opStatus != OperationalStatusEnum::kSkipped)) + { + progressElement.totalOperationalTime.Value().SetNull(); + } + + // add the updated element to the progress attribute + if (!mDelegate->ModifyProgressElement(listIndex, progressElement)) + { + return false; + } + + NotifyProgressChanged(); + return true; +} + +bool Instance::SetProgressTotalOperationalTime(uint32_t aLocationId, const DataModel::Nullable & aTotalOperationalTime) +{ + uint32_t listIndex; + Structs::ProgressStruct::Type progressElement; + + if (!mDelegate->GetProgressElementById(aLocationId, listIndex, progressElement)) + { + ChipLogError(Zcl, "SetProgressTotalOperationalTime - progress element does not exist for location %u", aLocationId); + return false; + } + + // If the time value is not changing, there is no need to modify the existing element. + if (progressElement.totalOperationalTime == aTotalOperationalTime) + { + return true; + } + + // This attribute SHALL be null if the Status field is not set to Completed or Skipped + if (((progressElement.status == OperationalStatusEnum::kCompleted) || + (progressElement.status == OperationalStatusEnum::kSkipped)) && + !aTotalOperationalTime.IsNull()) + { + ChipLogError(Zcl, + "SetProgressTotalOperationalTime - location %u opStatus value %u - can be non-null only if opStatus is " + "Completed or Skipped", + aLocationId, to_underlying(progressElement.status)); + return false; + } + + // set the time in the local copy + progressElement.totalOperationalTime.Emplace(aTotalOperationalTime); + + // add the updated element to the progress attribute + if (!mDelegate->ModifyProgressElement(listIndex, progressElement)) + { + return false; + } + + NotifyProgressChanged(); + return true; +} + +bool Instance::SetProgressEstimatedTime(uint32_t aLocationId, const DataModel::Nullable & aEstimatedTime) +{ + uint32_t listIndex; + Structs::ProgressStruct::Type progressElement; + + if (!mDelegate->GetProgressElementById(aLocationId, listIndex, progressElement)) + { + ChipLogError(Zcl, "SetProgressEstimatedTime - progress element does not exist for location %u", aLocationId); + return false; + } + + // If the time value is not changing, there is no need to modify the existing element. + if (progressElement.estimatedTime == aEstimatedTime) + { + return true; + }; + + // set the time in the local copy + progressElement.estimatedTime.Emplace(aEstimatedTime); + + // add the updated element to the progress attribute + if (!mDelegate->ModifyProgressElement(listIndex, progressElement)) + { + return false; + } + + NotifyProgressChanged(); + return true; +} + +bool Instance::ClearProgress() +{ + if (mDelegate->ClearProgress()) + { + NotifyProgressChanged(); + return true; + } + + return false; +} + +// attribute manipulators - Feature Map + +bool Instance::HasFeature(Feature feature) const +{ + return mFeature.Has(feature); +} + +} // namespace ServiceArea +} // namespace Clusters +} // namespace app +} // namespace chip diff --git a/src/app/clusters/service-area-server/service-area-server.h b/src/app/clusters/service-area-server/service-area-server.h new file mode 100644 index 00000000000000..f43ea07337406f --- /dev/null +++ b/src/app/clusters/service-area-server/service-area-server.h @@ -0,0 +1,357 @@ +/* + * + * Copyright (c) 2024 Project CHIP Authors + * All rights reserved. + * + * 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. + */ + +#pragma once + +#include "service-area-cluster-objects.h" +#include "service-area-delegate.h" +#include + +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace chip { +namespace app { +namespace Clusters { +namespace ServiceArea { + +/** + * Instance is a class that represents an instance of the generic Service Area cluster. + * It implements AttributeAccessInterface and CommandHandlerInterface so it can + * handle commands for any implementation of the location cluster. + * Custom implementations of the Service Area cluster override functions in the Delegate class + * must be provided to operate with specific devices. + */ +class Instance : public AttributeAccessInterface, public CommandHandlerInterface +{ +public: + /** + * @brief Creates a Service Area cluster instance. The Init() method needs to be called for this instance + * to be registered and called by the interaction model at the appropriate times. + * @param[in] aDelegate A pointer to the delegate to be used by this server. + * @param[in] aEndpointId The endpoint on which this cluster exists. This must match the zap configuration. + * @param[in] aFeature The supported features of this Service Area Cluster. + * + * @note the caller must ensure that the delegate lives throughout the instance's lifetime. + */ + Instance(Delegate * aDelegate, EndpointId aEndpointId, BitMask aFeature); + + ~Instance() override; + + /** + * Stop this class objects from being copied. + */ + Instance(const Instance &) = delete; + Instance & operator=(const Instance &) = delete; + + /** + * @brief Initialise the Service Area server instance. + * @return an error if the given endpoint and cluster Id have not been enabled in zap or if the + * CommandHandler or AttributeHandler registration fails, else CHIP_NO_ERROR. + */ + CHIP_ERROR Init(); + +private: + Delegate * mDelegate; + EndpointId mEndpointId; + ClusterId mClusterId; + + // Attribute Data Store + DataModel::Nullable mCurrentLocation; + DataModel::Nullable mEstimatedEndTime; + BitMask mFeature; + + //************************************************************************* + // core functions + + /** + * @brief Read Attribute - inherited from AttributeAccessInterface. + * @return appropriately mapped CHIP_ERROR if applicable. + */ + CHIP_ERROR Read(const ConcreteReadAttributePath & aPath, AttributeValueEncoder & aEncoder) override; + + /** + * @brief Command handler - inherited from CommandHandlerInterface. + * @param[in, out] ctx command context. + */ + void InvokeCommand(HandlerContext & ctx) override; + + //************************************************************************* + // attribute readers + + CHIP_ERROR ReadSupportedLocations(chip::app::AttributeValueEncoder & aEncoder); + + CHIP_ERROR ReadSupportedMaps(chip::app::AttributeValueEncoder & aEncoder); + + CHIP_ERROR ReadSelectedLocations(chip::app::AttributeValueEncoder & aEncoder); + + CHIP_ERROR ReadProgress(chip::app::AttributeValueEncoder & aEncoder); + + //************************************************************************* + // command handlers + + /** + * @param[in, out] ctx Returns the Interaction Model status code which was user determined in the business logic. + * If the input value is invalid, returns the Interaction Model status code of INVALID_COMMAND. + * @param[in] req the command parameters + */ + void HandleSelectLocationsCmd(HandlerContext & ctx, const Commands::SelectLocations::DecodableType & req); + + /** + * @param[in, out] ctx Returns the Interaction Model status code which was user determined in the business logic. + * If the input value is invalid, returns the Interaction Model status code of INVALID_COMMAND. + */ + void HandleSkipCurrentLocationCmd(HandlerContext & ctx); + + //************************************************************************* + // attribute notifications + + void NotifySupportedLocationsChanged(); + void NotifySupportedMapsChanged(); + void NotifySelectedLocationsChanged(); + void NotifyCurrentLocationChanged(); + void NotifyEstimatedEndTimeChanged(); + void NotifyProgressChanged(); + + //************************************************************************* + // Supported Locations manipulators + + /** + * @return true if a location with the aLocationId ID exists in the supported locations attribute. False otherwise. + */ + bool IsSupportedLocation(uint32_t aLocationId); + + /** + * @brief Check if the given location adheres to the restrictions required by the supported locations attribute. + * @return true if the aLocation meets all checks. + */ + bool IsValidSupportedLocation(const LocationStructureWrapper & aLocation); + + /** + * @brief check if aLocation is unique with regard to supported locations. + * @param[in] aLocation the location to check. + * @param[out] ignoreLocationId if true, we do not check if the location ID is unique. + * @return true if there isn't a location in supported locations that matches aLocation. + * + * @note This method may ignore checking the MapId uniqueness. This depends on whether the SupportedMaps attribute is null. + */ + bool IsUniqueSupportedLocation(const LocationStructureWrapper & aLocation, bool ignoreLocationId); + + /** + * @brief Check if changing the estimated end time attribute to aEstimatedEndTime requires the change to be reported. + * @param aEstimatedEndTime The new estimated end time. + * @return true if the change requires a report. + */ + bool ReportEstimatedEndTimeChange(const DataModel::Nullable & aEstimatedEndTime); + +public: + /** + * @brief Add new location to the supported locations list. + * @param[in] aLocationId unique identifier of this location. + * @param[in] aMapId identifier of supported map. + * @param[in] aLocationName human readable name for this location (empty string if not used). + * @param[in] aFloorNumber represents floor level - negative values for below ground. + * @param[in] aAreaType common namespace Area tag - indicates an association of the location with an indoor or outdoor area of a + * home. + * @param[in] aLandmarkTag common namespace Landmark tag - indicates an association of the location with a home landmark. + * @param[in] aPositionTag common namespace Position tag - indicates the position of the location with respect to the landmark. + * @param[in] aSurfaceTag common namespace Floor Surface tag - indicates an association of the location with a surface type. + * @return true if the new location passed validation checks and was successfully added to the list. + * + * @note if aLocationName is larger than kLocationNameMaxSize, it will be truncated. + */ + bool AddSupportedLocation(uint32_t aLocationId, const DataModel::Nullable & aMapId, const CharSpan & aLocationName, + const DataModel::Nullable & aFloorNumber, const DataModel::Nullable & aAreaType, + const DataModel::Nullable & aLandmarkTag, + const DataModel::Nullable & aPositionTag, + const DataModel::Nullable & aSurfaceTag); + + /** + * @brief Modify/replace an existing location in the supported locations list. + * @param[in] aLocationId unique identifier of this location. + * @param[in] aMapId identifier of supported map (will not be modified). + * @param[in] aLocationName human readable name for this location (empty string if not used). + * @param[in] aFloorNumber represents floor level - negative values for below ground. + * @param[in] aAreaType common namespace Area tag - indicates an association of the location with an indoor or outdoor area of a + * home. + * @param[in] aLandmarkTag common namespace Landmark tag - indicates an association of the location with a home landmark. + * @param[in] aPositionTag common namespace Position tag - indicates the position of the location with respect to the landmark. + * @param[in] aSurfaceTag common namespace Floor Surface tag - indicates an association of the location with a surface type. + * @return true if the location is a member of supported locations, the modifications pass all validation checks and the + * location was modified. + * + * @note if aLocationName is larger than kLocationNameMaxSize, it will be truncated. + * @note if mapID is changed, the delegate's HandleSupportedLocationsUpdated method is called. + */ + bool ModifySupportedLocation(uint32_t aLocationId, const DataModel::Nullable & aMapId, const CharSpan & aLocationName, + const DataModel::Nullable & aFloorNumber, + const DataModel::Nullable & aAreaType, + const DataModel::Nullable & aLandmarkTag, + const DataModel::Nullable & aPositionTag, + const DataModel::Nullable & aSurfaceTag); + + /** + * @return true if the SupportedLocations attribute was not already null. + * + * @note if SupportedLocations is cleared, the delegate's HandleSupportedLocationsUpdated method is called. + */ + bool ClearSupportedLocations(); + + //************************************************************************* + // Supported Maps manipulators + + /** + * @return true if a map with the aMapId ID exists in the supported maps attribute. False otherwise. + */ + bool IsSupportedMap(uint8_t aMapId); + + /** + * @brief Add a new map to the supported maps list. + * @param[in] aMapId The ID of the new map to be added. + * @param[in] aMapName The name of the map to be added. This cannot be an empty string. + * @return true if the new map passed validation checks and was successfully added to the list. + */ + bool AddSupportedMap(uint8_t aMapId, const CharSpan & aMapName); + + /** + * @brief Rename an existing map in the supported maps list. + * @param[in] aMapId The id of the map to modify. + * @param[in] newMapName The new name of the map. This cannot be empty string. + * @return true if the new name passed validation checks and was successfully modified. + * + * @note if the specified map is not a member of the supported maps list, returns false with no action taken. + */ + bool RenameSupportedMap(uint8_t aMapId, const CharSpan & newMapName); + + /** + * @return true if the SupportedMaps attribute was not already null. + * + * @note if SupportedMaps is cleared, the delegate's HandleSupportedLocationsUpdated method is called. + */ + bool ClearSupportedMaps(); + + //************************************************************************* + // Selected Locations manipulators + + /** + * @brief Add a selected location. + * @param[in] aSelectedLocation The locationID to add. + * @bool true if successfully added. + */ + bool AddSelectedLocation(uint32_t & aSelectedLocation); + + /** + * @return true if the SelectedLocations attribute was not already null. + */ + bool ClearSelectedLocations(); + + //************************************************************************* + // Current Location manipulators + + DataModel::Nullable GetCurrentLocation(); + + /** + * @param[in] aCurrentLocation The location ID that the CurrentLocation attribute should be set to. Must be a supported location + * or NULL. + * @return true if the current location is set, false otherwise. + * + * @note if current location is set to null, estimated end time will be set to null. + */ + bool SetCurrentLocation(const DataModel::Nullable & aCurrentLocation); + + //************************************************************************* + // Estimated End Time manipulators + + /** + * @return The estimated epoch time in seconds when operation at the location indicated by the CurrentLocation attribute will be + * completed. + */ + DataModel::Nullable GetEstimatedEndTime(); + + /** + * @param[in] aEstimatedEndTime The estimated epoch time in seconds when operation at the location indicated by the + * CurrentLocation attribute will be completed. + * @return true if attribute is set, false otherwise. + */ + bool SetEstimatedEndTime(const DataModel::Nullable & aEstimatedEndTime); + + //************************************************************************* + // Progress list manipulators + + /** + * @brief Add a progress element in a pending status to the progress list. + * @param[in] aLocationId location id of the progress element. + * @return true if the new progress element passed validation checks and was successfully added to the list, false otherwise. + */ + bool AddPendingProgressElement(uint32_t aLocationId); + + /** + * @brief Set the status of progress element identified by locationID. + * @param[in] aLocationId The locationID of the progress element to update. + * @param[in] status The location cluster operation status for this location. + * @return true if progress element is found and status is set, false otherwise. + * + * @note TotalOperationalTime is set to null if resulting opStatus is not equal to Completed or Skipped. + */ + bool SetProgressStatus(uint32_t aLocationId, OperationalStatusEnum opStatus); + + /** + * @brief Set the total operational time for the progress element identified by locationID. + * @param[in] aLocationId The locationID of the progress element to update. + * @param[in] aTotalOperationalTime The total operational time for this location. + * @return true if progress element is found and operational time is set, false otherwise. + */ + bool SetProgressTotalOperationalTime(uint32_t aLocationId, const DataModel::Nullable & aTotalOperationalTime); + + /** + * @brief Set the estimated time for the progress element identified by locationID. + * @param[in] aLocationId The locationID of the progress element to update. + * @param[in] aEstimatedTime The estimated time for this location. + * @return true if progress element is found and estimated time is set, false otherwise. + */ + bool SetProgressEstimatedTime(uint32_t aLocationId, const DataModel::Nullable & aEstimatedTime); + + /** + * @return true if the progress list was not already null, false otherwise. + */ + bool ClearProgress(); + + //************************************************************************* + // Feature Map attribute + + /** + * @brief Check if a feature is supported. + * @param[in] feature the feature enum. + * @return true if the feature is supported. + * + * @note the Service Area features are set at startup and are read-only to both device and client. + */ + bool HasFeature(ServiceArea::Feature feature) const; +}; + +} // namespace ServiceArea +} // namespace Clusters +} // namespace app +} // namespace chip diff --git a/src/app/common/templates/config-data.yaml b/src/app/common/templates/config-data.yaml index d77db76970818c..d8d276f53a0700 100644 --- a/src/app/common/templates/config-data.yaml +++ b/src/app/common/templates/config-data.yaml @@ -24,6 +24,7 @@ CommandHandlerInterfaceOnlyClusters: - Scenes Management - RVC Run Mode - RVC Clean Mode + - Service Area - Dishwasher Mode - Laundry Washer Mode - Oven Mode diff --git a/src/app/util/util.cpp b/src/app/util/util.cpp index f903b5b52ea8da..763e198f41fded 100644 --- a/src/app/util/util.cpp +++ b/src/app/util/util.cpp @@ -139,6 +139,7 @@ void MatterEnergyEvseModePluginServerInitCallback() {} void MatterPowerTopologyPluginServerInitCallback() {} void MatterElectricalEnergyMeasurementPluginServerInitCallback() {} void MatterElectricalPowerMeasurementPluginServerInitCallback() {} +void MatterServiceAreaPluginServerInitCallback() {} void MatterWaterHeaterManagementPluginServerInitCallback() {} void MatterWaterHeaterModePluginServerInitCallback() {} diff --git a/src/app/zap-templates/zcl/zcl-with-test-extensions.json b/src/app/zap-templates/zcl/zcl-with-test-extensions.json index 1452fa0f9ed8f7..4c5a59200e7d49 100644 --- a/src/app/zap-templates/zcl/zcl-with-test-extensions.json +++ b/src/app/zap-templates/zcl/zcl-with-test-extensions.json @@ -669,7 +669,8 @@ "PreferredExtendedPanID", "ThreadNetworks", "ThreadNetworkTableSize" - ] + ], + "Service Area": ["CurrentLocation", "EstimatedEndTime", "FeatureMap"] }, "defaultReportingPolicy": "mandatory", "ZCLDataTypes": ["ARRAY", "BITMAP", "ENUM", "NUMBER", "STRING", "STRUCT"], diff --git a/src/app/zap-templates/zcl/zcl.json b/src/app/zap-templates/zcl/zcl.json index 769e6e87b698e8..777847fb7f06ad 100644 --- a/src/app/zap-templates/zcl/zcl.json +++ b/src/app/zap-templates/zcl/zcl.json @@ -667,7 +667,8 @@ "PreferredExtendedPanID", "ThreadNetworks", "ThreadNetworkTableSize" - ] + ], + "Service Area": ["CurrentLocation", "EstimatedEndTime", "FeatureMap"] }, "defaultReportingPolicy": "mandatory", "ZCLDataTypes": ["ARRAY", "BITMAP", "ENUM", "NUMBER", "STRING", "STRUCT"], diff --git a/src/app/zap_cluster_list.json b/src/app/zap_cluster_list.json index 6939c1475ead70..e5f0ea9e2e892a 100644 --- a/src/app/zap_cluster_list.json +++ b/src/app/zap_cluster_list.json @@ -108,6 +108,7 @@ "RVC_CLEAN_MODE_CLUSTER": [], "RVC_RUN_MODE_CLUSTER": [], "SCENES_CLUSTER": [], + "SERVICE_AREA_CLUSTER": [], "SMOKE_CO_ALARM_CLUSTER": [], "SOFTWARE_DIAGNOSTICS_CLUSTER": [], "SWITCH_CLUSTER": [], @@ -284,6 +285,7 @@ "RVC_CLEAN_MODE_CLUSTER": ["mode-base-server"], "RVC_RUN_MODE_CLUSTER": ["mode-base-server"], "SCENES_CLUSTER": ["scenes-server"], + "SERVICE_AREA_CLUSTER": ["service-area-server"], "SMOKE_CO_ALARM_CLUSTER": ["smoke-co-alarm-server"], "SOFTWARE_DIAGNOSTICS_CLUSTER": ["software-diagnostics-server"], "SWITCH_CLUSTER": ["switch-server"], diff --git a/src/controller/python/chip/clusters/__init__.py b/src/controller/python/chip/clusters/__init__.py index 779b8c6fe5a4bb..5fbb13dcc616a9 100644 --- a/src/controller/python/chip/clusters/__init__.py +++ b/src/controller/python/chip/clusters/__init__.py @@ -42,8 +42,8 @@ PowerSourceConfiguration, PowerTopology, PressureMeasurement, ProxyConfiguration, ProxyDiscovery, ProxyValid, PulseWidthModulation, PumpConfigurationAndControl, RadonConcentrationMeasurement, RefrigeratorAlarm, RefrigeratorAndTemperatureControlledCabinetMode, RelativeHumidityMeasurement, RvcCleanMode, - RvcOperationalState, RvcRunMode, ScenesManagement, SmokeCoAlarm, SoftwareDiagnostics, Switch, TargetNavigator, - TemperatureControl, TemperatureMeasurement, Thermostat, ThermostatUserInterfaceConfiguration, + RvcOperationalState, RvcRunMode, ScenesManagement, ServiceArea, SmokeCoAlarm, SoftwareDiagnostics, Switch, + TargetNavigator, TemperatureControl, TemperatureMeasurement, Thermostat, ThermostatUserInterfaceConfiguration, ThreadBorderRouterManagement, ThreadNetworkDiagnostics, ThreadNetworkDirectory, TimeFormatLocalization, TimeSynchronization, TotalVolatileOrganicCompoundsConcentrationMeasurement, UnitLocalization, UnitTesting, UserLabel, ValveConfigurationAndControl, WakeOnLan, WaterHeaterManagement, WaterHeaterMode, @@ -66,7 +66,7 @@ Pm25ConcentrationMeasurement, PowerSource, PowerSourceConfiguration, PowerTopology, PressureMeasurement, ProxyConfiguration, ProxyDiscovery, ProxyValid, PulseWidthModulation, PumpConfigurationAndControl, RadonConcentrationMeasurement, RefrigeratorAlarm, RefrigeratorAndTemperatureControlledCabinetMode, RelativeHumidityMeasurement, RvcCleanMode, - RvcOperationalState, RvcRunMode, ScenesManagement, SmokeCoAlarm, SoftwareDiagnostics, + RvcOperationalState, RvcRunMode, ScenesManagement, ServiceArea, SmokeCoAlarm, SoftwareDiagnostics, Switch, TargetNavigator, TemperatureControl, TemperatureMeasurement, Thermostat, ThermostatUserInterfaceConfiguration, ThreadBorderRouterManagement, ThreadNetworkDiagnostics, ThreadNetworkDirectory, TimeFormatLocalization, TimeSynchronization, TotalVolatileOrganicCompoundsConcentrationMeasurement, UnitLocalization, diff --git a/zzz_generated/app-common/app-common/zap-generated/attributes/Accessors.cpp b/zzz_generated/app-common/app-common/zap-generated/attributes/Accessors.cpp index 31476aab9492bc..81744d5c6994ff 100644 --- a/zzz_generated/app-common/app-common/zap-generated/attributes/Accessors.cpp +++ b/zzz_generated/app-common/app-common/zap-generated/attributes/Accessors.cpp @@ -20950,228 +20950,6 @@ Protocols::InteractionModel::Status Set(chip::EndpointId endpoint, uint16_t valu namespace ServiceArea { namespace Attributes { -namespace CurrentLocation { - -Protocols::InteractionModel::Status Get(chip::EndpointId endpoint, DataModel::Nullable & value) -{ - using Traits = NumericAttributeTraits; - Traits::StorageType temp; - uint8_t * readable = Traits::ToAttributeStoreRepresentation(temp); - Protocols::InteractionModel::Status status = - emberAfReadAttribute(endpoint, Clusters::ServiceArea::Id, Id, readable, sizeof(temp)); - VerifyOrReturnError(Protocols::InteractionModel::Status::Success == status, status); - if (Traits::IsNullValue(temp)) - { - value.SetNull(); - } - else - { - value.SetNonNull() = Traits::StorageToWorking(temp); - } - return status; -} - -Protocols::InteractionModel::Status Set(chip::EndpointId endpoint, uint32_t value, MarkAttributeDirty markDirty) -{ - using Traits = NumericAttributeTraits; - if (!Traits::CanRepresentValue(/* isNullable = */ true, value)) - { - return Protocols::InteractionModel::Status::ConstraintError; - } - Traits::StorageType storageValue; - Traits::WorkingToStorage(value, storageValue); - uint8_t * writable = Traits::ToAttributeStoreRepresentation(storageValue); - return emberAfWriteAttribute(endpoint, Clusters::ServiceArea::Id, Id, writable, ZCL_INT32U_ATTRIBUTE_TYPE, markDirty); -} - -Protocols::InteractionModel::Status Set(chip::EndpointId endpoint, uint32_t value) -{ - using Traits = NumericAttributeTraits; - if (!Traits::CanRepresentValue(/* isNullable = */ true, value)) - { - return Protocols::InteractionModel::Status::ConstraintError; - } - Traits::StorageType storageValue; - Traits::WorkingToStorage(value, storageValue); - uint8_t * writable = Traits::ToAttributeStoreRepresentation(storageValue); - return emberAfWriteAttribute(endpoint, Clusters::ServiceArea::Id, Id, writable, ZCL_INT32U_ATTRIBUTE_TYPE); -} - -Protocols::InteractionModel::Status SetNull(chip::EndpointId endpoint, MarkAttributeDirty markDirty) -{ - using Traits = NumericAttributeTraits; - Traits::StorageType value; - Traits::SetNull(value); - uint8_t * writable = Traits::ToAttributeStoreRepresentation(value); - return emberAfWriteAttribute(endpoint, Clusters::ServiceArea::Id, Id, writable, ZCL_INT32U_ATTRIBUTE_TYPE, markDirty); -} - -Protocols::InteractionModel::Status SetNull(chip::EndpointId endpoint) -{ - using Traits = NumericAttributeTraits; - Traits::StorageType value; - Traits::SetNull(value); - uint8_t * writable = Traits::ToAttributeStoreRepresentation(value); - return emberAfWriteAttribute(endpoint, Clusters::ServiceArea::Id, Id, writable, ZCL_INT32U_ATTRIBUTE_TYPE); -} - -Protocols::InteractionModel::Status Set(chip::EndpointId endpoint, const chip::app::DataModel::Nullable & value, - MarkAttributeDirty markDirty) -{ - if (value.IsNull()) - { - return SetNull(endpoint, markDirty); - } - - return Set(endpoint, value.Value(), markDirty); -} - -Protocols::InteractionModel::Status Set(chip::EndpointId endpoint, const chip::app::DataModel::Nullable & value) -{ - if (value.IsNull()) - { - return SetNull(endpoint); - } - - return Set(endpoint, value.Value()); -} - -} // namespace CurrentLocation - -namespace EstimatedEndTime { - -Protocols::InteractionModel::Status Get(chip::EndpointId endpoint, DataModel::Nullable & value) -{ - using Traits = NumericAttributeTraits; - Traits::StorageType temp; - uint8_t * readable = Traits::ToAttributeStoreRepresentation(temp); - Protocols::InteractionModel::Status status = - emberAfReadAttribute(endpoint, Clusters::ServiceArea::Id, Id, readable, sizeof(temp)); - VerifyOrReturnError(Protocols::InteractionModel::Status::Success == status, status); - if (Traits::IsNullValue(temp)) - { - value.SetNull(); - } - else - { - value.SetNonNull() = Traits::StorageToWorking(temp); - } - return status; -} - -Protocols::InteractionModel::Status Set(chip::EndpointId endpoint, uint32_t value, MarkAttributeDirty markDirty) -{ - using Traits = NumericAttributeTraits; - if (!Traits::CanRepresentValue(/* isNullable = */ true, value)) - { - return Protocols::InteractionModel::Status::ConstraintError; - } - Traits::StorageType storageValue; - Traits::WorkingToStorage(value, storageValue); - uint8_t * writable = Traits::ToAttributeStoreRepresentation(storageValue); - return emberAfWriteAttribute(endpoint, Clusters::ServiceArea::Id, Id, writable, ZCL_EPOCH_S_ATTRIBUTE_TYPE, markDirty); -} - -Protocols::InteractionModel::Status Set(chip::EndpointId endpoint, uint32_t value) -{ - using Traits = NumericAttributeTraits; - if (!Traits::CanRepresentValue(/* isNullable = */ true, value)) - { - return Protocols::InteractionModel::Status::ConstraintError; - } - Traits::StorageType storageValue; - Traits::WorkingToStorage(value, storageValue); - uint8_t * writable = Traits::ToAttributeStoreRepresentation(storageValue); - return emberAfWriteAttribute(endpoint, Clusters::ServiceArea::Id, Id, writable, ZCL_EPOCH_S_ATTRIBUTE_TYPE); -} - -Protocols::InteractionModel::Status SetNull(chip::EndpointId endpoint, MarkAttributeDirty markDirty) -{ - using Traits = NumericAttributeTraits; - Traits::StorageType value; - Traits::SetNull(value); - uint8_t * writable = Traits::ToAttributeStoreRepresentation(value); - return emberAfWriteAttribute(endpoint, Clusters::ServiceArea::Id, Id, writable, ZCL_EPOCH_S_ATTRIBUTE_TYPE, markDirty); -} - -Protocols::InteractionModel::Status SetNull(chip::EndpointId endpoint) -{ - using Traits = NumericAttributeTraits; - Traits::StorageType value; - Traits::SetNull(value); - uint8_t * writable = Traits::ToAttributeStoreRepresentation(value); - return emberAfWriteAttribute(endpoint, Clusters::ServiceArea::Id, Id, writable, ZCL_EPOCH_S_ATTRIBUTE_TYPE); -} - -Protocols::InteractionModel::Status Set(chip::EndpointId endpoint, const chip::app::DataModel::Nullable & value, - MarkAttributeDirty markDirty) -{ - if (value.IsNull()) - { - return SetNull(endpoint, markDirty); - } - - return Set(endpoint, value.Value(), markDirty); -} - -Protocols::InteractionModel::Status Set(chip::EndpointId endpoint, const chip::app::DataModel::Nullable & value) -{ - if (value.IsNull()) - { - return SetNull(endpoint); - } - - return Set(endpoint, value.Value()); -} - -} // namespace EstimatedEndTime - -namespace FeatureMap { - -Protocols::InteractionModel::Status Get(chip::EndpointId endpoint, uint32_t * value) -{ - using Traits = NumericAttributeTraits; - Traits::StorageType temp; - uint8_t * readable = Traits::ToAttributeStoreRepresentation(temp); - Protocols::InteractionModel::Status status = - emberAfReadAttribute(endpoint, Clusters::ServiceArea::Id, Id, readable, sizeof(temp)); - VerifyOrReturnError(Protocols::InteractionModel::Status::Success == status, status); - if (!Traits::CanRepresentValue(/* isNullable = */ false, temp)) - { - return Protocols::InteractionModel::Status::ConstraintError; - } - *value = Traits::StorageToWorking(temp); - return status; -} - -Protocols::InteractionModel::Status Set(chip::EndpointId endpoint, uint32_t value, MarkAttributeDirty markDirty) -{ - using Traits = NumericAttributeTraits; - if (!Traits::CanRepresentValue(/* isNullable = */ false, value)) - { - return Protocols::InteractionModel::Status::ConstraintError; - } - Traits::StorageType storageValue; - Traits::WorkingToStorage(value, storageValue); - uint8_t * writable = Traits::ToAttributeStoreRepresentation(storageValue); - return emberAfWriteAttribute(endpoint, Clusters::ServiceArea::Id, Id, writable, ZCL_BITMAP32_ATTRIBUTE_TYPE, markDirty); -} - -Protocols::InteractionModel::Status Set(chip::EndpointId endpoint, uint32_t value) -{ - using Traits = NumericAttributeTraits; - if (!Traits::CanRepresentValue(/* isNullable = */ false, value)) - { - return Protocols::InteractionModel::Status::ConstraintError; - } - Traits::StorageType storageValue; - Traits::WorkingToStorage(value, storageValue); - uint8_t * writable = Traits::ToAttributeStoreRepresentation(storageValue); - return emberAfWriteAttribute(endpoint, Clusters::ServiceArea::Id, Id, writable, ZCL_BITMAP32_ATTRIBUTE_TYPE); -} - -} // namespace FeatureMap - namespace ClusterRevision { Protocols::InteractionModel::Status Get(chip::EndpointId endpoint, uint16_t * value) diff --git a/zzz_generated/app-common/app-common/zap-generated/attributes/Accessors.h b/zzz_generated/app-common/app-common/zap-generated/attributes/Accessors.h index 5432b0efab52ac..cd73287e99b4ca 100644 --- a/zzz_generated/app-common/app-common/zap-generated/attributes/Accessors.h +++ b/zzz_generated/app-common/app-common/zap-generated/attributes/Accessors.h @@ -3359,34 +3359,6 @@ Protocols::InteractionModel::Status Set(chip::EndpointId endpoint, uint16_t valu namespace ServiceArea { namespace Attributes { -namespace CurrentLocation { -Protocols::InteractionModel::Status Get(chip::EndpointId endpoint, DataModel::Nullable & value); // int32u -Protocols::InteractionModel::Status Set(chip::EndpointId endpoint, uint32_t value); -Protocols::InteractionModel::Status Set(chip::EndpointId endpoint, uint32_t value, MarkAttributeDirty markDirty); -Protocols::InteractionModel::Status SetNull(chip::EndpointId endpoint); -Protocols::InteractionModel::Status SetNull(chip::EndpointId endpoint, MarkAttributeDirty markDirty); -Protocols::InteractionModel::Status Set(chip::EndpointId endpoint, const chip::app::DataModel::Nullable & value); -Protocols::InteractionModel::Status Set(chip::EndpointId endpoint, const chip::app::DataModel::Nullable & value, - MarkAttributeDirty markDirty); -} // namespace CurrentLocation - -namespace EstimatedEndTime { -Protocols::InteractionModel::Status Get(chip::EndpointId endpoint, DataModel::Nullable & value); // epoch_s -Protocols::InteractionModel::Status Set(chip::EndpointId endpoint, uint32_t value); -Protocols::InteractionModel::Status Set(chip::EndpointId endpoint, uint32_t value, MarkAttributeDirty markDirty); -Protocols::InteractionModel::Status SetNull(chip::EndpointId endpoint); -Protocols::InteractionModel::Status SetNull(chip::EndpointId endpoint, MarkAttributeDirty markDirty); -Protocols::InteractionModel::Status Set(chip::EndpointId endpoint, const chip::app::DataModel::Nullable & value); -Protocols::InteractionModel::Status Set(chip::EndpointId endpoint, const chip::app::DataModel::Nullable & value, - MarkAttributeDirty markDirty); -} // namespace EstimatedEndTime - -namespace FeatureMap { -Protocols::InteractionModel::Status Get(chip::EndpointId endpoint, uint32_t * value); // bitmap32 -Protocols::InteractionModel::Status Set(chip::EndpointId endpoint, uint32_t value); -Protocols::InteractionModel::Status Set(chip::EndpointId endpoint, uint32_t value, MarkAttributeDirty markDirty); -} // namespace FeatureMap - namespace ClusterRevision { Protocols::InteractionModel::Status Get(chip::EndpointId endpoint, uint16_t * value); // int16u Protocols::InteractionModel::Status Set(chip::EndpointId endpoint, uint16_t value); diff --git a/zzz_generated/app-common/app-common/zap-generated/callback.h b/zzz_generated/app-common/app-common/zap-generated/callback.h index fada12b193c24b..60cdf13689cf10 100644 --- a/zzz_generated/app-common/app-common/zap-generated/callback.h +++ b/zzz_generated/app-common/app-common/zap-generated/callback.h @@ -6294,18 +6294,6 @@ bool emberAfBarrierControlClusterBarrierControlGoToPercentCallback( bool emberAfBarrierControlClusterBarrierControlStopCallback( chip::app::CommandHandler * commandObj, const chip::app::ConcreteCommandPath & commandPath, const chip::app::Clusters::BarrierControl::Commands::BarrierControlStop::DecodableType & commandData); -/** - * @brief Service Area Cluster SelectLocations Command callback (from client) - */ -bool emberAfServiceAreaClusterSelectLocationsCallback( - chip::app::CommandHandler * commandObj, const chip::app::ConcreteCommandPath & commandPath, - const chip::app::Clusters::ServiceArea::Commands::SelectLocations::DecodableType & commandData); -/** - * @brief Service Area Cluster SkipCurrentLocation Command callback (from client) - */ -bool emberAfServiceAreaClusterSkipCurrentLocationCallback( - chip::app::CommandHandler * commandObj, const chip::app::ConcreteCommandPath & commandPath, - const chip::app::Clusters::ServiceArea::Commands::SkipCurrentLocation::DecodableType & commandData); /** * @brief Thermostat Cluster SetpointRaiseLower Command callback (from client) */