/* * * Copyright (c) 2022-2023 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 "LockEndpoint.h" #include #include #include #include #include #include using chip::ByteSpan; using chip::MutableByteSpan; using chip::Optional; using chip::to_underlying; using chip::app::DataModel::MakeNullable; struct LockActionData { chip::EndpointId endpointId; DlLockState lockState; OperationSourceEnum opSource; Nullable userIndex; uint16_t credentialIndex; Nullable fabricIdx; Nullable nodeId; bool moving = false; }; static LockActionData gCurrentAction; bool LockEndpoint::Lock(const Nullable & fabricIdx, const Nullable & nodeId, const Optional & pin, OperationErrorEnum & err, OperationSourceEnum opSource) { return setLockState(fabricIdx, nodeId, DlLockState::kLocked, pin, err, opSource); } bool LockEndpoint::Unlock(const Nullable & fabricIdx, const Nullable & nodeId, const Optional & pin, OperationErrorEnum & err, OperationSourceEnum opSource) { if (DoorLockServer::Instance().SupportsUnbolt(mEndpointId)) { // If Unbolt is supported Unlock is supposed to pull the latch return setLockState(fabricIdx, nodeId, DlLockState::kUnlatched, pin, err, opSource); } return setLockState(fabricIdx, nodeId, DlLockState::kUnlocked, pin, err, opSource); } bool LockEndpoint::Unbolt(const Nullable & fabricIdx, const Nullable & nodeId, const Optional & pin, OperationErrorEnum & err, OperationSourceEnum opSource) { return setLockState(fabricIdx, nodeId, DlLockState::kUnlocked, pin, err, opSource); } bool LockEndpoint::GetUser(uint16_t userIndex, EmberAfPluginDoorLockUserInfo & user) const { ChipLogProgress(Zcl, "Lock App: LockEndpoint::GetUser [endpoint=%d,userIndex=%hu]", mEndpointId, userIndex); auto adjustedUserIndex = static_cast(userIndex - 1); if (adjustedUserIndex > mLockUsers.size()) { ChipLogError(Zcl, "Cannot get user - index out of range [endpoint=%d,index=%hu,adjustedIndex=%d]", mEndpointId, userIndex, adjustedUserIndex); return false; } const auto & userInDb = mLockUsers[adjustedUserIndex]; user.userStatus = userInDb.userStatus; if (UserStatusEnum::kAvailable == user.userStatus) { ChipLogDetail(Zcl, "Found unoccupied user [endpoint=%d,adjustedIndex=%hu]", mEndpointId, adjustedUserIndex); return true; } user.userName = chip::CharSpan(userInDb.userName, strlen(userInDb.userName)); user.credentials = chip::Span(userInDb.credentials.data(), userInDb.credentials.size()); user.userUniqueId = userInDb.userUniqueId; user.userType = userInDb.userType; user.credentialRule = userInDb.credentialRule; // So far there's no way to actually create the credential outside the matter, so here we always set the creation/modification // source to Matter user.creationSource = DlAssetSource::kMatterIM; user.createdBy = userInDb.createdBy; user.modificationSource = DlAssetSource::kMatterIM; user.lastModifiedBy = userInDb.lastModifiedBy; ChipLogDetail(Zcl, "Found occupied user " "[endpoint=%d,adjustedIndex=%hu,name=\"%.*s\",credentialsCount=%u,uniqueId=%x,type=%u,credentialRule=%u," "createdBy=%d,lastModifiedBy=%d]", mEndpointId, adjustedUserIndex, static_cast(user.userName.size()), user.userName.data(), static_cast(user.credentials.size()), user.userUniqueId, to_underlying(user.userType), to_underlying(user.credentialRule), user.createdBy, user.lastModifiedBy); return true; } bool LockEndpoint::SetUser(uint16_t userIndex, chip::FabricIndex creator, chip::FabricIndex modifier, const chip::CharSpan & userName, uint32_t uniqueId, UserStatusEnum userStatus, UserTypeEnum usertype, CredentialRuleEnum credentialRule, const CredentialStruct * credentials, size_t totalCredentials) { ChipLogProgress(Zcl, "Lock App: LockEndpoint::SetUser " "[endpoint=%d,userIndex=%u,creator=%d,modifier=%d,userName=\"%.*s\",uniqueId=%" PRIx32 ",userStatus=%u,userType=%u," "credentialRule=%u,credentials=%p,totalCredentials=%u]", mEndpointId, userIndex, creator, modifier, static_cast(userName.size()), userName.data(), uniqueId, to_underlying(userStatus), to_underlying(usertype), to_underlying(credentialRule), credentials, static_cast(totalCredentials)); auto adjustedUserIndex = static_cast(userIndex - 1); if (adjustedUserIndex > mLockUsers.size()) { ChipLogError(Zcl, "Cannot set user - index out of range [endpoint=%d,index=%d,adjustedUserIndex=%u]", mEndpointId, userIndex, adjustedUserIndex); return false; } auto & userInStorage = mLockUsers[adjustedUserIndex]; if (userName.size() > DOOR_LOCK_MAX_USER_NAME_SIZE) { ChipLogError(Zcl, "Cannot set user - user name is too long [endpoint=%d,index=%d,adjustedUserIndex=%u]", mEndpointId, userIndex, adjustedUserIndex); return false; } if (totalCredentials > userInStorage.credentials.capacity()) { ChipLogError(Zcl, "Cannot set user - total number of credentials is too big [endpoint=%d,index=%d,adjustedUserIndex=%u" ",totalCredentials=%u,maxNumberOfCredentials=%u]", mEndpointId, userIndex, adjustedUserIndex, static_cast(totalCredentials), static_cast(userInStorage.credentials.capacity())); return false; } chip::Platform::CopyString(userInStorage.userName, userName); userInStorage.userName[userName.size()] = 0; userInStorage.userUniqueId = uniqueId; userInStorage.userStatus = userStatus; userInStorage.userType = usertype; userInStorage.credentialRule = credentialRule; userInStorage.lastModifiedBy = modifier; userInStorage.createdBy = creator; userInStorage.credentials.clear(); for (size_t i = 0; i < totalCredentials; ++i) { userInStorage.credentials.push_back(credentials[i]); } ChipLogProgress(Zcl, "Successfully set the user [mEndpointId=%d,index=%d,adjustedIndex=%d]", mEndpointId, userIndex, adjustedUserIndex); return true; } DoorStateEnum LockEndpoint::GetDoorState() const { return mDoorState; } bool LockEndpoint::SetDoorState(DoorStateEnum newState) { if (mDoorState != newState) { ChipLogProgress(Zcl, "Changing the door state to: %d [endpointId=%d,previousState=%d]", to_underlying(newState), mEndpointId, to_underlying(mDoorState)); mDoorState = newState; return DoorLockServer::Instance().SetDoorState(mEndpointId, mDoorState); } return true; } bool LockEndpoint::SendLockAlarm(AlarmCodeEnum alarmCode) const { ChipLogProgress(Zcl, "Sending the LockAlarm event [endpointId=%d,alarmCode=%u]", mEndpointId, to_underlying(alarmCode)); return DoorLockServer::Instance().SendLockAlarmEvent(mEndpointId, alarmCode); } bool LockEndpoint::GetCredential(uint16_t credentialIndex, CredentialTypeEnum credentialType, EmberAfPluginDoorLockCredentialInfo & credential) const { ChipLogProgress(Zcl, "Lock App: LockEndpoint::GetCredential [endpoint=%d,credentialIndex=%u,credentialType=%u]", mEndpointId, credentialIndex, to_underlying(credentialType)); if (to_underlying(credentialType) >= mLockCredentials.size()) { ChipLogError(Zcl, "Cannot get the credential - index out of range [endpoint=%d,index=%d]", mEndpointId, credentialIndex); return false; } if (credentialIndex >= mLockCredentials.at(to_underlying(credentialType)).size() || (0 == credentialIndex && CredentialTypeEnum::kProgrammingPIN != credentialType)) { ChipLogError(Zcl, "Cannot get the credential - index out of range [endpoint=%d,index=%d]: %d", mEndpointId, credentialIndex, static_cast(mLockCredentials.at(to_underlying(credentialType)).size())); return false; } const auto & credentialInStorage = mLockCredentials[to_underlying(credentialType)][credentialIndex]; credential.status = credentialInStorage.status; if (DlCredentialStatus::kAvailable == credential.status) { ChipLogDetail(Zcl, "Found unoccupied credential [endpoint=%d,index=%u]", mEndpointId, credentialIndex); return true; } credential.credentialType = credentialInStorage.credentialType; credential.credentialData = chip::ByteSpan(credentialInStorage.credentialData, credentialInStorage.credentialDataSize); // So far there's no way to actually create the credential outside the matter, so here we always set the creation/modification // source to Matter credential.creationSource = DlAssetSource::kMatterIM; credential.createdBy = credentialInStorage.createdBy; credential.modificationSource = DlAssetSource::kMatterIM; credential.lastModifiedBy = credentialInStorage.modifiedBy; ChipLogDetail(Zcl, "Found occupied credential [endpoint=%d,index=%u,type=%u,dataSize=%u,createdBy=%u,modifiedBy=%u]", mEndpointId, credentialIndex, to_underlying(credential.credentialType), static_cast(credential.credentialData.size()), credential.createdBy, credential.lastModifiedBy); return true; } bool LockEndpoint::SetCredential(uint16_t credentialIndex, chip::FabricIndex creator, chip::FabricIndex modifier, DlCredentialStatus credentialStatus, CredentialTypeEnum credentialType, const chip::ByteSpan & credentialData) { ChipLogProgress( Zcl, "Lock App: LockEndpoint::SetCredential " "[endpoint=%d,credentialIndex=%u,credentialStatus=%u,credentialType=%u,credentialDataSize=%u,creator=%u,modifier=%u]", mEndpointId, credentialIndex, to_underlying(credentialStatus), to_underlying(credentialType), static_cast(credentialData.size()), creator, modifier); if (to_underlying(credentialType) >= mLockCredentials.capacity()) { ChipLogError(Zcl, "Cannot set the credential - type out of range [endpoint=%d,type=%d]", mEndpointId, to_underlying(credentialType)); return false; } if (credentialIndex >= mLockCredentials.at(to_underlying(credentialType)).size() || (0 == credentialIndex && CredentialTypeEnum::kProgrammingPIN != credentialType)) { ChipLogError(Zcl, "Cannot set the credential - index out of range [endpoint=%d,index=%d]", mEndpointId, credentialIndex); return false; } auto & credentialInStorage = mLockCredentials[to_underlying(credentialType)][credentialIndex]; if (credentialData.size() > DOOR_LOCK_CREDENTIAL_INFO_MAX_DATA_SIZE) { ChipLogError(Zcl, "Cannot get the credential - data size exceeds limit " "[endpoint=%d,index=%d,dataSize=%u,maxDataSize=%u]", mEndpointId, credentialIndex, static_cast(credentialData.size()), static_cast(DOOR_LOCK_CREDENTIAL_INFO_MAX_DATA_SIZE)); return false; } credentialInStorage.status = credentialStatus; credentialInStorage.credentialType = credentialType; credentialInStorage.createdBy = creator; credentialInStorage.modifiedBy = modifier; std::memcpy(credentialInStorage.credentialData, credentialData.data(), credentialData.size()); credentialInStorage.credentialDataSize = credentialData.size(); ChipLogProgress(Zcl, "Successfully set the credential [mEndpointId=%d,index=%d,credentialType=%u,creator=%u,modifier=%u]", mEndpointId, credentialIndex, to_underlying(credentialType), credentialInStorage.createdBy, credentialInStorage.modifiedBy); return true; } DlStatus LockEndpoint::GetSchedule(uint8_t weekDayIndex, uint16_t userIndex, EmberAfPluginDoorLockWeekDaySchedule & schedule) { if (0 == userIndex || userIndex > mWeekDaySchedules.size()) { return DlStatus::kFailure; } if (0 == weekDayIndex || weekDayIndex > mWeekDaySchedules.at(userIndex - 1).size()) { return DlStatus::kFailure; } const auto & scheduleInStorage = mWeekDaySchedules.at(userIndex - 1).at(weekDayIndex - 1); if (DlScheduleStatus::kAvailable == scheduleInStorage.status) { return DlStatus::kNotFound; } schedule = scheduleInStorage.schedule; return DlStatus::kSuccess; } DlStatus LockEndpoint::SetSchedule(uint8_t weekDayIndex, uint16_t userIndex, DlScheduleStatus status, DaysMaskMap daysMask, uint8_t startHour, uint8_t startMinute, uint8_t endHour, uint8_t endMinute) { if (0 == userIndex || userIndex > mWeekDaySchedules.size()) { return DlStatus::kFailure; } if (0 == weekDayIndex || weekDayIndex > mWeekDaySchedules.at(userIndex - 1).size()) { return DlStatus::kFailure; } auto & scheduleInStorage = mWeekDaySchedules.at(userIndex - 1).at(weekDayIndex - 1); scheduleInStorage.schedule.daysMask = daysMask; scheduleInStorage.schedule.startHour = startHour; scheduleInStorage.schedule.startMinute = startMinute; scheduleInStorage.schedule.endHour = endHour; scheduleInStorage.schedule.endMinute = endMinute; scheduleInStorage.status = status; return DlStatus::kSuccess; } DlStatus LockEndpoint::GetSchedule(uint8_t yearDayIndex, uint16_t userIndex, EmberAfPluginDoorLockYearDaySchedule & schedule) { if (0 == userIndex || userIndex > mYearDaySchedules.size()) { return DlStatus::kFailure; } if (0 == yearDayIndex || yearDayIndex > mYearDaySchedules.at(userIndex - 1).size()) { return DlStatus::kFailure; } const auto & scheduleInStorage = mYearDaySchedules.at(userIndex - 1).at(yearDayIndex - 1); if (DlScheduleStatus::kAvailable == scheduleInStorage.status) { return DlStatus::kNotFound; } schedule = scheduleInStorage.schedule; return DlStatus::kSuccess; } DlStatus LockEndpoint::SetSchedule(uint8_t yearDayIndex, uint16_t userIndex, DlScheduleStatus status, uint32_t localStartTime, uint32_t localEndTime) { if (0 == userIndex || userIndex > mYearDaySchedules.size()) { return DlStatus::kFailure; } if (0 == yearDayIndex || yearDayIndex > mYearDaySchedules.at(userIndex - 1).size()) { return DlStatus::kFailure; } auto & scheduleInStorage = mYearDaySchedules.at(userIndex - 1).at(yearDayIndex - 1); scheduleInStorage.schedule.localStartTime = localStartTime; scheduleInStorage.schedule.localEndTime = localEndTime; scheduleInStorage.status = status; return DlStatus::kSuccess; } DlStatus LockEndpoint::GetSchedule(uint8_t holidayIndex, EmberAfPluginDoorLockHolidaySchedule & schedule) { if (0 == holidayIndex || holidayIndex > mHolidaySchedules.size()) { return DlStatus::kFailure; } const auto & scheduleInStorage = mHolidaySchedules[holidayIndex - 1]; if (DlScheduleStatus::kAvailable == scheduleInStorage.status) { return DlStatus::kNotFound; } schedule = scheduleInStorage.schedule; return DlStatus::kSuccess; } DlStatus LockEndpoint::SetSchedule(uint8_t holidayIndex, DlScheduleStatus status, uint32_t localStartTime, uint32_t localEndTime, OperatingModeEnum operatingMode) { if (0 == holidayIndex || holidayIndex > mHolidaySchedules.size()) { return DlStatus::kFailure; } auto & scheduleInStorage = mHolidaySchedules[holidayIndex - 1]; scheduleInStorage.schedule.localStartTime = localStartTime; scheduleInStorage.schedule.localEndTime = localEndTime; scheduleInStorage.schedule.operatingMode = operatingMode; scheduleInStorage.status = status; return DlStatus::kSuccess; } CHIP_ERROR LockEndpoint::GetAliroReaderVerificationKey(MutableByteSpan & verificationKey) { if (!mAliroStateInitialized) { verificationKey.reduce_size(0); return CHIP_NO_ERROR; } return chip::CopySpanToMutableSpan(ByteSpan(mAliroReaderVerificationKey), verificationKey); } CHIP_ERROR LockEndpoint::GetAliroReaderGroupIdentifier(MutableByteSpan & groupIdentifier) { if (!mAliroStateInitialized) { groupIdentifier.reduce_size(0); return CHIP_NO_ERROR; } return CopySpanToMutableSpan(ByteSpan(mAliroReaderGroupIdentifier), groupIdentifier); } CHIP_ERROR LockEndpoint::GetAliroReaderGroupSubIdentifier(MutableByteSpan & groupSubIdentifier) { return CopySpanToMutableSpan(ByteSpan(mAliroReaderGroupSubIdentifier), groupSubIdentifier); } namespace { CHIP_ERROR CopyProtocolVersionIntoSpan(uint16_t protocolVersionValue, MutableByteSpan & protocolVersion) { using namespace chip::app::Clusters::DoorLock; static_assert(sizeof(protocolVersionValue) == kAliroProtocolVersionSize); if (protocolVersion.size() < kAliroProtocolVersionSize) { return CHIP_ERROR_INVALID_ARGUMENT; } // Per Aliro spec, protocol version encoding is big-endian chip::Encoding::BigEndian::Put16(protocolVersion.data(), protocolVersionValue); protocolVersion.reduce_size(kAliroProtocolVersionSize); return CHIP_NO_ERROR; } } // anonymous namespace CHIP_ERROR LockEndpoint::GetAliroExpeditedTransactionSupportedProtocolVersionAtIndex(size_t index, MutableByteSpan & protocolVersion) { // Only claim support for the one known protocol version for now: 0x0100. constexpr uint16_t knownProtocolVersion = 0x0100; if (index > 0) { return CHIP_ERROR_PROVIDER_LIST_EXHAUSTED; } return CopyProtocolVersionIntoSpan(knownProtocolVersion, protocolVersion); } CHIP_ERROR LockEndpoint::GetAliroGroupResolvingKey(MutableByteSpan & groupResolvingKey) { if (!mAliroStateInitialized) { groupResolvingKey.reduce_size(0); return CHIP_NO_ERROR; } return CopySpanToMutableSpan(ByteSpan(mAliroGroupResolvingKey), groupResolvingKey); } CHIP_ERROR LockEndpoint::GetAliroSupportedBLEUWBProtocolVersionAtIndex(size_t index, MutableByteSpan & protocolVersion) { // Only claim support for the one known protocol version for now: 0x0100. constexpr uint16_t knownProtocolVersion = 0x0100; if (index > 0) { return CHIP_ERROR_PROVIDER_LIST_EXHAUSTED; } return CopyProtocolVersionIntoSpan(knownProtocolVersion, protocolVersion); } uint8_t LockEndpoint::GetAliroBLEAdvertisingVersion() { // For now the only define value of the BLE advertising version for Aliro is 0. return 0; } uint16_t LockEndpoint::GetNumberOfAliroCredentialIssuerKeysSupported() { using namespace chip::app::Clusters::DoorLock; // Our vector has an extra entry at index 0 that is not a valid entry, so // the actual number of credentials supported is one length than the length. return static_cast(mLockCredentials.at(to_underlying(CredentialTypeEnum::kAliroCredentialIssuerKey)).size() - 1); } uint16_t LockEndpoint::GetNumberOfAliroEndpointKeysSupported() { using namespace chip::app::Clusters::DoorLock; // Our vector has an extra entry at index 0 that is not a valid entry, so // the actual number of credentials supported is one length than the length. // // Also, our arrays are the same size, so we just return the size of one of // the arrays: that is the cap on the total number of endpoint keys // supported, which can be of either type. return static_cast(mLockCredentials.at(to_underlying(CredentialTypeEnum::kAliroEvictableEndpointKey)).size() - 1); } CHIP_ERROR LockEndpoint::SetAliroReaderConfig(const ByteSpan & signingKey, const ByteSpan & verificationKey, const ByteSpan & groupIdentifier, const Optional & groupResolvingKey) { // We ignore the signing key, since we never do anything with it. VerifyOrReturnError(verificationKey.size() == sizeof(mAliroReaderVerificationKey), CHIP_ERROR_INVALID_ARGUMENT); memcpy(mAliroReaderVerificationKey, verificationKey.data(), sizeof(mAliroReaderVerificationKey)); VerifyOrReturnError(groupIdentifier.size() == sizeof(mAliroReaderGroupIdentifier), CHIP_ERROR_INVALID_ARGUMENT); memcpy(mAliroReaderGroupIdentifier, groupIdentifier.data(), sizeof(mAliroReaderGroupIdentifier)); if (groupResolvingKey.HasValue()) { VerifyOrReturnError(groupResolvingKey.Value().size() == sizeof(mAliroGroupResolvingKey), CHIP_ERROR_INVALID_ARGUMENT); memcpy(mAliroGroupResolvingKey, groupResolvingKey.Value().data(), sizeof(mAliroGroupResolvingKey)); } mAliroStateInitialized = true; return CHIP_NO_ERROR; } CHIP_ERROR LockEndpoint::ClearAliroReaderConfig() { // A real implementation would clear out key data from the other parts of // the application that might use it. mAliroStateInitialized = false; return CHIP_NO_ERROR; } bool LockEndpoint::setLockState(const Nullable & fabricIdx, const Nullable & nodeId, DlLockState lockState, const Optional & pin, OperationErrorEnum & err, OperationSourceEnum opSource) { // Assume pin is required until told otherwise bool requirePin = true; chip::app::Clusters::DoorLock::Attributes::RequirePINforRemoteOperation::Get(mEndpointId, &requirePin); // If a pin code is not given if (!pin.HasValue()) { ChipLogDetail(Zcl, "Door Lock App: PIN code is not specified [endpointId=%d]", mEndpointId); // If a pin code is not required if (!requirePin) { ChipLogProgress(Zcl, "Door Lock App: setting door lock state to \"%s\" [endpointId=%d]", lockStateToString(lockState), mEndpointId); if (gCurrentAction.moving == true) { ChipLogProgress(Zcl, "Lock App: not executing lock action as another lock action is already active [endpointId=%d]", mEndpointId); return false; } gCurrentAction.moving = true; gCurrentAction.endpointId = mEndpointId; gCurrentAction.lockState = lockState; gCurrentAction.opSource = opSource; gCurrentAction.userIndex = NullNullable; gCurrentAction.fabricIdx = fabricIdx; gCurrentAction.nodeId = nodeId; // Do this async as a real lock would do too but use 0s delay to speed up CI tests chip::DeviceLayer::SystemLayer().StartTimer(chip::System::Clock::Seconds16(0), OnLockActionCompleteCallback, nullptr); return true; } ChipLogError(Zcl, "Door Lock App: PIN code is not specified, but it is required [endpointId=%d]", mEndpointId); err = OperationErrorEnum::kInvalidCredential; return false; } // Find the credential so we can make sure it is not absent right away auto & pinCredentials = mLockCredentials[to_underlying(CredentialTypeEnum::kPin)]; auto credential = std::find_if(pinCredentials.begin(), pinCredentials.end(), [&pin](const LockCredentialInfo & c) { return (c.status != DlCredentialStatus::kAvailable) && chip::ByteSpan{ c.credentialData, c.credentialDataSize }.data_equal(pin.Value()); }); if (credential == pinCredentials.end()) { ChipLogDetail(Zcl, "Lock App: specified PIN code was not found in the database, ignoring command to set lock state to \"%s\" " "[endpointId=%d]", lockStateToString(lockState), mEndpointId); err = OperationErrorEnum::kInvalidCredential; return false; } // Find a user that correspond to this credential auto credentialIndex = static_cast(credential - pinCredentials.begin()); auto user = std::find_if(mLockUsers.begin(), mLockUsers.end(), [credential, credentialIndex](const LockUserInfo & u) { return std::any_of(u.credentials.begin(), u.credentials.end(), [&credential, credentialIndex](const CredentialStruct & c) { return c.credentialIndex == credentialIndex && c.credentialType == credential->credentialType; }); }); if (user == mLockUsers.end()) { ChipLogDetail(Zcl, "Lock App: specified PIN code was found in the database, but the lock user is not associated with it " "[endpointId=%d,credentialIndex=%u]", mEndpointId, credentialIndex); } auto userIndex = static_cast(user - mLockUsers.begin()); // Check if schedules affect the user bool haveWeekDaySchedules = false; bool haveYearDaySchedules = false; if (weekDayScheduleForbidsAccess(userIndex, &haveWeekDaySchedules) || yearDayScheduleForbidsAccess(userIndex, &haveYearDaySchedules) || // Also disallow access for a user that's supposed to have _some_ // schedule but doesn't have any (user->userType == UserTypeEnum::kScheduleRestrictedUser && !haveWeekDaySchedules && !haveYearDaySchedules)) { ChipLogDetail(Zcl, "Lock App: associated user is not allowed to operate the lock due to schedules" "[endpointId=%d,userIndex=%u]", mEndpointId, userIndex); err = OperationErrorEnum::kRestricted; return false; } ChipLogProgress( Zcl, "Lock App: specified PIN code was found in the database, setting door lock state to \"%s\" [endpointId=%d,userIndex=%u]", lockStateToString(lockState), mEndpointId, userIndex); if (gCurrentAction.moving == true) { ChipLogProgress(Zcl, "Lock App: not executing lock action as another lock action is already active [endpointId=%d,userIndex=%u]", mEndpointId, userIndex); return false; } gCurrentAction.moving = true; gCurrentAction.endpointId = mEndpointId; gCurrentAction.lockState = lockState; gCurrentAction.opSource = opSource; gCurrentAction.userIndex = MakeNullable(static_cast(userIndex + 1)); gCurrentAction.credentialIndex = static_cast(credentialIndex); gCurrentAction.fabricIdx = fabricIdx; gCurrentAction.nodeId = nodeId; // Do this async as a real lock would do too but use 0s delay to speed up CI tests chip::DeviceLayer::SystemLayer().StartTimer(chip::System::Clock::Seconds16(0), OnLockActionCompleteCallback, nullptr); return true; } void LockEndpoint::OnLockActionCompleteCallback(chip::System::Layer *, void * callbackContext) { if (gCurrentAction.userIndex.IsNull()) { DoorLockServer::Instance().SetLockState(gCurrentAction.endpointId, gCurrentAction.lockState, gCurrentAction.opSource, NullNullable, NullNullable, gCurrentAction.fabricIdx, gCurrentAction.nodeId); } else { LockOpCredentials userCredential[] = { { CredentialTypeEnum::kPin, gCurrentAction.credentialIndex } }; auto userCredentials = MakeNullable>(userCredential); DoorLockServer::Instance().SetLockState(gCurrentAction.endpointId, gCurrentAction.lockState, gCurrentAction.opSource, gCurrentAction.userIndex, userCredentials, gCurrentAction.fabricIdx, gCurrentAction.nodeId); } // move back to Unlocked after Unlatch if (gCurrentAction.lockState == DlLockState::kUnlatched) { gCurrentAction.lockState = DlLockState::kUnlocked; // Do this async as a real lock would do too but use 0s delay to speed up CI tests chip::DeviceLayer::SystemLayer().StartTimer(chip::System::Clock::Seconds16(0), OnLockActionCompleteCallback, nullptr); } else { gCurrentAction.moving = false; } } bool LockEndpoint::weekDayScheduleForbidsAccess(uint16_t userIndex, bool * haveSchedule) const { *haveSchedule = std::any_of(mWeekDaySchedules[userIndex].begin(), mWeekDaySchedules[userIndex].end(), [](const WeekDaysScheduleInfo & s) { return s.status == DlScheduleStatus::kOccupied; }); const auto & user = mLockUsers[userIndex]; if (user.userType != UserTypeEnum::kScheduleRestrictedUser && user.userType != UserTypeEnum::kWeekDayScheduleUser) { // Weekday schedules don't apply to this user. return false; } if (user.userType == UserTypeEnum::kScheduleRestrictedUser && !*haveSchedule) { // It's valid to not have any schedules of a given type; on its own this // does not prevent access. return false; } chip::System::Clock::Milliseconds64 cTMs; auto chipError = chip::System::SystemClock().GetClock_RealTimeMS(cTMs); if (chipError != CHIP_NO_ERROR) { ChipLogError(Zcl, "Lock App: unable to get current time to check user schedules [endpointId=%d,error=%d (%s)]", mEndpointId, chipError.AsInteger(), chipError.AsString()); return true; } time_t unixEpoch = std::chrono::duration_cast(cTMs).count(); tm calendarTime{}; localtime_r(&unixEpoch, &calendarTime); auto currentTime = calendarTime.tm_hour * chip::kSecondsPerHour + calendarTime.tm_min * chip::kSecondsPerMinute + calendarTime.tm_sec; // Now check whether any schedule allows the current time. If it does, // access is not forbidden. return !std::any_of( mWeekDaySchedules[userIndex].begin(), mWeekDaySchedules[userIndex].end(), [currentTime, calendarTime](const WeekDaysScheduleInfo & s) { auto startTime = s.schedule.startHour * chip::kSecondsPerHour + s.schedule.startMinute * chip::kSecondsPerMinute; auto endTime = s.schedule.endHour * chip::kSecondsPerHour + s.schedule.endMinute * chip::kSecondsPerMinute; return s.status == DlScheduleStatus::kOccupied && (to_underlying(s.schedule.daysMask) & (1 << calendarTime.tm_wday)) && startTime <= currentTime && currentTime <= endTime; }); } bool LockEndpoint::yearDayScheduleForbidsAccess(uint16_t userIndex, bool * haveSchedule) const { *haveSchedule = std::any_of(mYearDaySchedules[userIndex].begin(), mYearDaySchedules[userIndex].end(), [](const YearDayScheduleInfo & sch) { return sch.status == DlScheduleStatus::kOccupied; }); const auto & user = mLockUsers[userIndex]; if (user.userType != UserTypeEnum::kScheduleRestrictedUser && user.userType != UserTypeEnum::kYearDayScheduleUser) { return false; } if (user.userType == UserTypeEnum::kScheduleRestrictedUser && !*haveSchedule) { // It's valid to not have any schedules of a given type; on its own this // does not prevent access. return false; } chip::System::Clock::Milliseconds64 cTMs; auto chipError = chip::System::SystemClock().GetClock_RealTimeMS(cTMs); if (chipError != CHIP_NO_ERROR) { ChipLogError(Zcl, "Lock App: unable to get current time to check user schedules [endpointId=%d,error=%d (%s)]", mEndpointId, chipError.AsInteger(), chipError.AsString()); return true; } auto unixEpoch = std::chrono::duration_cast(cTMs).count(); uint32_t chipEpoch = 0; if (!chip::UnixEpochToChipEpochTime(unixEpoch, chipEpoch)) { ChipLogError(Zcl, "Lock App: unable to convert Unix Epoch time to Matter Epoch Time to check user schedules " "[endpointId=%d,userIndex=%d]", mEndpointId, userIndex); return false; } return !std::any_of(mYearDaySchedules[userIndex].begin(), mYearDaySchedules[userIndex].end(), [chipEpoch](const YearDayScheduleInfo & sch) { return sch.status == DlScheduleStatus::kOccupied && sch.schedule.localStartTime <= chipEpoch && chipEpoch <= sch.schedule.localEndTime; }); } const char * LockEndpoint::lockStateToString(DlLockState lockState) const { switch (lockState) { case DlLockState::kNotFullyLocked: return "Not Fully Locked"; case DlLockState::kLocked: return "Locked"; case DlLockState::kUnlocked: return "Unlocked"; case DlLockState::kUnlatched: return "Unlatched"; case DlLockState::kUnknownEnumValue: break; } return "Unknown"; }