| /** |
| * |
| * Copyright (c) 2020 Project CHIP Authors |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| // clusters specific header |
| #include "level-control.h" |
| |
| #include <algorithm> |
| |
| // this file contains all the common includes for clusters in the util |
| #include <app-common/zap-generated/attributes/Accessors.h> |
| #include <app-common/zap-generated/cluster-objects.h> |
| #include <app/CommandHandler.h> |
| #include <app/ConcreteCommandPath.h> |
| #include <app/cluster-building-blocks/QuieterReporting.h> |
| #include <app/util/attribute-storage.h> |
| #include <app/util/config.h> |
| #include <app/util/util.h> |
| |
| #include <app/reporting/reporting.h> |
| #include <lib/core/Optional.h> |
| #include <platform/CHIPDeviceConfig.h> |
| #include <platform/CHIPDeviceLayer.h> |
| #include <platform/PlatformManager.h> |
| #include <tracing/macros.h> |
| |
| #ifdef MATTER_DM_PLUGIN_SCENES_MANAGEMENT |
| #include <app/clusters/scenes-server/scenes-server.h> |
| #endif // MATTER_DM_PLUGIN_SCENES_MANAGEMENT |
| |
| #ifdef MATTER_DM_PLUGIN_ON_OFF |
| #include <app/clusters/on-off-server/on-off-server.h> |
| #endif // MATTER_DM_PLUGIN_ON_OFF |
| |
| #ifdef MATTER_DM_PLUGIN_COLOR_CONTROL_SERVER_TEMP |
| #include <app/clusters/color-control-server/color-control-server.h> |
| #endif // MATTER_DM_PLUGIN_COLOR_CONTROL_SERVER_TEMP |
| |
| #include <assert.h> |
| |
| using namespace chip; |
| using namespace chip::app; |
| using namespace chip::app::Clusters; |
| using namespace chip::app::Clusters::LevelControl; |
| using chip::Protocols::InteractionModel::Status; |
| |
| #ifndef IGNORE_LEVEL_CONTROL_CLUSTER_START_UP_CURRENT_LEVEL |
| static bool areStartUpLevelControlServerAttributesNonVolatile(EndpointId endpoint); |
| #endif // IGNORE_LEVEL_CONTROL_CLUSTER_START_UP_CURRENT_LEVEL |
| |
| #if (MATTER_DM_PLUGIN_LEVEL_CONTROL_RATE == 0) |
| #define FASTEST_TRANSITION_TIME_MS 0 |
| #else |
| #define FASTEST_TRANSITION_TIME_MS (MILLISECOND_TICKS_PER_SECOND / MATTER_DM_PLUGIN_LEVEL_CONTROL_RATE) |
| #endif // MATTER_DM_PLUGIN_LEVEL_CONTROL_RATE |
| |
| #define LEVEL_CONTROL_LIGHTING_MIN_LEVEL 0x01 |
| #define LEVEL_CONTROL_LIGHTING_MAX_LEVEL 0xFE |
| |
| #define INVALID_STORED_LEVEL 0xFFFF |
| |
| #define STARTUP_CURRENT_LEVEL_USE_DEVICE_MINIMUM 0x00 |
| #define STARTUP_CURRENT_LEVEL_USE_PREVIOUS_LEVEL 0xFF |
| |
| static constexpr size_t kLevelControlStateTableSize = |
| MATTER_DM_LEVEL_CONTROL_CLUSTER_SERVER_ENDPOINT_COUNT + CHIP_DEVICE_CONFIG_DYNAMIC_ENDPOINT_COUNT; |
| static_assert(kLevelControlStateTableSize <= kEmberInvalidEndpointIndex, "LevelControl state table size error"); |
| |
| struct CallbackScheduleState |
| { |
| System::Clock::Timestamp idealTimestamp; // The ideal time-stamp for the next callback to be scheduled. |
| System::Clock::Milliseconds32 runTime; // The duration of the previous scheduled callback function. |
| // e.g. running time of emberAfLevelControlClusterServerTickCallback |
| // when called consecutively |
| }; |
| |
| struct EmberAfLevelControlState |
| { |
| CommandId commandId; |
| uint8_t moveToLevel; |
| bool increasing; |
| uint8_t onLevel; |
| uint8_t minLevel; |
| uint8_t maxLevel; |
| uint16_t storedLevel; |
| uint32_t eventDurationMs; |
| uint32_t transitionTimeMs; |
| uint32_t elapsedTimeMs; |
| CallbackScheduleState callbackSchedule; |
| QuieterReportingAttribute<uint8_t> quietCurrentLevel{ DataModel::NullNullable }; |
| QuieterReportingAttribute<uint16_t> quietRemainingTime{ DataModel::MakeNullable<uint16_t>(0) }; |
| }; |
| |
| static EmberAfLevelControlState stateTable[kLevelControlStateTableSize]; |
| |
| static EmberAfLevelControlState * getState(EndpointId endpoint); |
| |
| static Status moveToLevelHandler(EndpointId endpoint, CommandId commandId, uint8_t level, |
| DataModel::Nullable<uint16_t> transitionTimeDs, chip::Optional<BitMask<OptionsBitmap>> optionsMask, |
| chip::Optional<BitMask<OptionsBitmap>> optionsOverride, uint16_t storedLevel); |
| static void moveHandler(CommandHandler * commandObj, const ConcreteCommandPath & commandPath, MoveModeEnum moveMode, |
| DataModel::Nullable<uint8_t> rate, chip::Optional<BitMask<OptionsBitmap>> optionsMask, |
| chip::Optional<BitMask<OptionsBitmap>> optionsOverride); |
| static void stepHandler(CommandHandler * commandObj, const ConcreteCommandPath & commandPath, StepModeEnum stepMode, |
| uint8_t stepSize, DataModel::Nullable<uint16_t> transitionTimeDs, |
| chip::Optional<BitMask<OptionsBitmap>> optionsMask, chip::Optional<BitMask<OptionsBitmap>> optionsOverride); |
| static void stopHandler(CommandHandler * commandObj, const ConcreteCommandPath & commandPath, |
| chip::Optional<BitMask<OptionsBitmap>> optionsMask, chip::Optional<BitMask<OptionsBitmap>> optionsOverride); |
| |
| static void setOnOffValue(EndpointId endpoint, bool onOff); |
| static void writeRemainingTime(EndpointId endpoint, uint16_t remainingTimeMs, bool isNewTransition = false); |
| static bool shouldExecuteIfOff(EndpointId endpoint, CommandId commandId, chip::Optional<chip::BitMask<OptionsBitmap>> optionsMask, |
| chip::Optional<chip::BitMask<OptionsBitmap>> optionsOverride); |
| |
| static Status SetCurrentLevelQuietReport(EndpointId endpoint, EmberAfLevelControlState * state, |
| DataModel::Nullable<uint8_t> newValue, bool isEndOfTransition); |
| |
| #if defined(MATTER_DM_PLUGIN_SCENES_MANAGEMENT) && CHIP_CONFIG_SCENES_USE_DEFAULT_HANDLERS |
| class DefaultLevelControlSceneHandler : public scenes::DefaultSceneHandlerImpl |
| { |
| public: |
| // As per spec, 2 attributes are scenable in the level control cluster |
| static constexpr uint8_t kLevelMaxScenableAttributes = 2; |
| |
| DefaultLevelControlSceneHandler() = default; |
| ~DefaultLevelControlSceneHandler() override {} |
| |
| // Default function for LevelControl cluster, only puts the LevelControl cluster ID in the span if supported on the caller |
| // endpoint |
| virtual void GetSupportedClusters(EndpointId endpoint, Span<ClusterId> & clusterBuffer) override |
| { |
| if (emberAfContainsServer(endpoint, LevelControl::Id) && clusterBuffer.size() >= 1) |
| { |
| clusterBuffer[0] = LevelControl::Id; |
| clusterBuffer.reduce_size(1); |
| } |
| else |
| { |
| clusterBuffer.reduce_size(0); |
| } |
| } |
| |
| // Default function for LevelControl cluster, only checks if LevelControl is enabled on the endpoint |
| bool SupportsCluster(EndpointId endpoint, ClusterId cluster) override |
| { |
| return (cluster == LevelControl::Id) && (emberAfContainsServer(endpoint, LevelControl::Id)); |
| } |
| |
| /// @brief Serialize the Cluster's EFS value |
| /// @param endpoint target endpoint |
| /// @param cluster target cluster |
| /// @param serializedBytes data to serialize into EFS |
| /// @return CHIP_NO_ERROR if successfully serialized the data, CHIP_ERROR_INVALID_ARGUMENT otherwise |
| CHIP_ERROR SerializeSave(EndpointId endpoint, ClusterId cluster, MutableByteSpan & serializedBytes) override |
| { |
| using AttributeValuePair = ScenesManagement::Structs::AttributeValuePairStruct::Type; |
| |
| DataModel::Nullable<uint8_t> level; |
| VerifyOrReturnError(Status::Success == Attributes::CurrentLevel::Get(endpoint, level), CHIP_ERROR_READ_FAILED); |
| |
| AttributeValuePair pairs[kLevelMaxScenableAttributes]; |
| |
| uint8_t maxLevel; |
| VerifyOrReturnError(Status::Success == Attributes::MaxLevel::Get(endpoint, &maxLevel), CHIP_ERROR_READ_FAILED); |
| |
| pairs[0].attributeID = Attributes::CurrentLevel::Id; |
| if (!level.IsNull()) |
| { |
| pairs[0].valueUnsigned8.SetValue(level.Value()); |
| } |
| else |
| { |
| pairs[0].valueUnsigned8.SetValue(NumericAttributeTraits<uint8_t>::kNullValue); |
| } |
| size_t attributeCount = 1; |
| if (LevelControlHasFeature(endpoint, LevelControl::Feature::kFrequency)) |
| { |
| uint16_t frequency; |
| VerifyOrReturnError(Status::Success == Attributes::CurrentFrequency::Get(endpoint, &frequency), CHIP_ERROR_READ_FAILED); |
| pairs[attributeCount].attributeID = Attributes::CurrentFrequency::Id; |
| pairs[attributeCount].valueUnsigned16.SetValue(frequency); |
| attributeCount++; |
| } |
| |
| DataModel::List<AttributeValuePair> attributeValueList(pairs, attributeCount); |
| |
| return EncodeAttributeValueList(attributeValueList, serializedBytes); |
| } |
| |
| /// @brief Default EFS interaction when applying scene to the OnOff Cluster |
| /// @param endpoint target endpoint |
| /// @param cluster target cluster |
| /// @param serializedBytes Data from nvm |
| /// @param timeMs transition time in ms |
| /// @return CHIP_NO_ERROR if value as expected, CHIP_ERROR_INVALID_ARGUMENT otherwise |
| CHIP_ERROR ApplyScene(EndpointId endpoint, ClusterId cluster, const ByteSpan & serializedBytes, |
| scenes::TransitionTimeMs timeMs) override |
| { |
| DataModel::DecodableList<ScenesManagement::Structs::AttributeValuePairStruct::DecodableType> attributeValueList; |
| |
| ReturnErrorOnFailure(DecodeAttributeValueList(serializedBytes, attributeValueList)); |
| |
| size_t attributeCount = 0; |
| ReturnErrorOnFailure(attributeValueList.ComputeSize(&attributeCount)); |
| VerifyOrReturnError(attributeCount <= kLevelMaxScenableAttributes, CHIP_ERROR_BUFFER_TOO_SMALL); |
| |
| auto pair_iterator = attributeValueList.begin(); |
| |
| // The level control cluster should have a maximum of 2 attributes |
| uint8_t level = 0; |
| // TODO : Uncomment when frequency is supported by the level control cluster |
| // uint16_t frequency; |
| while (pair_iterator.Next()) |
| { |
| auto & decodePair = pair_iterator.GetValue(); |
| |
| // If attribute ID was encoded, checks which attribute from LC cluster is there |
| switch (decodePair.attributeID) |
| { |
| case Attributes::CurrentLevel::Id: |
| VerifyOrReturnError(decodePair.valueUnsigned8.HasValue(), CHIP_ERROR_INVALID_ARGUMENT); |
| level = decodePair.valueUnsigned8.Value(); |
| break; |
| case Attributes::CurrentFrequency::Id: |
| // TODO : Uncomment when frequency is supported by the level control cluster |
| // VerifyOrReturnError(decodePair.valueUnsigned16.HasValue(), CHIP_ERROR_INVALID_ARGUMENT); |
| // frequency = decodePair.valueUnsigned16.Value(); |
| break; |
| default: |
| return CHIP_ERROR_INVALID_ARGUMENT; |
| } |
| } |
| ReturnErrorOnFailure(pair_iterator.GetStatus()); |
| |
| // TODO : Implement action on frequency when frequency not provisional anymore |
| // if(LevelControlHasFeature(endpoint, LevelControl::Feature::kFrequency)){} |
| |
| EmberAfLevelControlState * state = getState(endpoint); |
| if (level < state->minLevel || level > state->maxLevel) |
| { |
| NumericAttributeTraits<uint8_t>::SetNull(level); |
| } |
| |
| if (!NumericAttributeTraits<uint8_t>::IsNullValue(level)) |
| { |
| moveToLevelHandler( |
| endpoint, Commands::MoveToLevel::Id, level, DataModel::MakeNullable(static_cast<uint16_t>(timeMs / 100)), |
| chip::Optional<BitMask<OptionsBitmap>>(1), chip::Optional<BitMask<OptionsBitmap>>(1), INVALID_STORED_LEVEL); |
| } |
| |
| return CHIP_NO_ERROR; |
| } |
| }; |
| static DefaultLevelControlSceneHandler sLevelControlSceneHandler; |
| |
| #endif // defined(MATTER_DM_PLUGIN_SCENES_MANAGEMENT) && CHIP_CONFIG_SCENES_USE_DEFAULT_HANDLERS |
| |
| #if !defined(IGNORE_LEVEL_CONTROL_CLUSTER_OPTIONS) && defined(MATTER_DM_PLUGIN_COLOR_CONTROL_SERVER_TEMP) |
| static void reallyUpdateCoupledColorTemp(EndpointId endpoint); |
| #define updateCoupledColorTemp(endpoint) reallyUpdateCoupledColorTemp(endpoint) |
| #else |
| #define updateCoupledColorTemp(endpoint) |
| #endif // IGNORE_LEVEL_CONTROL_CLUSTER_OPTIONS && MATTER_DM_PLUGIN_COLOR_CONTROL_SERVER_TEMP |
| |
| void emberAfLevelControlClusterServerTickCallback(EndpointId endpoint); |
| |
| static void timerCallback(System::Layer *, void * callbackContext) |
| { |
| emberAfLevelControlClusterServerTickCallback(static_cast<EndpointId>(reinterpret_cast<uintptr_t>(callbackContext))); |
| } |
| |
| static uint32_t computeCallbackWaitTimeMs(CallbackScheduleState & callbackSchedule, uint32_t delayMs) |
| { |
| auto delay = System::Clock::Milliseconds32(delayMs); |
| auto waitTime = delay; |
| const auto currentTime = System::SystemClock().GetMonotonicTimestamp(); |
| |
| // Subsequent call |
| if (callbackSchedule.runTime.count()) |
| { |
| // Check whether the previous scheduled callback was late and whether its running time |
| // is smaller than the desired delay |
| // If the running time of the scheduled callback is greater than the desired delay |
| // then do nothing; do not flood the event loop if the device is not fast enough |
| if ((currentTime > callbackSchedule.idealTimestamp) && (callbackSchedule.runTime < delay)) |
| { |
| System::Clock::Timestamp latency = currentTime - callbackSchedule.idealTimestamp; |
| |
| if (latency >= delay) |
| { |
| waitTime = System::Clock::Milliseconds32(0); |
| } |
| else |
| { |
| waitTime -= latency; |
| } |
| } |
| } |
| // First-time call |
| else |
| { |
| // initialize idealTimestamp |
| callbackSchedule.idealTimestamp = currentTime; |
| } |
| |
| callbackSchedule.idealTimestamp += System::Clock::Milliseconds32(delayMs); |
| callbackSchedule.runTime = System::Clock::Milliseconds32(0); |
| |
| return waitTime.count(); |
| } |
| |
| static void scheduleTimerCallbackMs(EndpointId endpoint, uint32_t delayMs) |
| { |
| CHIP_ERROR err = DeviceLayer::SystemLayer().StartTimer(chip::System::Clock::Milliseconds32(delayMs), timerCallback, |
| reinterpret_cast<void *>(static_cast<uintptr_t>(endpoint))); |
| |
| if (err != CHIP_NO_ERROR) |
| { |
| ChipLogError(Zcl, "Level Control Server failed to schedule event: %" CHIP_ERROR_FORMAT, err.Format()); |
| } |
| } |
| |
| static void cancelEndpointTimerCallback(EndpointId endpoint) |
| { |
| DeviceLayer::SystemLayer().CancelTimer(timerCallback, reinterpret_cast<void *>(static_cast<uintptr_t>(endpoint))); |
| } |
| |
| static EmberAfLevelControlState * getState(EndpointId endpoint) |
| { |
| uint16_t ep = |
| emberAfGetClusterServerEndpointIndex(endpoint, LevelControl::Id, MATTER_DM_LEVEL_CONTROL_CLUSTER_SERVER_ENDPOINT_COUNT); |
| return (ep >= kLevelControlStateTableSize ? nullptr : &stateTable[ep]); |
| } |
| |
| #if !defined(IGNORE_LEVEL_CONTROL_CLUSTER_OPTIONS) && defined(MATTER_DM_PLUGIN_COLOR_CONTROL_SERVER_TEMP) |
| static void reallyUpdateCoupledColorTemp(EndpointId endpoint) |
| { |
| LevelControl::Attributes::Options::TypeInfo::Type options; |
| Status status = Attributes::Options::Get(endpoint, &options); |
| if (status != Status::Success) |
| { |
| ChipLogProgress(Zcl, "Unable to read Options attribute: 0x%X", to_underlying(status)); |
| return; |
| } |
| |
| if (emberAfContainsAttribute(endpoint, ColorControl::Id, ColorControl::Attributes::ColorTemperatureMireds::Id)) |
| { |
| if (options.Has(OptionsBitmap::kCoupleColorTempToLevel)) |
| { |
| emberAfPluginLevelControlCoupledColorTempChangeCallback(endpoint); |
| } |
| } |
| } |
| #endif // IGNORE_LEVEL_CONTROL_CLUSTER_OPTIONS && MATTER_DM_PLUGIN_COLOR_CONTROL_SERVER_TEMP |
| |
| /* |
| * @brief |
| * This function is used to update the current level attribute |
| * while respecting its defined quiet reporting quality: |
| * The attribute will be reported: |
| * - At most once per second, or |
| * - At the end of the movement/transition, or |
| * - When it changes from null to any other value and vice versa. |
| * |
| * @param endpoint: endpoint on which the currentLevel attribute must be updated. |
| * @param state: LevelControlState struct of this given endpoint. |
| * @param newValue: Value to update the attribute with |
| * @param isEndOfTransition: Boolean that indicate whether the update is occuring at the end of a level transition |
| * @return Success in setting the attribute value or the IM error code for the failure. |
| */ |
| static Status SetCurrentLevelQuietReport(EndpointId endpoint, EmberAfLevelControlState * state, |
| DataModel::Nullable<uint8_t> newValue, bool isEndOfTransition) |
| { |
| AttributeDirtyState dirtyState; |
| auto now = System::SystemClock().GetMonotonicTimestamp(); |
| |
| if (isEndOfTransition) |
| { |
| // At the start or end of the movement/transition we must report |
| auto predicate = [](const decltype(state->quietCurrentLevel)::SufficientChangePredicateCandidate &) -> bool { |
| return true; |
| }; |
| dirtyState = state->quietCurrentLevel.SetValue(newValue, now, predicate); |
| } |
| else |
| { |
| // During transtions, reports should be at most once per second |
| System::Clock::Milliseconds64 reportInterval = |
| std::max(System::Clock::Milliseconds64(1000), System::Clock::Milliseconds64(state->transitionTimeMs / 4)); |
| auto predicate = state->quietCurrentLevel.GetPredicateForSufficientTimeSinceLastDirty(reportInterval); |
| dirtyState = state->quietCurrentLevel.SetValue(newValue, now, predicate); |
| } |
| |
| MarkAttributeDirty markDirty = MarkAttributeDirty::kNo; |
| if (dirtyState == AttributeDirtyState::kMustReport) |
| { |
| markDirty = MarkAttributeDirty::kYes; |
| } |
| return Attributes::CurrentLevel::Set(endpoint, state->quietCurrentLevel.value(), markDirty); |
| } |
| |
| void emberAfLevelControlClusterServerTickCallback(EndpointId endpoint) |
| { |
| EmberAfLevelControlState * state = getState(endpoint); |
| Status status; |
| DataModel::Nullable<uint8_t> currentLevel; |
| const auto callbackStartTimestamp = System::SystemClock().GetMonotonicTimestamp(); |
| bool isTransitionStart = false; |
| bool isTransitionEnd = false; |
| |
| if (state == nullptr) |
| { |
| return; |
| } |
| |
| isTransitionStart = (state->elapsedTimeMs == 0); |
| state->elapsedTimeMs += state->eventDurationMs; |
| |
| // Read the attribute; print error message and return if it can't be read |
| status = LevelControl::Attributes::CurrentLevel::Get(endpoint, currentLevel); |
| |
| if (status != Status::Success || currentLevel.IsNull()) |
| { |
| ChipLogProgress(Zcl, "ERR: reading current level %x", to_underlying(status)); |
| state->callbackSchedule.runTime = System::Clock::Milliseconds32(0); |
| writeRemainingTime(endpoint, 0); |
| return; |
| } |
| |
| ChipLogDetail(Zcl, "Event: move from %d", currentLevel.Value()); |
| |
| // adjust by the proper amount, either up or down |
| if (state->transitionTimeMs == 0) |
| { |
| // Immediate, not over a time interval. |
| currentLevel.SetNonNull(state->moveToLevel); |
| } |
| else if (state->increasing) |
| { |
| assert(currentLevel.Value() < state->maxLevel); |
| assert(currentLevel.Value() < state->moveToLevel); |
| currentLevel.SetNonNull(static_cast<uint8_t>(currentLevel.Value() + 1)); |
| } |
| else |
| { |
| assert(state->minLevel < currentLevel.Value()); |
| assert(state->moveToLevel < currentLevel.Value()); |
| currentLevel.SetNonNull(static_cast<uint8_t>(currentLevel.Value() - 1)); |
| } |
| |
| ChipLogDetail(Zcl, " to %d ", currentLevel.Value()); |
| ChipLogDetail(Zcl, "(diff %c1)", state->increasing ? '+' : '-'); |
| |
| // Are we at the requested level? |
| isTransitionEnd = (currentLevel.Value() == state->moveToLevel); |
| status = SetCurrentLevelQuietReport(endpoint, state, currentLevel, isTransitionEnd); |
| if (status != Status::Success) |
| { |
| ChipLogProgress(Zcl, "ERR: writing current level %x", to_underlying(status)); |
| state->callbackSchedule.runTime = System::Clock::Milliseconds32(0); |
| writeRemainingTime(endpoint, 0); |
| return; |
| } |
| |
| updateCoupledColorTemp(endpoint); |
| |
| if (isTransitionEnd) |
| { |
| if (state->commandId == Commands::MoveToLevelWithOnOff::Id || state->commandId == Commands::MoveWithOnOff::Id || |
| state->commandId == Commands::StepWithOnOff::Id) |
| { |
| setOnOffValue(endpoint, (currentLevel.Value() != state->minLevel)); |
| } |
| |
| if (state->storedLevel != INVALID_STORED_LEVEL) |
| { |
| uint8_t storedLevel8u = (uint8_t) state->storedLevel; |
| status = Attributes::CurrentLevel::Set(endpoint, storedLevel8u); |
| if (status != Status::Success) |
| { |
| ChipLogProgress(Zcl, "ERR: writing current level %x", to_underlying(status)); |
| } |
| else |
| { |
| updateCoupledColorTemp(endpoint); |
| } |
| } |
| |
| state->callbackSchedule.runTime = System::Clock::Milliseconds32(0); |
| writeRemainingTime(endpoint, 0); |
| } |
| else |
| { |
| state->callbackSchedule.runTime = System::SystemClock().GetMonotonicTimestamp() - callbackStartTimestamp; |
| writeRemainingTime(endpoint, static_cast<uint16_t>(state->transitionTimeMs - state->elapsedTimeMs), isTransitionStart); |
| scheduleTimerCallbackMs(endpoint, computeCallbackWaitTimeMs(state->callbackSchedule, state->eventDurationMs)); |
| } |
| } |
| |
| static void writeRemainingTime(EndpointId endpoint, uint16_t remainingTimeMs, bool isNewTransition) |
| { |
| #ifndef IGNORE_LEVEL_CONTROL_CLUSTER_LEVEL_CONTROL_REMAINING_TIME |
| if (emberAfContainsAttribute(endpoint, LevelControl::Id, LevelControl::Attributes::RemainingTime::Id)) |
| { |
| // Convert milliseconds to tenths of a second, rounding any fractional value |
| // up to the nearest whole value. This means: |
| // |
| // 0 ms = 0.00 ds = 0 ds |
| // 1 ms = 0.01 ds = 1 ds |
| // ... |
| // 100 ms = 1.00 ds = 1 ds |
| // 101 ms = 1.01 ds = 2 ds |
| // ... |
| // 200 ms = 2.00 ds = 2 ds |
| // 201 ms = 2.01 ds = 3 ds |
| // ... |
| // |
| // This is done to ensure that the attribute, in tenths of a second, only |
| // goes to zero when the remaining time in milliseconds is actually zero. |
| auto markDirty = MarkAttributeDirty::kNo; |
| auto state = getState(endpoint); |
| auto now = System::SystemClock().GetMonotonicTimestamp(); |
| uint16_t remainingTimeDs = static_cast<uint16_t>((remainingTimeMs + 99) / 100); |
| uint16_t lastRemainingTime = state->quietRemainingTime.value().ValueOr(0); |
| |
| // RemainingTime Quiet report conditions: |
| // - When it changes to 0, or |
| // - When it changes from 0 to any value higher than 10, or |
| // - When it changes, with a delta larger than 10, caused by the invoke of a command. |
| auto predicate = [isNewTransition, lastRemainingTime]( |
| const decltype(state->quietRemainingTime)::SufficientChangePredicateCandidate & candidate) -> bool { |
| constexpr uint16_t reportDelta = 10; |
| bool isDirty = false; |
| if (candidate.newValue.Value() == 0 || |
| (candidate.lastDirtyValue.Value() == 0 && candidate.newValue.Value() > reportDelta)) |
| { |
| isDirty = true; |
| } |
| else if (isNewTransition && |
| (candidate.newValue.Value() > static_cast<uint32_t>(lastRemainingTime + reportDelta) || |
| static_cast<uint32_t>(candidate.newValue.Value() + reportDelta) < lastRemainingTime || |
| candidate.newValue.Value() > static_cast<uint32_t>(candidate.lastDirtyValue.Value() + reportDelta))) |
| { |
| isDirty = true; |
| } |
| return isDirty; |
| }; |
| |
| if (state->quietRemainingTime.SetValue(remainingTimeDs, now, predicate) == AttributeDirtyState::kMustReport) |
| { |
| markDirty = MarkAttributeDirty::kYes; |
| } |
| |
| Attributes::RemainingTime::Set(endpoint, state->quietRemainingTime.value().Value(), markDirty); |
| } |
| #endif // IGNORE_LEVEL_CONTROL_CLUSTER_LEVEL_CONTROL_REMAINING_TIME |
| } |
| |
| static void setOnOffValue(EndpointId endpoint, bool onOff) |
| { |
| #ifdef MATTER_DM_PLUGIN_ON_OFF |
| if (emberAfContainsServer(endpoint, OnOff::Id)) |
| { |
| ChipLogProgress(Zcl, "Setting on/off to %s due to level change", onOff ? "ON" : "OFF"); |
| OnOffServer::Instance().setOnOffValue(endpoint, (onOff ? OnOff::Commands::On::Id : OnOff::Commands::Off::Id), true); |
| } |
| #endif // MATTER_DM_PLUGIN_ON_OFF |
| } |
| |
| static bool shouldExecuteIfOff(EndpointId endpoint, CommandId commandId, chip::Optional<chip::BitMask<OptionsBitmap>> optionsMask, |
| chip::Optional<chip::BitMask<OptionsBitmap>> optionsOverride) |
| { |
| #ifndef IGNORE_LEVEL_CONTROL_CLUSTER_OPTIONS |
| if (emberAfContainsAttribute(endpoint, LevelControl::Id, Attributes::Options::Id)) |
| { |
| // From 3.10.2.2.8.1 of ZCL7 document 14-0127-20j-zcl-ch-3-general.docx: |
| // "Command execution SHALL NOT continue beyond the Options processing if |
| // all of these criteria are true: |
| // - The command is one of the ‘without On/Off’ commands: Move, Move to |
| // Level, Stop, or Step. |
| // - The On/Off cluster exists on the same endpoint as this cluster. |
| // - The OnOff attribute of the On/Off cluster, on this endpoint, is 0x00 |
| // (FALSE). |
| // - The value of the ExecuteIfOff bit is 0." |
| if (commandId > Commands::Stop::Id) |
| { |
| return true; |
| } |
| |
| if (!emberAfContainsServer(endpoint, OnOff::Id)) |
| { |
| return true; |
| } |
| |
| LevelControl::Attributes::Options::TypeInfo::Type options; |
| Status status = Attributes::Options::Get(endpoint, &options); |
| if (status != Status::Success) |
| { |
| ChipLogProgress(Zcl, "Unable to read Options attribute: 0x%X", to_underlying(status)); |
| // If we can't read the attribute, then we should just assume that it has its |
| // default value. |
| } |
| |
| bool on; |
| status = OnOff::Attributes::OnOff::Get(endpoint, &on); |
| if (status != Status::Success) |
| { |
| ChipLogProgress(Zcl, "Unable to read OnOff attribute: 0x%X", to_underlying(status)); |
| return true; |
| } |
| // The device is on - hence ExecuteIfOff does not matter |
| if (on) |
| { |
| return true; |
| } |
| // The OptionsMask & OptionsOverride fields SHALL both be present or both |
| // omitted in the command. A temporary Options bitmap SHALL be created from |
| // the Options attribute, using the OptionsMask & OptionsOverride fields, if |
| // present. Each bit of the temporary Options bitmap SHALL be determined as |
| // follows: |
| // Each bit in the Options attribute SHALL determine the corresponding bit in |
| // the temporary Options bitmap, unless the OptionsMask field is present and |
| // has the corresponding bit set to 1, in which case the corresponding bit in |
| // the OptionsOverride field SHALL determine the corresponding bit in the |
| // temporary Options bitmap. |
| // The resulting temporary Options bitmap SHALL then be processed as defined |
| // in section 3.10.2.2.3. |
| |
| // ---------- The following order is important in decision making ------- |
| // -----------more readable ---------- |
| // |
| if (!optionsMask.HasValue() || !optionsOverride.HasValue()) |
| { |
| // in case optionMask or optionOverride is not set, use of option |
| // attribute to decide execution of the command |
| return options.Has(OptionsBitmap::kExecuteIfOff); |
| } |
| // ---------- The above is to distinguish if the payload is present or not |
| |
| if (optionsMask.Value().Has(OptionsBitmap::kExecuteIfOff)) |
| { |
| // Mask is present and set in the command payload, this indicates |
| // use the over ride as temporary option |
| return optionsOverride.Value().Has(OptionsBitmap::kExecuteIfOff); |
| } |
| // if we are here - use the option bits |
| return options.Has(OptionsBitmap::kExecuteIfOff); |
| } |
| |
| #endif // IGNORE_LEVEL_CONTROL_CLUSTER_OPTIONS |
| // By default, we return true to continue supporting backwards compatibility. |
| return true; |
| } |
| |
| bool emberAfLevelControlClusterMoveToLevelCallback(CommandHandler * commandObj, const ConcreteCommandPath & commandPath, |
| const Commands::MoveToLevel::DecodableType & commandData) |
| { |
| MATTER_TRACE_SCOPE("MoveToLevel", "LevelControl"); |
| commandObj->AddStatus(commandPath, LevelControlServer::MoveToLevel(commandPath.mEndpointId, commandData)); |
| return true; |
| } |
| |
| namespace LevelControlServer { |
| |
| Status MoveToLevel(EndpointId endpointId, const Commands::MoveToLevel::DecodableType & commandData) |
| { |
| auto & level = commandData.level; |
| auto & transitionTime = commandData.transitionTime; |
| auto & optionsMask = commandData.optionsMask; |
| auto & optionsOverride = commandData.optionsOverride; |
| |
| if (transitionTime.IsNull()) |
| { |
| ChipLogProgress(Zcl, "%s MOVE_TO_LEVEL %x null %x %x", "RX level-control:", level, optionsMask.Raw(), |
| optionsOverride.Raw()); |
| } |
| else |
| { |
| ChipLogProgress(Zcl, "%s MOVE_TO_LEVEL %x %2x %x %x", "RX level-control:", level, transitionTime.Value(), optionsMask.Raw(), |
| optionsOverride.Raw()); |
| } |
| |
| return moveToLevelHandler(endpointId, Commands::MoveToLevel::Id, level, transitionTime, |
| Optional<BitMask<OptionsBitmap>>(optionsMask), Optional<BitMask<OptionsBitmap>>(optionsOverride), |
| INVALID_STORED_LEVEL); // Don't revert to the stored level |
| } |
| |
| #ifdef MATTER_DM_PLUGIN_SCENES_MANAGEMENT |
| chip::scenes::SceneHandler * GetSceneHandler() |
| { |
| #if CHIP_CONFIG_SCENES_USE_DEFAULT_HANDLERS |
| return &sLevelControlSceneHandler; |
| #else |
| return nullptr; |
| #endif // CHIP_CONFIG_SCENES_USE_DEFAULT_HANDLERS |
| } |
| #endif // ifdef MATTER_DM_PLUGIN_SCENES_MANAGEMENT |
| |
| } // namespace LevelControlServer |
| |
| bool emberAfLevelControlClusterMoveToLevelWithOnOffCallback(CommandHandler * commandObj, const ConcreteCommandPath & commandPath, |
| const Commands::MoveToLevelWithOnOff::DecodableType & commandData) |
| { |
| MATTER_TRACE_SCOPE("MoveToLevelWithOnOff", "LevelControl"); |
| auto & level = commandData.level; |
| auto & transitionTime = commandData.transitionTime; |
| auto & optionsMask = commandData.optionsMask; |
| auto & optionsOverride = commandData.optionsOverride; |
| |
| if (transitionTime.IsNull()) |
| { |
| ChipLogProgress(Zcl, "%s MOVE_TO_LEVEL_WITH_ON_OFF %x null %x %x", "RX level-control:", level, optionsMask.Raw(), |
| optionsOverride.Raw()); |
| } |
| else |
| { |
| ChipLogProgress(Zcl, "%s MOVE_TO_LEVEL_WITH_ON_OFF %x %2x %x %x", "RX level-control:", level, transitionTime.Value(), |
| optionsMask.Raw(), optionsOverride.Raw()); |
| } |
| |
| Status status = |
| moveToLevelHandler(commandPath.mEndpointId, Commands::MoveToLevelWithOnOff::Id, level, transitionTime, |
| Optional<BitMask<OptionsBitmap>>(optionsMask), Optional<BitMask<OptionsBitmap>>(optionsOverride), |
| INVALID_STORED_LEVEL); // Don't revert to the stored level |
| |
| commandObj->AddStatus(commandPath, status); |
| |
| return true; |
| } |
| |
| bool emberAfLevelControlClusterMoveCallback(CommandHandler * commandObj, const ConcreteCommandPath & commandPath, |
| const Commands::Move::DecodableType & commandData) |
| { |
| MATTER_TRACE_SCOPE("Move", "LevelControl"); |
| auto & moveMode = commandData.moveMode; |
| auto & rate = commandData.rate; |
| auto & optionsMask = commandData.optionsMask; |
| auto & optionsOverride = commandData.optionsOverride; |
| |
| if (rate.IsNull()) |
| { |
| ChipLogProgress(Zcl, "%s MOVE %x null %x %x", "RX level-control:", to_underlying(moveMode), optionsMask.Raw(), |
| optionsOverride.Raw()); |
| } |
| else |
| { |
| ChipLogProgress(Zcl, "%s MOVE %x %u %x %x", "RX level-control:", to_underlying(moveMode), rate.Value(), optionsMask.Raw(), |
| optionsOverride.Raw()); |
| } |
| |
| moveHandler(commandObj, commandPath, moveMode, rate, Optional<BitMask<OptionsBitmap>>(optionsMask), |
| Optional<BitMask<OptionsBitmap>>(optionsOverride)); |
| return true; |
| } |
| |
| bool emberAfLevelControlClusterMoveWithOnOffCallback(CommandHandler * commandObj, const ConcreteCommandPath & commandPath, |
| const Commands::MoveWithOnOff::DecodableType & commandData) |
| { |
| MATTER_TRACE_SCOPE("MoveWithOnOff", "LevelControl"); |
| auto & moveMode = commandData.moveMode; |
| auto & rate = commandData.rate; |
| auto & optionsMask = commandData.optionsMask; |
| auto & optionsOverride = commandData.optionsOverride; |
| |
| if (rate.IsNull()) |
| { |
| ChipLogProgress(Zcl, "%s MOVE_WITH_ON_OFF %x null %x %x", "RX level-control:", to_underlying(moveMode), optionsMask.Raw(), |
| optionsOverride.Raw()); |
| } |
| else |
| { |
| ChipLogProgress(Zcl, "%s MOVE_WITH_ON_OFF %u %2x %x %x", "RX level-control:", to_underlying(moveMode), rate.Value(), |
| optionsMask.Raw(), optionsOverride.Raw()); |
| } |
| |
| moveHandler(commandObj, commandPath, moveMode, rate, Optional<BitMask<OptionsBitmap>>(optionsMask), |
| Optional<BitMask<OptionsBitmap>>(optionsOverride)); |
| return true; |
| } |
| |
| bool emberAfLevelControlClusterStepCallback(CommandHandler * commandObj, const ConcreteCommandPath & commandPath, |
| const Commands::Step::DecodableType & commandData) |
| { |
| MATTER_TRACE_SCOPE("Step", "LevelControl"); |
| auto & stepMode = commandData.stepMode; |
| auto & stepSize = commandData.stepSize; |
| auto & transitionTime = commandData.transitionTime; |
| auto & optionsMask = commandData.optionsMask; |
| auto & optionsOverride = commandData.optionsOverride; |
| |
| if (transitionTime.IsNull()) |
| { |
| ChipLogProgress(Zcl, "%s STEP %x %x null %x %x", "RX level-control:", to_underlying(stepMode), stepSize, optionsMask.Raw(), |
| optionsOverride.Raw()); |
| } |
| else |
| { |
| ChipLogProgress(Zcl, "%s STEP %x %x %2x %x %x", "RX level-control:", to_underlying(stepMode), stepSize, |
| transitionTime.Value(), optionsMask.Raw(), optionsOverride.Raw()); |
| } |
| |
| stepHandler(commandObj, commandPath, stepMode, stepSize, transitionTime, Optional<BitMask<OptionsBitmap>>(optionsMask), |
| Optional<BitMask<OptionsBitmap>>(optionsOverride)); |
| return true; |
| } |
| |
| bool emberAfLevelControlClusterStepWithOnOffCallback(CommandHandler * commandObj, const ConcreteCommandPath & commandPath, |
| const Commands::StepWithOnOff::DecodableType & commandData) |
| { |
| MATTER_TRACE_SCOPE("StepWithOnOff", "LevelControl"); |
| auto & stepMode = commandData.stepMode; |
| auto & stepSize = commandData.stepSize; |
| auto & transitionTime = commandData.transitionTime; |
| auto & optionsMask = commandData.optionsMask; |
| auto & optionsOverride = commandData.optionsOverride; |
| |
| if (transitionTime.IsNull()) |
| { |
| ChipLogProgress(Zcl, "%s STEP_WITH_ON_OFF %x %x null %x %x", "RX level-control:", to_underlying(stepMode), stepSize, |
| optionsMask.Raw(), optionsOverride.Raw()); |
| } |
| else |
| { |
| ChipLogProgress(Zcl, "%s STEP_WITH_ON_OFF %x %x %2x %x %x", "RX level-control:", to_underlying(stepMode), stepSize, |
| transitionTime.Value(), optionsMask.Raw(), optionsOverride.Raw()); |
| } |
| |
| stepHandler(commandObj, commandPath, stepMode, stepSize, transitionTime, Optional<BitMask<OptionsBitmap>>(optionsMask), |
| Optional<BitMask<OptionsBitmap>>(optionsOverride)); |
| return true; |
| } |
| |
| bool emberAfLevelControlClusterStopCallback(CommandHandler * commandObj, const ConcreteCommandPath & commandPath, |
| const Commands::Stop::DecodableType & commandData) |
| { |
| MATTER_TRACE_SCOPE("Stop", "LevelControl"); |
| auto & optionsMask = commandData.optionsMask; |
| auto & optionsOverride = commandData.optionsOverride; |
| |
| ChipLogProgress(Zcl, "%s STOP", "RX level-control:"); |
| stopHandler(commandObj, commandPath, Optional<BitMask<OptionsBitmap>>(optionsMask), |
| Optional<BitMask<OptionsBitmap>>(optionsOverride)); |
| return true; |
| } |
| |
| bool emberAfLevelControlClusterStopWithOnOffCallback(CommandHandler * commandObj, const ConcreteCommandPath & commandPath, |
| const Commands::StopWithOnOff::DecodableType & commandData) |
| { |
| MATTER_TRACE_SCOPE("StopWithOnOff", "LevelControl"); |
| auto & optionsMask = commandData.optionsMask; |
| auto & optionsOverride = commandData.optionsOverride; |
| ChipLogProgress(Zcl, "%s STOP_WITH_ON_OFF", "RX level-control:"); |
| stopHandler(commandObj, commandPath, Optional<BitMask<OptionsBitmap>>(optionsMask), |
| Optional<BitMask<OptionsBitmap>>(optionsOverride)); |
| return true; |
| } |
| |
| static Status moveToLevelHandler(EndpointId endpoint, CommandId commandId, uint8_t level, |
| DataModel::Nullable<uint16_t> transitionTimeDs, chip::Optional<BitMask<OptionsBitmap>> optionsMask, |
| chip::Optional<BitMask<OptionsBitmap>> optionsOverride, uint16_t storedLevel) |
| { |
| EmberAfLevelControlState * state = getState(endpoint); |
| DataModel::Nullable<uint8_t> currentLevel; |
| uint8_t actualStepSize; |
| |
| if (state == nullptr) |
| { |
| return Status::Failure; |
| } |
| |
| if (level > MATTER_DM_PLUGIN_LEVEL_CONTROL_MAXIMUM_LEVEL) |
| { |
| return Status::InvalidCommand; |
| } |
| |
| if (!shouldExecuteIfOff(endpoint, commandId, optionsMask, optionsOverride)) |
| { |
| return Status::Success; |
| } |
| |
| // Cancel any currently active command before fiddling with the state. |
| cancelEndpointTimerCallback(endpoint); |
| |
| Status status = Attributes::CurrentLevel::Get(endpoint, currentLevel); |
| if (status != Status::Success) |
| { |
| ChipLogProgress(Zcl, "ERR: reading current level %x", to_underlying(status)); |
| return status; |
| } |
| |
| if (currentLevel.IsNull()) |
| { |
| ChipLogProgress(Zcl, "ERR: Current Level is null"); |
| return Status::Failure; |
| } |
| |
| state->commandId = commandId; |
| |
| // Move To Level commands cause the device to move from its current level to |
| // the specified level at the specified rate. |
| if (state->maxLevel <= level) |
| { |
| state->moveToLevel = state->maxLevel; |
| } |
| else if (level <= state->minLevel) |
| { |
| state->moveToLevel = state->minLevel; |
| } |
| else |
| { |
| state->moveToLevel = level; |
| } |
| |
| // If the level is decreasing, the On/Off attribute is left unchanged. This |
| // logic is to prevent a light from transitioning from off to bright to dim. |
| // Instead, a light that is off will stay off until the target level is |
| // reached. |
| if (currentLevel.Value() <= state->moveToLevel) |
| { |
| if (commandId == Commands::MoveToLevelWithOnOff::Id) |
| { |
| setOnOffValue(endpoint, (state->moveToLevel != state->minLevel)); |
| } |
| if (currentLevel.Value() == state->moveToLevel) |
| { |
| return Status::Success; |
| } |
| state->increasing = true; |
| actualStepSize = static_cast<uint8_t>(state->moveToLevel - currentLevel.Value()); |
| } |
| else |
| { |
| state->increasing = false; |
| actualStepSize = static_cast<uint8_t>(currentLevel.Value() - state->moveToLevel); |
| } |
| |
| #ifndef IGNORE_LEVEL_CONTROL_CLUSTER_TRANSITION |
| // If the Transition time field takes the value null, then the time taken |
| // to move to the new level shall instead be determined by the On/Off |
| // Transition Time attribute. If On/Off Transition Time, which is an |
| // optional attribute, is not present, the device shall move to its new level |
| // as fast as it is able. |
| if (transitionTimeDs.IsNull()) |
| { |
| #ifndef IGNORE_LEVEL_CONTROL_CLUSTER_ON_OFF_TRANSITION_TIME |
| if (emberAfContainsAttribute(endpoint, LevelControl::Id, Attributes::OnOffTransitionTime::Id)) |
| { |
| uint16_t onOffTransitionTime = 0; |
| status = Attributes::OnOffTransitionTime::Get(endpoint, &onOffTransitionTime); |
| if (status != Status::Success) |
| { |
| ChipLogProgress(Zcl, "ERR: reading on/off transition time %x", to_underlying(status)); |
| return status; |
| } |
| |
| // Transition time comes in (or is stored, in the case of On/Off Transition |
| // Time) as tenths of a second, but we work in milliseconds. |
| state->transitionTimeMs = (onOffTransitionTime * MILLISECOND_TICKS_PER_SECOND / 10); |
| } |
| else |
| { |
| state->transitionTimeMs = FASTEST_TRANSITION_TIME_MS; |
| } |
| #else |
| // If the Transition Time field is 0xFFFF and On/Off Transition Time, |
| // which is an optional attribute, is not present, the device shall move to |
| // its new level as fast as it is able. |
| state->transitionTimeMs = FASTEST_TRANSITION_TIME_MS; |
| #endif // IGNORE_LEVEL_CONTROL_CLUSTER_ON_OFF_TRANSITION_TIME |
| } |
| else |
| { |
| // Transition time comes in (or is stored, in the case of On/Off Transition |
| // Time) as tenths of a second, but we work in milliseconds. |
| state->transitionTimeMs = (transitionTimeDs.Value() * MILLISECOND_TICKS_PER_SECOND / 10); |
| } |
| #else |
| // Transition is not supported so always use fastest transition time and ignore |
| // both the provided transition time as well as OnOffTransitionTime. |
| ChipLogProgress(Zcl, "Device does not support transition, ignoring transition time"); |
| state->transitionTimeMs = FASTEST_TRANSITION_TIME_MS; |
| #endif // IGNORE_LEVEL_CONTROL_CLUSTER_TRANSITION |
| |
| // The duration between events will be the transition time divided by the |
| // distance we must move. |
| state->eventDurationMs = state->transitionTimeMs / std::max(static_cast<uint8_t>(1u), actualStepSize); |
| state->elapsedTimeMs = 0; |
| state->storedLevel = storedLevel; |
| state->callbackSchedule.runTime = System::Clock::Milliseconds32(0); |
| |
| #ifdef MATTER_DM_PLUGIN_SCENES_MANAGEMENT |
| // The level has changed, the scene is no longer valid. |
| if (emberAfContainsServer(endpoint, ScenesManagement::Id)) |
| { |
| ScenesManagement::ScenesServer::Instance().MakeSceneInvalidForAllFabrics(endpoint); |
| } |
| #endif // MATTER_DM_PLUGIN_SCENES_MANAGEMENT |
| |
| // The setup was successful, so mark the new state as active and return. |
| scheduleTimerCallbackMs(endpoint, computeCallbackWaitTimeMs(state->callbackSchedule, state->eventDurationMs)); |
| |
| #ifdef MATTER_DM_PLUGIN_ON_OFF |
| // Check that the received MoveToLevelWithOnOff produces a On action and that the onoff support the lighting featuremap |
| if (commandId == Commands::MoveToLevelWithOnOff::Id && state->moveToLevel != state->minLevel && |
| OnOffServer::Instance().SupportsLightingApplications(endpoint)) |
| { |
| OnOff::Attributes::GlobalSceneControl::Set(endpoint, true); |
| } |
| #endif // MATTER_DM_PLUGIN_ON_OFF |
| |
| return Status::Success; |
| } |
| |
| static void moveHandler(CommandHandler * commandObj, const ConcreteCommandPath & commandPath, MoveModeEnum moveMode, |
| DataModel::Nullable<uint8_t> rate, chip::Optional<BitMask<OptionsBitmap>> optionsMask, |
| chip::Optional<BitMask<OptionsBitmap>> optionsOverride) |
| { |
| Status status; |
| uint8_t difference; |
| EmberAfLevelControlState * state; |
| DataModel::Nullable<uint8_t> currentLevel; |
| |
| EndpointId endpoint = commandPath.mEndpointId; |
| CommandId commandId = commandPath.mCommandId; |
| // Validate the received rate and moveMode first. |
| if (rate == static_cast<uint8_t>(0) || moveMode == MoveModeEnum::kUnknownEnumValue) |
| { |
| status = Status::InvalidCommand; |
| goto send_default_response; |
| } |
| |
| state = getState(endpoint); |
| if (state == nullptr) |
| { |
| status = Status::Failure; |
| goto send_default_response; |
| } |
| |
| if (!shouldExecuteIfOff(endpoint, commandId, optionsMask, optionsOverride)) |
| { |
| status = Status::Success; |
| goto send_default_response; |
| } |
| |
| uint8_t eventDuration; // use this local var so state->eventDurationMs is only set once the command is validated. |
| #ifndef IGNORE_LEVEL_CONTROL_CLUSTER_TRANSITION |
| // If the Rate field is null, the device should move at the default move rate, if available, |
| // Otherwise, move as fast as possible |
| if (rate.IsNull()) |
| { |
| DataModel::Nullable<uint8_t> defaultMoveRate; |
| status = Attributes::DefaultMoveRate::Get(endpoint, defaultMoveRate); |
| if (status != Status::Success || defaultMoveRate.IsNull()) |
| { |
| ChipLogProgress(Zcl, "ERR: reading default move rate %x", to_underlying(status)); |
| eventDuration = FASTEST_TRANSITION_TIME_MS; |
| } |
| else |
| { |
| // This should never occur, but old devices could have this, now invalid, value stored. |
| if (defaultMoveRate.Value() == 0) |
| { |
| // The spec is not explicit about what should be done if this happens. |
| // For now Error out if DefaultMoveRate is equal to 0 as this is invalid |
| // until spec defines a behaviour. |
| status = Status::InvalidCommand; |
| goto send_default_response; |
| } |
| // Already checked that defaultMoveRate.Value() != 0. |
| eventDuration = static_cast<uint8_t>(MILLISECOND_TICKS_PER_SECOND / defaultMoveRate.Value()); |
| } |
| } |
| else |
| { |
| // Already confirmed rate.Value() != 0. |
| eventDuration = static_cast<uint8_t>(MILLISECOND_TICKS_PER_SECOND / rate.Value()); |
| } |
| #else |
| // Transition/rate is not supported so always use fastest transition time and ignore |
| // both the provided transition time as well as OnOffTransitionTime. |
| ChipLogProgress(Zcl, "Device does not support transition, ignoring rate"); |
| eventDuration = FASTEST_TRANSITION_TIME_MS; |
| #endif // IGNORE_LEVEL_CONTROL_CLUSTER_TRANSITION |
| |
| // Cancel any currently active command before fiddling with the state. |
| cancelEndpointTimerCallback(endpoint); |
| |
| state->eventDurationMs = eventDuration; |
| status = Attributes::CurrentLevel::Get(endpoint, currentLevel); |
| if (status != Status::Success) |
| { |
| ChipLogProgress(Zcl, "ERR: reading current level %x", to_underlying(status)); |
| goto send_default_response; |
| } |
| |
| if (currentLevel.IsNull()) |
| { |
| ChipLogProgress(Zcl, "ERR: Current Level is null"); |
| status = Status::Failure; |
| |
| goto send_default_response; |
| } |
| |
| state->commandId = commandId; |
| |
| // Move commands cause the device to move from its current level to either |
| // the maximum or minimum level at the specified rate. |
| switch (moveMode) |
| { |
| case MoveModeEnum::kUp: |
| state->increasing = true; |
| state->moveToLevel = state->maxLevel; |
| difference = static_cast<uint8_t>(state->maxLevel - currentLevel.Value()); |
| break; |
| case MoveModeEnum::kDown: |
| state->increasing = false; |
| state->moveToLevel = state->minLevel; |
| difference = static_cast<uint8_t>(currentLevel.Value() - state->minLevel); |
| break; |
| default: |
| status = Status::InvalidCommand; |
| goto send_default_response; |
| } |
| |
| // If the level is decreasing, the On/Off attribute is left unchanged. This |
| // logic is to prevent a light from transitioning from off to bright to dim. |
| // Instead, a light that is off will stay off until the target level is |
| // reached. |
| if (currentLevel.Value() <= state->moveToLevel) |
| { |
| if (commandId == Commands::MoveWithOnOff::Id) |
| { |
| setOnOffValue(endpoint, (state->moveToLevel != state->minLevel)); |
| } |
| if (currentLevel.Value() == state->moveToLevel) |
| { |
| status = Status::Success; |
| goto send_default_response; |
| } |
| } |
| |
| state->transitionTimeMs = difference * state->eventDurationMs; |
| state->elapsedTimeMs = 0; |
| |
| // storedLevel is not used for Move commands. |
| state->storedLevel = INVALID_STORED_LEVEL; |
| state->callbackSchedule.runTime = System::Clock::Milliseconds32(0); |
| |
| // The setup was successful, so mark the new state as active and return. |
| scheduleTimerCallbackMs(endpoint, computeCallbackWaitTimeMs(state->callbackSchedule, state->eventDurationMs)); |
| status = Status::Success; |
| |
| send_default_response: |
| commandObj->AddStatus(commandPath, status); |
| } |
| |
| static void stepHandler(CommandHandler * commandObj, const ConcreteCommandPath & commandPath, StepModeEnum stepMode, |
| uint8_t stepSize, DataModel::Nullable<uint16_t> transitionTimeDs, |
| chip::Optional<BitMask<OptionsBitmap>> optionsMask, chip::Optional<BitMask<OptionsBitmap>> optionsOverride) |
| { |
| Status status; |
| EmberAfLevelControlState * state; |
| DataModel::Nullable<uint8_t> currentLevel; |
| |
| EndpointId endpoint = commandPath.mEndpointId; |
| CommandId commandId = commandPath.mCommandId; |
| uint8_t actualStepSize = stepSize; |
| |
| // Validate the received stepSize and stepMode first. |
| if (stepSize == 0 || stepMode == StepModeEnum::kUnknownEnumValue) |
| { |
| status = Status::InvalidCommand; |
| goto send_default_response; |
| } |
| |
| state = getState(endpoint); |
| if (state == nullptr) |
| { |
| status = Status::Failure; |
| goto send_default_response; |
| } |
| |
| if (!shouldExecuteIfOff(endpoint, commandId, optionsMask, optionsOverride)) |
| { |
| status = Status::Success; |
| goto send_default_response; |
| } |
| |
| // Cancel any currently active command before fiddling with the state. |
| cancelEndpointTimerCallback(endpoint); |
| |
| status = Attributes::CurrentLevel::Get(endpoint, currentLevel); |
| if (status != Status::Success) |
| { |
| ChipLogProgress(Zcl, "ERR: reading current level %x", to_underlying(status)); |
| goto send_default_response; |
| } |
| |
| if (currentLevel.IsNull()) |
| { |
| ChipLogProgress(Zcl, "ERR: Current Level is null"); |
| status = Status::Failure; |
| |
| goto send_default_response; |
| } |
| |
| state->commandId = commandId; |
| |
| // Step commands cause the device to move from its current level to a new |
| // level over the specified transition time. |
| switch (stepMode) |
| { |
| case StepModeEnum::kUp: |
| state->increasing = true; |
| if (state->maxLevel - currentLevel.Value() < stepSize) |
| { |
| state->moveToLevel = state->maxLevel; |
| actualStepSize = static_cast<uint8_t>(state->maxLevel - currentLevel.Value()); |
| } |
| else |
| { |
| state->moveToLevel = static_cast<uint8_t>(currentLevel.Value() + stepSize); |
| } |
| break; |
| case StepModeEnum::kDown: |
| state->increasing = false; |
| if (currentLevel.Value() - state->minLevel < stepSize) |
| { |
| state->moveToLevel = state->minLevel; |
| actualStepSize = static_cast<uint8_t>(currentLevel.Value() - state->minLevel); |
| } |
| else |
| { |
| state->moveToLevel = static_cast<uint8_t>(currentLevel.Value() - stepSize); |
| } |
| break; |
| default: |
| // Should never happen as it is verified at function entry. |
| status = Status::InvalidCommand; |
| goto send_default_response; |
| } |
| |
| // If the level is decreasing, the On/Off attribute is left unchanged. This |
| // logic is to prevent a light from transitioning from off to bright to dim. |
| // Instead, a light that is off will stay off until the target level is |
| // reached. |
| if (currentLevel.Value() <= state->moveToLevel) |
| { |
| if (commandId == Commands::StepWithOnOff::Id) |
| { |
| setOnOffValue(endpoint, (state->moveToLevel != state->minLevel)); |
| } |
| if (currentLevel.Value() == state->moveToLevel) |
| { |
| status = Status::Success; |
| goto send_default_response; |
| } |
| } |
| |
| #ifndef IGNORE_LEVEL_CONTROL_CLUSTER_TRANSITION |
| // If the Transition Time field is null, the device should move as fast as |
| // it is able. |
| if (transitionTimeDs.IsNull()) |
| { |
| state->transitionTimeMs = FASTEST_TRANSITION_TIME_MS; |
| } |
| else |
| { |
| // Transition time comes in as tenths of a second, but we work in |
| // milliseconds. |
| state->transitionTimeMs = (transitionTimeDs.Value() * MILLISECOND_TICKS_PER_SECOND / 10); |
| // If the new level was pegged at the minimum level, the transition time |
| // shall be proportionally reduced. This is done after the conversion to |
| // milliseconds to reduce rounding errors in integer division. |
| if (stepSize != actualStepSize) |
| { |
| state->transitionTimeMs = (state->transitionTimeMs * actualStepSize / std::max(static_cast<uint8_t>(1u), stepSize)); |
| } |
| } |
| #else |
| // Transition is not supported so always use fastest transition time and ignore |
| // both the provided transition time as well as OnOffTransitionTime. |
| ChipLogProgress(Zcl, "Device does not support transition, ignoring transition time"); |
| state->transitionTimeMs = FASTEST_TRANSITION_TIME_MS; |
| #endif // IGNORE_LEVEL_CONTROL_CLUSTER_TRANSITION |
| |
| // The duration between events will be the transition time divided by the |
| // distance we must move. |
| state->eventDurationMs = state->transitionTimeMs / std::max(static_cast<uint8_t>(1u), actualStepSize); |
| state->elapsedTimeMs = 0; |
| |
| // storedLevel is not used for Step commands |
| state->storedLevel = INVALID_STORED_LEVEL; |
| state->callbackSchedule.runTime = System::Clock::Milliseconds32(0); |
| |
| // The setup was successful, so mark the new state as active and return. |
| scheduleTimerCallbackMs(endpoint, computeCallbackWaitTimeMs(state->callbackSchedule, state->eventDurationMs)); |
| status = Status::Success; |
| |
| send_default_response: |
| commandObj->AddStatus(commandPath, status); |
| } |
| |
| static void stopHandler(CommandHandler * commandObj, const ConcreteCommandPath & commandPath, |
| chip::Optional<BitMask<OptionsBitmap>> optionsMask, chip::Optional<BitMask<OptionsBitmap>> optionsOverride) |
| { |
| EndpointId endpoint = commandPath.mEndpointId; |
| CommandId commandId = commandPath.mCommandId; |
| EmberAfLevelControlState * state = getState(endpoint); |
| Status status = Status::Success; |
| |
| if (state == nullptr) |
| { |
| status = Status::Failure; |
| goto send_default_response; |
| } |
| |
| if (!shouldExecuteIfOff(endpoint, commandId, optionsMask, optionsOverride)) |
| { |
| goto send_default_response; |
| } |
| |
| // Cancel any currently active command. |
| cancelEndpointTimerCallback(endpoint); |
| SetCurrentLevelQuietReport(endpoint, state, state->quietCurrentLevel.value(), true /*isEndOfTransition*/); |
| writeRemainingTime(endpoint, 0); |
| |
| send_default_response: |
| commandObj->AddStatus(commandPath, status); |
| } |
| |
| // Follows 07-5123-04 (ZigBee Cluster Library doc), section 3.10.2.1.1. |
| // Quotes are from table 3.46. |
| void emberAfOnOffClusterLevelControlEffectCallback(EndpointId endpoint, bool newValue) |
| { |
| DataModel::Nullable<uint8_t> resolvedLevel; |
| DataModel::Nullable<uint8_t> temporaryCurrentLevelCache; |
| DataModel::Nullable<uint16_t> transitionTime; |
| |
| uint16_t currentOnOffTransitionTime; |
| Status status; |
| bool useOnLevel = false; |
| |
| EmberAfLevelControlState * state = getState(endpoint); |
| if (state == nullptr) |
| { |
| ChipLogProgress(Zcl, "ERR: Level control cluster not available on ep%d", endpoint); |
| return; |
| } |
| |
| uint8_t minimumLevelAllowedForTheDevice = state->minLevel; |
| |
| // "Temporarily store CurrentLevel." |
| status = Attributes::CurrentLevel::Get(endpoint, temporaryCurrentLevelCache); |
| if (status != Status::Success) |
| { |
| ChipLogProgress(Zcl, "ERR: reading current level %x", to_underlying(status)); |
| return; |
| } |
| |
| if (temporaryCurrentLevelCache.IsNull()) |
| { |
| ChipLogProgress(Zcl, "ERR: Current Level is null"); |
| return; |
| } |
| |
| #ifndef IGNORE_LEVEL_CONTROL_CLUSTER_ON_LEVEL_ATTRIBUTE |
| if (emberAfContainsAttribute(endpoint, LevelControl::Id, Attributes::OnLevel::Id)) |
| { |
| status = Attributes::OnLevel::Get(endpoint, resolvedLevel); |
| if (status != Status::Success) |
| { |
| ChipLogProgress(Zcl, "ERR: reading on level %x", to_underlying(status)); |
| return; |
| } |
| |
| if (resolvedLevel.IsNull()) |
| { |
| // OnLevel has undefined value; fall back to CurrentLevel. |
| resolvedLevel.SetNonNull(temporaryCurrentLevelCache.Value()); |
| } |
| else |
| { |
| useOnLevel = true; |
| } |
| } |
| else |
| { |
| resolvedLevel.SetNonNull(temporaryCurrentLevelCache.Value()); |
| } |
| #else |
| resolvedLevel.SetNonNull(temporaryCurrentLevelCache.Value()); |
| #endif // IGNORE_LEVEL_CONTROL_CLUSTER_ON_LEVEL_ATTRIBUTE |
| |
| // Read the OnOffTransitionTime attribute. |
| #ifndef IGNORE_LEVEL_CONTROL_CLUSTER_ON_OFF_TRANSITION_TIME |
| if (emberAfContainsAttribute(endpoint, LevelControl::Id, Attributes::OnOffTransitionTime::Id)) |
| { |
| status = Attributes::OnOffTransitionTime::Get(endpoint, ¤tOnOffTransitionTime); |
| if (status != Status::Success) |
| { |
| ChipLogProgress(Zcl, "ERR: reading current level %x", to_underlying(status)); |
| return; |
| } |
| transitionTime.SetNonNull(currentOnOffTransitionTime); |
| } |
| else |
| { |
| transitionTime.SetNull(); |
| } |
| #else |
| transitionTime.SetNull(); |
| #endif // IGNORE_LEVEL_CONTROL_CLUSTER_ON_OFF_TRANSITION_TIME |
| |
| if (newValue) |
| { |
| // If newValue is OnOff::Commands::On::Id... |
| // "Set CurrentLevel to minimum level allowed for the device." |
| status = SetCurrentLevelQuietReport(endpoint, state, minimumLevelAllowedForTheDevice, false /*isEndOfTransition*/); |
| if (status != Status::Success) |
| { |
| ChipLogProgress(Zcl, "ERR: reading current level %x", to_underlying(status)); |
| return; |
| } |
| |
| // "Move CurrentLevel to OnLevel, or to the stored level if OnLevel is not |
| // defined, over the time period OnOffTransitionTime." |
| moveToLevelHandler(endpoint, Commands::MoveToLevel::Id, resolvedLevel.Value(), transitionTime, chip::NullOptional, |
| chip::NullOptional, |
| INVALID_STORED_LEVEL); // Don't revert to stored level |
| } |
| else |
| { |
| // ...else if newValue is OnOff::Commands::Off::Id... |
| // "Move CurrentLevel to the minimum level allowed for the device over the |
| // time period OnOffTransitionTime." |
| if (useOnLevel) |
| { |
| // If OnLevel is defined, don't revert to stored level. |
| moveToLevelHandler(endpoint, Commands::MoveToLevelWithOnOff::Id, minimumLevelAllowedForTheDevice, transitionTime, |
| chip::NullOptional, chip::NullOptional, INVALID_STORED_LEVEL); |
| } |
| else |
| { |
| // If OnLevel is not defined, set the CurrentLevel to the stored level. |
| moveToLevelHandler(endpoint, Commands::MoveToLevelWithOnOff::Id, minimumLevelAllowedForTheDevice, transitionTime, |
| chip::NullOptional, chip::NullOptional, temporaryCurrentLevelCache.Value()); |
| } |
| } |
| } |
| |
| void emberAfLevelControlClusterServerInitCallback(EndpointId endpoint) |
| { |
| EmberAfLevelControlState * state = getState(endpoint); |
| |
| if (state == nullptr) |
| { |
| ChipLogProgress(Zcl, "ERR: Level control cluster not available on ep%d", endpoint); |
| return; |
| } |
| |
| state->minLevel = MATTER_DM_PLUGIN_LEVEL_CONTROL_MINIMUM_LEVEL; |
| state->maxLevel = MATTER_DM_PLUGIN_LEVEL_CONTROL_MAXIMUM_LEVEL; |
| |
| // If these read only attributes are enabled we use those values as our set minLevel and maxLevel |
| // if get isn't possible, value stays at default |
| Attributes::MinLevel::Get(endpoint, &state->minLevel); |
| Attributes::MaxLevel::Get(endpoint, &state->maxLevel); |
| |
| if (LevelControlHasFeature(endpoint, Feature::kLighting)) |
| { |
| if (state->minLevel < LEVEL_CONTROL_LIGHTING_MIN_LEVEL) |
| { |
| state->minLevel = LEVEL_CONTROL_LIGHTING_MIN_LEVEL; |
| } |
| |
| if (state->maxLevel > LEVEL_CONTROL_LIGHTING_MAX_LEVEL) |
| { |
| state->maxLevel = LEVEL_CONTROL_LIGHTING_MAX_LEVEL; |
| } |
| } |
| |
| DataModel::Nullable<uint8_t> currentLevel; |
| Status status = Attributes::CurrentLevel::Get(endpoint, currentLevel); |
| if (status == Status::Success) |
| { |
| #ifndef IGNORE_LEVEL_CONTROL_CLUSTER_START_UP_CURRENT_LEVEL |
| // StartUp behavior relies StartUpCurrentLevel attributes being Non Volatile. |
| if (areStartUpLevelControlServerAttributesNonVolatile(endpoint)) |
| { |
| // 1.5.14. StartUpCurrentLevel Attribute |
| // The StartUpCurrentLevel attribute SHALL define the desired startup level |
| // for a device when it is supplied with power and this level SHALL be |
| // reflected in the CurrentLevel attribute. The values of the StartUpCurrentLevel |
| // attribute are listed below: |
| // Table 4. Values of the StartUpCurrentLevel attribute |
| // Value Action on power up |
| // 0x00 Set the CurrentLevel attribute to the minimum value permitted on the device. |
| // 0x01-0xfe Set the CurrentLevel attribute to this value. |
| // NULL Set the CurrentLevel attribute to its previous value. |
| // 0xFF Work Around ZAP Can't set default value to NULL |
| // https://github.com/project-chip/zap/issues/354 |
| |
| DataModel::Nullable<uint8_t> startUpCurrentLevel; |
| status = Attributes::StartUpCurrentLevel::Get(endpoint, startUpCurrentLevel); |
| if (status == Status::Success) |
| { |
| if (!startUpCurrentLevel.IsNull()) |
| { |
| if (startUpCurrentLevel.Value() == STARTUP_CURRENT_LEVEL_USE_DEVICE_MINIMUM) |
| { |
| currentLevel.SetNonNull(state->minLevel); |
| } |
| else |
| { |
| // Otherwise set to specified value 0x01-0xFE. |
| // But, need to enforce currentLevel's min/max, right? |
| // Spec doesn't mention this. |
| if (startUpCurrentLevel.Value() < state->minLevel) |
| { |
| currentLevel.SetNonNull(state->minLevel); |
| } |
| else if (startUpCurrentLevel.Value() > state->maxLevel) |
| { |
| currentLevel.SetNonNull(state->maxLevel); |
| } |
| else |
| { |
| currentLevel.SetNonNull(startUpCurrentLevel.Value()); |
| } |
| } |
| } |
| // Otherwise Set the CurrentLevel attribute to its previous value which was already fetch above |
| SetCurrentLevelQuietReport(endpoint, state, currentLevel, false /*isEndOfTransition*/); |
| } |
| } |
| #endif // IGNORE_LEVEL_CONTROL_CLUSTER_START_UP_CURRENT_LEVEL |
| // In any case, we make sure that the respects min/max |
| if (currentLevel.IsNull() || currentLevel.Value() < state->minLevel) |
| { |
| SetCurrentLevelQuietReport(endpoint, state, state->minLevel, false /*isEndOfTransition*/); |
| } |
| else if (currentLevel.Value() > state->maxLevel) |
| { |
| SetCurrentLevelQuietReport(endpoint, state, state->maxLevel, false /*isEndOfTransition*/); |
| } |
| } |
| |
| #if defined(MATTER_DM_PLUGIN_SCENES_MANAGEMENT) && CHIP_CONFIG_SCENES_USE_DEFAULT_HANDLERS |
| // Registers Scene handlers for the level control cluster on the server |
| Clusters::ScenesManagement::ScenesServer::Instance().RegisterSceneHandler(endpoint, LevelControlServer::GetSceneHandler()); |
| #endif // defined(MATTER_DM_PLUGIN_SCENES_MANAGEMENT) && CHIP_CONFIG_SCENES_USE_DEFAULT_HANDLERS |
| |
| emberAfPluginLevelControlClusterServerPostInitCallback(endpoint); |
| } |
| |
| void MatterLevelControlClusterServerShutdownCallback(EndpointId endpoint) |
| { |
| ChipLogProgress(Zcl, "Shuting down level control server cluster on endpoint %d", endpoint); |
| cancelEndpointTimerCallback(endpoint); |
| } |
| |
| #ifndef IGNORE_LEVEL_CONTROL_CLUSTER_START_UP_CURRENT_LEVEL |
| static bool areStartUpLevelControlServerAttributesNonVolatile(EndpointId endpoint) |
| { |
| return !emberAfIsKnownVolatileAttribute(endpoint, LevelControl::Id, Attributes::CurrentLevel::Id) && |
| !emberAfIsKnownVolatileAttribute(endpoint, LevelControl::Id, Attributes::StartUpCurrentLevel::Id); |
| } |
| #endif // IGNORE_LEVEL_CONTROL_CLUSTER_START_UP_CURRENT_LEVEL |
| |
| void emberAfPluginLevelControlClusterServerPostInitCallback(EndpointId endpoint) {} |
| |
| bool LevelControlHasFeature(EndpointId endpoint, Feature feature) |
| { |
| bool success; |
| uint32_t featureMap; |
| success = (Attributes::FeatureMap::Get(endpoint, &featureMap) == Status::Success); |
| |
| return success ? ((featureMap & to_underlying(feature)) != 0) : false; |
| } |
| |
| void MatterLevelControlPluginServerInitCallback() {} |