From a5774cf02116d5639ef84c0f9134c5e570a23033 Mon Sep 17 00:00:00 2001 From: jamesharrow <93921463+jamesharrow@users.noreply.github.com> Date: Wed, 17 Jan 2024 01:34:52 +0000 Subject: [PATCH] EVSE add Session and Event support, test event trigger support (#31200) * Beginnings of Session handling * Added beginnings of EVConnected,EVNotDetected,EnergyTransferStarted,EnergyTransferStopped handling. State machine is not finished. Callback to read Energy Meter added * Added framework for EVSE Test Event triggers * Added EnergyEvseTestEventTrigger delegates * Restyled by whitespace * Restyled by gn * Added :energy-evse-test-event-trigger to public_deps to see if it resolves build errors * Restyled by gn * Fixed Darwin compile error - do not use else after return * Refactored code so that the EvseManufacturer instance could be retrieved for Test Event triggers * Started adding TC_EEVSE_2_2.py * Updated TC_EEVSE_2_2.py to support test events. Still needs to handle reading of Logged Events and verifying they are correct. * Refactored Handling of TestEvents to allow clear, and better error handling. * Refactored state handling by decomposing into state machine events where similar functions are performed based on state transition. Fixed TC chargingEnabledUntil cast to int. Note gets to step 6e * Fixed step 6e caused by not setting the cable limit / maxHardwareCurrentLimit in test events * Added comment to clarify purpose and definition of test eventtrigger field values. * Fixed several bugs in test script * Made SetChargingEnabledUntil take a nullable type. * Removed Reference to step 5c, and moved reading of SessionID to step 4b. More TC_EEVSE_2_2 bug fixes. Added event checking. Still fails at step 14. Does not have enable timeout timer implemented * Fixed issue with not detecting 2nd plug in event, and session ID not incrementing. Now test case passes all the way. * Restyled by isort * Made some attributes persisted per spec. * Fixed incorrect type - not picked up by all compilers. * Added provisional handling for Faults * Added new test event triggers to help test Fault and Diagnostics * Added TC_EEVSE_2_4 * Fix lint issue - unused datetime modules. * Added TC_EEVSE_2_5.py to support DiagnosticsCommand testing. Also changed the SupplyState reverting to Disabled once diagnostics is complete to match the spec. * Created a helper EEVSE base class to avoid repetition in the different test cases. * Restyled by isort * Fixed Lint issues * Revamped TC_EEVSE_2_5 to match spec behaviour (cannot start diagnostics unless Disabled). Also removed hard-coded endpoint ids in Utils * Implemented timer to disable the EVSE automatically. * Added documentation to cover concern about long-lived bytespan in enableKey * Fixed Lint and build issues on other platforms * Restyled by isort * Implemented some of the feedback on PR * Refactored HwSetState to use nested switch statements to be clear that all enums are caught. * Fixed error messages * Test scripts: Removed hardcoded endpoint 1 (use --endpoint 1 in args), allowed the enableKey to be passed in using --hex-arg enableKey:000102030405060708090a0b0c0d0e0f * Made enum class for callbacks and improved documentation comments based on feedback. * Fixed another python lint issue. * Updated README.md with help on how to build for test event triggers, using chip-repl and python testing. * Tweaks to README.md to avoid Myst syntax highlighting issues. * Improved error logging around GetEpochTS() * Made main use std::unique_ptr instead of using new/delete per PR comments. Also moved GetEVSEManufacturer declaration to header file. * Fixing MISSPELL issues in README.md * Fixes #31061 Updated DEVICE_TYPE to 0x050C now this has been allocated * Small correction to description in test case. * Update examples/energy-management-app/energy-management-common/include/EnergyEvseDelegateImpl.h Co-authored-by: Boris Zbarsky * Touched file to retrigger restyled job * Removed whitespace which was added to trigger restyled to rerun --------- Co-authored-by: Restyled.io Co-authored-by: Boris Zbarsky --- .github/.wordlist.txt | 2 + .../include/EVSECallbacks.h | 28 +- .../include/EVSEManufacturerImpl.h | 30 +- .../include/EnergyEvseDelegateImpl.h | 145 ++- .../include/EnergyEvseManager.h | 2 + .../src/EVSEManufacturerImpl.cpp | 245 ++++- .../src/EnergyEvseDelegateImpl.cpp | 998 +++++++++++++++--- .../src/EnergyEvseManager.cpp | 84 +- .../energy-management-app/linux/README.md | 369 ++++++- .../linux/include/CHIPProjectAppConfig.h | 4 +- examples/energy-management-app/linux/main.cpp | 82 +- examples/platform/linux/AppMain.cpp | 10 + examples/platform/linux/BUILD.gn | 8 +- src/app/chip_data_model.gni | 7 + .../EnergyEvseTestEventTriggerDelegate.cpp | 42 + .../EnergyEvseTestEventTriggerDelegate.h | 98 ++ .../energy-evse-server/energy-evse-server.cpp | 3 +- .../energy-evse-server/energy-evse-server.h | 7 +- src/python_testing/TC_EEVSE_2_2.py | 319 ++++++ src/python_testing/TC_EEVSE_2_4.py | 185 ++++ src/python_testing/TC_EEVSE_2_5.py | 137 +++ src/python_testing/TC_EEVSE_Utils.py | 209 ++++ 22 files changed, 2788 insertions(+), 226 deletions(-) create mode 100644 src/app/clusters/energy-evse-server/EnergyEvseTestEventTriggerDelegate.cpp create mode 100644 src/app/clusters/energy-evse-server/EnergyEvseTestEventTriggerDelegate.h create mode 100644 src/python_testing/TC_EEVSE_2_2.py create mode 100644 src/python_testing/TC_EEVSE_2_4.py create mode 100644 src/python_testing/TC_EEVSE_2_5.py create mode 100644 src/python_testing/TC_EEVSE_Utils.py diff --git a/.github/.wordlist.txt b/.github/.wordlist.txt index 44470a612f42c7..07f3df6b7a9822 100644 --- a/.github/.wordlist.txt +++ b/.github/.wordlist.txt @@ -519,8 +519,10 @@ EthyleneOxideConcentrationMeasurement EvalCode EvalCodeWithName EvalFrameDefault +EV EVB evk +EVSE exceptfds ExchangeContext exe diff --git a/examples/energy-management-app/energy-management-common/include/EVSECallbacks.h b/examples/energy-management-app/energy-management-common/include/EVSECallbacks.h index 5bdac2f8e853d6..7d288809952ee3 100644 --- a/examples/energy-management-app/energy-management-common/include/EVSECallbacks.h +++ b/examples/energy-management-app/energy-management-common/include/EVSECallbacks.h @@ -30,26 +30,41 @@ using namespace chip::app::Clusters::EnergyEvse; * This is not specific to the EnergyEVSE cluster, but includes DeviceEnergyManagement * and potential future clusters. */ -enum EVSECallbackType +enum class EVSECallbackType : uint8_t { /* * The State has changed (e.g. from Disabled to Charging, or vice-versa) */ StateChanged, /* - * ChargeCurrent has changed + * ChargeCurrent has changed (e.g. maxChargingCurrent so requires an + update to advertise a different charging current to the EV) */ ChargeCurrentChanged, /* * Charging Preferences have changed + * The daily charging target time, SoC / Added Energy schedules have changed + * and may require the local optimiser to re-run. */ ChargingPreferencesChanged, /* - * DeviceEnergyManagement has changed + * Energy Meter Reading requested from the hardware, e.g. so that the session + * information can be updated. + */ + EnergyMeterReadingRequested, + /* + * The associated DeviceEnergyManagement cluster has changed. This may mean + * that the start time, or power profile or power levels have been adjusted */ DeviceEnergyManagementChanged, }; +enum class ChargingDischargingType : uint8_t +{ + kCharging, + kDischarging +}; + struct EVSECbInfo { EVSECallbackType type; @@ -68,6 +83,13 @@ struct EVSECbInfo { int64_t maximumChargeCurrent; } ChargingCurrent; + + /* for type = EnergyMeterReadingRequested */ + struct + { + ChargingDischargingType meterType; + int64_t * energyMeterValuePtr; + } EnergyMeterReadingRequest; }; }; diff --git a/examples/energy-management-app/energy-management-common/include/EVSEManufacturerImpl.h b/examples/energy-management-app/energy-management-common/include/EVSEManufacturerImpl.h index 4ff45e925674aa..5bf253aef81d9b 100644 --- a/examples/energy-management-app/energy-management-common/include/EVSEManufacturerImpl.h +++ b/examples/energy-management-app/energy-management-common/include/EVSEManufacturerImpl.h @@ -33,15 +33,26 @@ namespace EnergyEvse { class EVSEManufacturer { public: + EVSEManufacturer(EnergyEvseManager * aInstance) { mInstance = aInstance; } + EnergyEvseManager * GetInstance() { return mInstance; } + EnergyEvseDelegate * GetDelegate() + { + if (mInstance) + { + return mInstance->GetDelegate(); + } + return nullptr; + } + /** * @brief Called at start up to apply hardware settings */ - CHIP_ERROR Init(EnergyEvseManager * aInstance); + CHIP_ERROR Init(); /** * @brief Called at shutdown */ - CHIP_ERROR Shutdown(EnergyEvseManager * aInstance); + CHIP_ERROR Shutdown(); /** * @brief Main Callback handler from delegate to user code @@ -49,8 +60,23 @@ class EVSEManufacturer static void ApplicationCallbackHandler(const EVSECbInfo * cb, intptr_t arg); private: + EnergyEvseManager * mInstance; + + int64_t mLastChargingEnergyMeter = 0; + int64_t mLastDischargingEnergyMeter = 0; }; +/** @brief Helper function to return the singleton EVSEManufacturer instance + * + * This is needed by the EVSEManufacturer class to support TestEventTriggers + * which are called outside of any class context. This allows the EVSEManufacturer + * class to return the relevant Delegate instance in which to invoke the test + * events on. + * + * This function is typically found in main.cpp or wherever the singleton is created. + */ +EVSEManufacturer * GetEvseManufacturer(); + } // namespace EnergyEvse } // namespace Clusters } // namespace app diff --git a/examples/energy-management-app/energy-management-common/include/EnergyEvseDelegateImpl.h b/examples/energy-management-app/energy-management-common/include/EnergyEvseDelegateImpl.h index f3c003d081fc6e..d27f35ee63201c 100644 --- a/examples/energy-management-app/energy-management-common/include/EnergyEvseDelegateImpl.h +++ b/examples/energy-management-app/energy-management-common/include/EnergyEvseDelegateImpl.h @@ -31,6 +31,68 @@ namespace app { namespace Clusters { namespace EnergyEvse { +/* Local state machine Events to allow simpler handling of state transitions */ +enum EVSEStateMachineEvent +{ + EVPluggedInEvent, /* EV has been plugged in */ + EVNotDetectedEvent, /* EV has been unplugged or detected as not connected */ + EVNoDemandEvent, /* EV has stopped asking for demand */ + EVDemandEvent, /* EV has asked for demand*/ + ChargingEnabledEvent, /* Charging has been enabled */ + DischargingEnabledEvent, /* Discharging has been enabled */ + DisabledEvent, /* EVSE has been disabled */ + FaultRaised, /* Fault has been raised */ + FaultCleared, /* Fault has been cleared */ +}; + +/** + * Helper class to handle all of the session related info + */ +class EvseSession +{ +public: + EvseSession(EndpointId aEndpoint) { mEndpointId = aEndpoint; } + /** + * @brief This function records the start time and provided energy meter values as part of the new session. + * + * @param chargingMeterValue - The current value of the energy meter (charging) in mWh + * @param dischargingMeterValue - The current value of the energy meter (discharging) in mWh + */ + void StartSession(int64_t chargingMeterValue, int64_t dischargingMeterValue); + + /** + * @brief This function updates the session Duration to allow read attributes to return latest values + */ + void RecalculateSessionDuration(); + + /** + * @brief This function updates the EnergyCharged meter value + * + * @param chargingMeterValue - The value of the energy meter (charging) in mWh + */ + void UpdateEnergyCharged(int64_t chargingMeterValue); + + /** + * @brief This function updates the EnergyDischarged meter value + * + * @param dischargingMeterValue - The value of the energy meter (discharging) in mWh + */ + void UpdateEnergyDischarged(int64_t dischargingMeterValue); + + /* Public members - represent attributes in the cluster */ + DataModel::Nullable mSessionID; + DataModel::Nullable mSessionDuration; + DataModel::Nullable mSessionEnergyCharged; + DataModel::Nullable mSessionEnergyDischarged; + +private: + EndpointId mEndpointId = 0; + + uint32_t mStartTime = 0; // Epoch_s - 0 means it hasn't started yet + int64_t mSessionEnergyChargedAtStart = 0; // in mWh - 0 means it hasn't been set yet + int64_t mSessionEnergyDischargedAtStart = 0; // in mWh - 0 means it hasn't been set yet +}; + /** * The application delegate. */ @@ -74,15 +136,41 @@ class EnergyEvseDelegate : public EnergyEvse::Delegate */ Status HwRegisterEvseCallbackHandler(EVSECallbackFunc handler, intptr_t arg); + /** + * @brief Decides if a timer is needed based on EVSE state and sets a callback if needed + * + * In order to ensure the EVSE restarts charging (if enabled) after power loss + * this should be called after the EVSE is initialised + * (e.g. HwSetMaxHardwareCurrentLimit and HwSetCircuitCapacity have been called) + * and the persisted attributes have been loaded, and time has been synchronised. + * + * If time isn't sync'd yet it will call itself back periodically (if required) + * until time is sync'd. + * + * It is also called when a EnableCharging or EnableDischarging command + * is recv'd to schedule when the EVSE should be automatically disabled based + * on ChargingEnabledUntil / DischargingEnabledUntil expiring. + */ + Status ScheduleCheckOnEnabledTimeout(); + // ----------------------------------------------------------------- // Internal API to allow an EVSE to change its internal state etc Status HwSetMaxHardwareCurrentLimit(int64_t currentmA); + int64_t HwGetMaxHardwareCurrentLimit() { return mMaxHardwareCurrentLimit; } Status HwSetCircuitCapacity(int64_t currentmA); Status HwSetCableAssemblyLimit(int64_t currentmA); + int64_t HwGetCableAssemblyLimit() { return mCableAssemblyCurrentLimit; } Status HwSetState(StateEnum state); + StateEnum HwGetState() { return mHwState; }; Status HwSetFault(FaultStateEnum fault); Status HwSetRFID(ByteSpan uid); Status HwSetVehicleID(const CharSpan & vehID); + Status HwDiagnosticsComplete(); + Status SendEVConnectedEvent(); + Status SendEVNotDetectedEvent(); + Status SendEnergyTransferStartedEvent(); + Status SendEnergyTransferStoppedEvent(EnergyTransferStoppedReasonEnum reason); + Status SendFaultEvent(FaultStateEnum newFaultState); // ------------------------------------------------------------------ // Get attribute methods @@ -96,10 +184,10 @@ class EnergyEvseDelegate : public EnergyEvse::Delegate CHIP_ERROR SetFaultState(FaultStateEnum); DataModel::Nullable GetChargingEnabledUntil() override; - CHIP_ERROR SetChargingEnabledUntil(uint32_t); + CHIP_ERROR SetChargingEnabledUntil(DataModel::Nullable); DataModel::Nullable GetDischargingEnabledUntil() override; - CHIP_ERROR SetDischargingEnabledUntil(uint32_t); + CHIP_ERROR SetDischargingEnabledUntil(DataModel::Nullable); int64_t GetCircuitCapacity() override; CHIP_ERROR SetCircuitCapacity(int64_t); @@ -128,7 +216,7 @@ class EnergyEvseDelegate : public EnergyEvse::Delegate DataModel::Nullable GetNextChargeTargetSoC() override; DataModel::Nullable GetApproximateEVEfficiency() override; - CHIP_ERROR SetApproximateEVEfficiency(uint16_t) override; + CHIP_ERROR SetApproximateEVEfficiency(DataModel::Nullable) override; /* SOC attributes */ DataModel::Nullable GetStateOfCharge() override; @@ -143,10 +231,11 @@ class EnergyEvseDelegate : public EnergyEvse::Delegate private: /* Constants */ - static constexpr int DEFAULT_MIN_CHARGE_CURRENT = 6000; /* 6A */ - static constexpr int DEFAULT_USER_MAXIMUM_CHARGE_CURRENT = kMaximumChargeCurrent; /* 80A */ - static constexpr int DEFAULT_RANDOMIZATION_DELAY_WINDOW = 600; /* 600s */ - static constexpr int kMaxVehicleIDBufSize = 32; + static constexpr int kDefaultMinChargeCurrent = 6000; /* 6A */ + static constexpr int kDefaultUserMaximumChargeCurrent = kMaximumChargeCurrent; /* 80A */ + static constexpr int kDefaultRandomizationDelayWindow = 600; /* 600s */ + static constexpr int kMaxVehicleIDBufSize = 32; + static constexpr int kPeriodicCheckIntervalRealTimeClockNotSynced = 30; /* private variables for controlling the hardware - these are not attributes */ int64_t mMaxHardwareCurrentLimit = 0; /* Hardware current limit in mA */ @@ -155,16 +244,42 @@ class EnergyEvseDelegate : public EnergyEvse::Delegate int64_t mActualChargingCurrentLimit = 0; StateEnum mHwState = StateEnum::kNotPluggedIn; /* Hardware state */ + /* Variables to hold State and SupplyState in case a fault is raised */ + StateEnum mStateBeforeFault = StateEnum::kUnknownEnumValue; + SupplyStateEnum mSupplyStateBeforeFault = SupplyStateEnum::kUnknownEnumValue; + /* Callback related */ EVSECallbackWrapper mCallbacks = { .handler = nullptr, .arg = 0 }; /* Wrapper to allow callbacks to be registered */ Status NotifyApplicationCurrentLimitChange(int64_t maximumChargeCurrent); Status NotifyApplicationStateChange(); + Status GetEVSEEnergyMeterValue(ChargingDischargingType meterType, int64_t & aMeterValue); + + /* Local State machine handling */ + Status CheckFaultOrDiagnostic(); + Status HandleStateMachineEvent(EVSEStateMachineEvent event); + Status HandleEVPluggedInEvent(); + Status HandleEVNotDetectedEvent(); + Status HandleEVNoDemandEvent(); + Status HandleEVDemandEvent(); + Status HandleChargingEnabledEvent(); + Status HandleDischargingEnabledEvent(); + Status HandleDisabledEvent(); + Status HandleFaultRaised(); + Status HandleFaultCleared(); /** * @brief Helper function to work out the charge limit based on conditions and settings */ Status ComputeMaxChargeCurrentLimit(); + /** + * @brief This checks if the charging or discharging needs to be disabled + * + * @params pointer to SystemLayer + * @params pointer to EnergyEvseDelegate + */ + static void EvseCheckTimerExpiry(System::Layer * systemLayer, void * delegate); + /* Attributes */ StateEnum mState = StateEnum::kNotPluggedIn; SupplyStateEnum mSupplyState = SupplyStateEnum::kDisabled; @@ -172,11 +287,11 @@ class EnergyEvseDelegate : public EnergyEvse::Delegate DataModel::Nullable mChargingEnabledUntil; // TODO Default to 0 to indicate disabled DataModel::Nullable mDischargingEnabledUntil; // TODO Default to 0 to indicate disabled int64_t mCircuitCapacity = 0; - int64_t mMinimumChargeCurrent = DEFAULT_MIN_CHARGE_CURRENT; + int64_t mMinimumChargeCurrent = kDefaultMinChargeCurrent; int64_t mMaximumChargeCurrent = 0; int64_t mMaximumDischargeCurrent = 0; - int64_t mUserMaximumChargeCurrent = DEFAULT_USER_MAXIMUM_CHARGE_CURRENT; // TODO update spec - uint32_t mRandomizationDelayWindow = DEFAULT_RANDOMIZATION_DELAY_WINDOW; + int64_t mUserMaximumChargeCurrent = kDefaultUserMaximumChargeCurrent; // TODO update spec + uint32_t mRandomizationDelayWindow = kDefaultRandomizationDelayWindow; /* PREF attributes */ uint8_t mNumberOfWeeklyTargets = 0; uint8_t mNumberOfDailyTargets = 1; @@ -193,11 +308,11 @@ class EnergyEvseDelegate : public EnergyEvse::Delegate /* PNC attributes*/ DataModel::Nullable mVehicleID; - /* Session SESS attributes */ - DataModel::Nullable mSessionID; - DataModel::Nullable mSessionDuration; - DataModel::Nullable mSessionEnergyCharged; - DataModel::Nullable mSessionEnergyDischarged; + /* Session Object */ + EvseSession mSession = EvseSession(mEndpointId); + + /* Helper variable to hold meter val since last EnergyTransferStarted event */ + int64_t mMeterValueAtEnergyTransferStart; }; } // namespace EnergyEvse diff --git a/examples/energy-management-app/energy-management-common/include/EnergyEvseManager.h b/examples/energy-management-app/energy-management-common/include/EnergyEvseManager.h index 9875c397990ef2..a6a64f35ee9767 100644 --- a/examples/energy-management-app/energy-management-common/include/EnergyEvseManager.h +++ b/examples/energy-management-app/energy-management-common/include/EnergyEvseManager.h @@ -46,6 +46,8 @@ class EnergyEvseManager : public Instance CHIP_ERROR Init(); void Shutdown(); + CHIP_ERROR LoadPersistentAttributes(); + EnergyEvseDelegate * GetDelegate() { return mDelegate; }; private: diff --git a/examples/energy-management-app/energy-management-common/src/EVSEManufacturerImpl.cpp b/examples/energy-management-app/energy-management-common/src/EVSEManufacturerImpl.cpp index 8daf1781103831..da21a51cf7eb5d 100644 --- a/examples/energy-management-app/energy-management-common/src/EVSEManufacturerImpl.cpp +++ b/examples/energy-management-app/energy-management-common/src/EVSEManufacturerImpl.cpp @@ -18,53 +18,74 @@ #include #include +#include +using namespace chip; using namespace chip::app; using namespace chip::app::Clusters; using namespace chip::app::Clusters::EnergyEvse; -CHIP_ERROR EVSEManufacturer::Init(EnergyEvseManager * aInstance) +CHIP_ERROR EVSEManufacturer::Init() { /* Manufacturers should modify this to do any custom initialisation */ /* Register callbacks */ - EnergyEvseDelegate * dg = aInstance->GetDelegate(); + EnergyEvseDelegate * dg = GetEvseManufacturer()->GetDelegate(); if (dg == nullptr) { ChipLogError(AppServer, "Delegate is not initialized"); return CHIP_ERROR_UNINITIALIZED; } - dg->HwRegisterEvseCallbackHandler(ApplicationCallbackHandler, reinterpret_cast(nullptr)); + dg->HwRegisterEvseCallbackHandler(ApplicationCallbackHandler, reinterpret_cast(this)); - /* Set the EVSE Hardware Maximum current limit */ - // For Manufacturer to specify the hardware capability in mA - dg->HwSetMaxHardwareCurrentLimit(32000); - - // For Manufacturer to specify the CircuitCapacity (e.g. from DIP switches) - dg->HwSetCircuitCapacity(20000); - - /* For now let's pretend the EV is plugged in, and asking for demand */ - dg->HwSetState(StateEnum::kPluggedInDemand); - dg->HwSetCableAssemblyLimit(63000); - - /* For now let's pretend the vehicle ID is set */ - dg->HwSetVehicleID(CharSpan::fromCharString("TEST_VEHICLE_123456789")); - dg->HwSetVehicleID(CharSpan::fromCharString("TEST_VEHICLE_9876543210")); + /* + * This is an example implementation for manufacturers to consider + * + * For Manufacturer to specify the hardware capability in mA: + * dg->HwSetMaxHardwareCurrentLimit(32000); // 32A + * + * For Manufacturer to specify the CircuitCapacity in mA (e.g. from DIP switches) + * dg->HwSetCircuitCapacity(20000); // 20A + * + */ - /* This next one will fail because it is too long */ - dg->HwSetVehicleID(CharSpan::fromCharString("TEST_VEHICLE_9876543210TOOOOOOOOOOOOOOOOOOO")); - - /* For now let's pretend the RFID sensor was triggered - send an event */ - uint8_t uid[10] = { 0x01, 0x23, 0x45, 0x67, 0x89, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE }; - dg->HwSetRFID(ByteSpan(uid)); + /* Once the system is initialised then check to see if the state was restored + * (e.g. after a power outage), and if the Enable timer check needs to be started + */ + dg->ScheduleCheckOnEnabledTimeout(); return CHIP_NO_ERROR; } -CHIP_ERROR EVSEManufacturer::Shutdown(EnergyEvseManager * aInstance) -{ +/* + * When the EV is plugged in, and asking for demand change the state + * and set the CableAssembly current limit + * + * EnergyEvseDelegate * dg = GetEvseManufacturer()->GetDelegate(); + * if (dg == nullptr) + * { + * ChipLogError(AppServer, "Delegate is not initialized"); + * return CHIP_ERROR_UNINITIALIZED; + * } + * + * dg->HwSetState(StateEnum::kPluggedInDemand); + * dg->HwSetCableAssemblyLimit(63000); // 63A = 63000mA + * + * + * If the vehicle ID can be retrieved (e.g. over Powerline) + * dg->HwSetVehicleID(CharSpan::fromCharString("TEST_VEHICLE_123456789")); + * + * + * If the EVSE has an RFID sensor, the RFID value read can cause an event to be sent + * (e.g. can be used to indicate if a user as tried to activate the charging) + * + * uint8_t uid[10] = { 0x01, 0x23, 0x45, 0x67, 0x89, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE }; + * dg->HwSetRFID(ByteSpan(uid)); + */ +CHIP_ERROR EVSEManufacturer::Shutdown() +{ return CHIP_NO_ERROR; } @@ -78,6 +99,8 @@ CHIP_ERROR EVSEManufacturer::Shutdown(EnergyEvseManager * aInstance) */ void EVSEManufacturer::ApplicationCallbackHandler(const EVSECbInfo * cb, intptr_t arg) { + EVSEManufacturer * pClass = reinterpret_cast(arg); + switch (cb->type) { case EVSECallbackType::StateChanged: @@ -87,7 +110,177 @@ void EVSEManufacturer::ApplicationCallbackHandler(const EVSECbInfo * cb, intptr_ ChipLogProgress(AppServer, "EVSE callback - maxChargeCurrent changed to %ld", static_cast(cb->ChargingCurrent.maximumChargeCurrent)); break; + case EVSECallbackType::EnergyMeterReadingRequested: + ChipLogProgress(AppServer, "EVSE callback - EnergyMeterReadingRequested"); + if (cb->EnergyMeterReadingRequest.meterType == ChargingDischargingType::kCharging) + { + *(cb->EnergyMeterReadingRequest.energyMeterValuePtr) = pClass->mLastChargingEnergyMeter; + } + else + { + *(cb->EnergyMeterReadingRequest.energyMeterValuePtr) = pClass->mLastDischargingEnergyMeter; + } + break; + + default: + ChipLogError(AppServer, "Unhandled EVSE Callback type %d", static_cast(cb->type)); + } +} + +struct EVSETestEventSaveData +{ + int64_t mOldMaxHardwareCurrentLimit; + int64_t mOldCircuitCapacity; + int64_t mOldUserMaximumChargeCurrent; + int64_t mOldCableAssemblyLimit; + StateEnum mOldHwStateBasic; /* For storing hwState before Basic Func event */ + StateEnum mOldHwStatePluggedIn; /* For storing hwState before PluggedIn event */ + StateEnum mOldHwStatePluggedInDemand; /* For storing hwState before PluggedInDemand event */ +}; + +static EVSETestEventSaveData sEVSETestEventSaveData; + +EnergyEvseDelegate * GetEvseDelegate() +{ + EVSEManufacturer * mn = GetEvseManufacturer(); + VerifyOrDieWithMsg(mn != nullptr, AppServer, "EVSEManufacturer is null"); + EnergyEvseDelegate * dg = mn->GetDelegate(); + VerifyOrDieWithMsg(dg != nullptr, AppServer, "EVSE Delegate is null"); + + return dg; +} + +void SetTestEventTrigger_BasicFunctionality() +{ + EnergyEvseDelegate * dg = GetEvseDelegate(); + + sEVSETestEventSaveData.mOldMaxHardwareCurrentLimit = dg->HwGetMaxHardwareCurrentLimit(); + sEVSETestEventSaveData.mOldCircuitCapacity = dg->GetCircuitCapacity(); + sEVSETestEventSaveData.mOldUserMaximumChargeCurrent = dg->GetUserMaximumChargeCurrent(); + sEVSETestEventSaveData.mOldHwStateBasic = dg->HwGetState(); + + dg->HwSetMaxHardwareCurrentLimit(32000); + dg->HwSetCircuitCapacity(32000); + dg->SetUserMaximumChargeCurrent(32000); + dg->HwSetState(StateEnum::kNotPluggedIn); +} +void SetTestEventTrigger_BasicFunctionalityClear() +{ + EnergyEvseDelegate * dg = GetEvseDelegate(); + + dg->HwSetMaxHardwareCurrentLimit(sEVSETestEventSaveData.mOldMaxHardwareCurrentLimit); + dg->HwSetCircuitCapacity(sEVSETestEventSaveData.mOldCircuitCapacity); + dg->SetUserMaximumChargeCurrent(sEVSETestEventSaveData.mOldUserMaximumChargeCurrent); + dg->HwSetState(sEVSETestEventSaveData.mOldHwStateBasic); +} +void SetTestEventTrigger_EVPluggedIn() +{ + EnergyEvseDelegate * dg = GetEvseDelegate(); + + sEVSETestEventSaveData.mOldCableAssemblyLimit = dg->HwGetCableAssemblyLimit(); + sEVSETestEventSaveData.mOldHwStatePluggedIn = dg->HwGetState(); + + dg->HwSetCableAssemblyLimit(63000); + dg->HwSetState(StateEnum::kPluggedInNoDemand); +} +void SetTestEventTrigger_EVPluggedInClear() +{ + EnergyEvseDelegate * dg = GetEvseDelegate(); + dg->HwSetCableAssemblyLimit(sEVSETestEventSaveData.mOldCableAssemblyLimit); + dg->HwSetState(sEVSETestEventSaveData.mOldHwStatePluggedIn); +} + +void SetTestEventTrigger_EVChargeDemand() +{ + EnergyEvseDelegate * dg = GetEvseDelegate(); + + sEVSETestEventSaveData.mOldHwStatePluggedInDemand = dg->HwGetState(); + dg->HwSetState(StateEnum::kPluggedInDemand); +} +void SetTestEventTrigger_EVChargeDemandClear() +{ + EnergyEvseDelegate * dg = GetEvseDelegate(); + + dg->HwSetState(sEVSETestEventSaveData.mOldHwStatePluggedInDemand); +} +void SetTestEventTrigger_EVSEGroundFault() +{ + EnergyEvseDelegate * dg = GetEvseDelegate(); + + dg->HwSetFault(FaultStateEnum::kGroundFault); +} + +void SetTestEventTrigger_EVSEOverTemperatureFault() +{ + EnergyEvseDelegate * dg = GetEvseDelegate(); + + dg->HwSetFault(FaultStateEnum::kOverTemperature); +} + +void SetTestEventTrigger_EVSEFaultClear() +{ + EnergyEvseDelegate * dg = GetEvseDelegate(); + + dg->HwSetFault(FaultStateEnum::kNoError); +} + +void SetTestEventTrigger_EVSEDiagnosticsComplete() +{ + EnergyEvseDelegate * dg = GetEvseDelegate(); + + dg->HwDiagnosticsComplete(); +} + +bool HandleEnergyEvseTestEventTrigger(uint64_t eventTrigger) +{ + EnergyEvseTrigger trigger = static_cast(eventTrigger); + + switch (trigger) + { + case EnergyEvseTrigger::kBasicFunctionality: + ChipLogProgress(Support, "[EnergyEVSE-Test-Event] => Basic Functionality install"); + SetTestEventTrigger_BasicFunctionality(); + break; + case EnergyEvseTrigger::kBasicFunctionalityClear: + ChipLogProgress(Support, "[EnergyEVSE-Test-Event] => Basic Functionality clear"); + SetTestEventTrigger_BasicFunctionalityClear(); + break; + case EnergyEvseTrigger::kEVPluggedIn: + ChipLogProgress(Support, "[EnergyEVSE-Test-Event] => EV plugged in"); + SetTestEventTrigger_EVPluggedIn(); + break; + case EnergyEvseTrigger::kEVPluggedInClear: + ChipLogProgress(Support, "[EnergyEVSE-Test-Event] => EV unplugged"); + SetTestEventTrigger_EVPluggedInClear(); + break; + case EnergyEvseTrigger::kEVChargeDemand: + ChipLogProgress(Support, "[EnergyEVSE-Test-Event] => EV Charge Demand"); + SetTestEventTrigger_EVChargeDemand(); + break; + case EnergyEvseTrigger::kEVChargeDemandClear: + ChipLogProgress(Support, "[EnergyEVSE-Test-Event] => EV Charge NoDemand"); + SetTestEventTrigger_EVChargeDemandClear(); + break; + case EnergyEvseTrigger::kEVSEGroundFault: + ChipLogProgress(Support, "[EnergyEVSE-Test-Event] => EVSE has a GroundFault fault"); + SetTestEventTrigger_EVSEGroundFault(); + break; + case EnergyEvseTrigger::kEVSEOverTemperatureFault: + ChipLogProgress(Support, "[EnergyEVSE-Test-Event] => EVSE has a OverTemperature fault"); + SetTestEventTrigger_EVSEOverTemperatureFault(); + break; + case EnergyEvseTrigger::kEVSEFaultClear: + ChipLogProgress(Support, "[EnergyEVSE-Test-Event] => EVSE faults have cleared"); + SetTestEventTrigger_EVSEFaultClear(); + break; + case EnergyEvseTrigger::kEVSEDiagnosticsComplete: + ChipLogProgress(Support, "[EnergyEVSE-Test-Event] => EVSE Diagnostics Completed"); + SetTestEventTrigger_EVSEDiagnosticsComplete(); + break; + default: - ChipLogError(AppServer, "Unhandler EVSE Callback type %d", static_cast(cb->type)); + return false; } + + return true; } diff --git a/examples/energy-management-app/energy-management-common/src/EnergyEvseDelegateImpl.cpp b/examples/energy-management-app/energy-management-common/src/EnergyEvseDelegateImpl.cpp index 4cc83eaaf8a835..2b4a6e44f5fcea 100644 --- a/examples/energy-management-app/energy-management-common/src/EnergyEvseDelegateImpl.cpp +++ b/examples/energy-management-app/energy-management-common/src/EnergyEvseDelegateImpl.cpp @@ -20,6 +20,7 @@ #include #include #include +#include using namespace chip; using namespace chip::app; @@ -41,6 +42,13 @@ EnergyEvseDelegate::~EnergyEvseDelegate() } } +/** + * @brief Helper function to get current timestamp in Epoch format + * + * @param chipEpoch reference to hold return timestamp + */ +CHIP_ERROR GetEpochTS(uint32_t & chipEpoch); + /** * @brief Called when EVSE cluster receives Disable command */ @@ -48,33 +56,10 @@ Status EnergyEvseDelegate::Disable() { ChipLogProgress(AppServer, "EnergyEvseDelegate::Disable()"); - /* Update State */ - switch (mHwState) - { - case StateEnum::kNotPluggedIn: - SetState(StateEnum::kNotPluggedIn); - break; - - case StateEnum::kPluggedInNoDemand: - SetState(StateEnum::kPluggedInNoDemand); - break; - - case StateEnum::kPluggedInDemand: - SetState(StateEnum::kPluggedInDemand); - break; - - default: - ChipLogError(AppServer, "Unexpected EVSE hardware state"); - SetState(StateEnum::kFault); - break; - } - - /* update SupplyState */ - SetSupplyState(SupplyStateEnum::kDisabled); - + DataModel::Nullable disableTime(0); /* update ChargingEnabledUntil & DischargingEnabledUntil to show 0 */ - SetChargingEnabledUntil(0); - SetDischargingEnabledUntil(0); + SetChargingEnabledUntil(disableTime); + SetDischargingEnabledUntil(disableTime); /* update MinimumChargeCurrent & MaximumChargeCurrent to 0 */ SetMinimumChargeCurrent(0); @@ -83,10 +68,7 @@ Status EnergyEvseDelegate::Disable() /* update MaximumDischargeCurrent to 0 */ SetMaximumDischargeCurrent(0); - NotifyApplicationStateChange(); - // TODO: Generate events - - return Status::Success; + return HandleStateMachineEvent(EVSEStateMachineEvent::DisabledEvent); } /** @@ -123,95 +105,131 @@ Status EnergyEvseDelegate::EnableCharging(const DataModel::Nullable & { /* Charging enabled indefinitely */ ChipLogError(AppServer, "Charging enabled indefinitely"); + SetChargingEnabledUntil(chargingEnabledUntil); } else { /* check chargingEnabledUntil is in the future */ ChipLogError(AppServer, "Charging enabled until: %lu", static_cast(chargingEnabledUntil.Value())); - // TODO - // if (checkChargingEnabled) + SetChargingEnabledUntil(chargingEnabledUntil); } - /* Check current state isn't already enabled */ - - /* If charging is already enabled, check that the parameters may have - changed, these may override an existing charging command */ - switch (mHwState) - { - case StateEnum::kNotPluggedIn: - // TODO handle errors here - SetState(StateEnum::kNotPluggedIn); - break; - - case StateEnum::kPluggedInNoDemand: - // TODO handle errors here - // TODO REFACTOR per Andrei's comment in PR30857 - can we collapse this switch statement? - SetState(StateEnum::kPluggedInNoDemand); - break; - - case StateEnum::kPluggedInDemand: - /* If the EVSE is asking for demand then enable charging */ - SetState(StateEnum::kPluggedInCharging); - break; - - default: - ChipLogError(AppServer, "Unexpected EVSE hardware state"); - SetState(StateEnum::kFault); - break; - } - - /* update SupplyState to say that charging is now enabled */ - SetSupplyState(SupplyStateEnum::kChargingEnabled); - /* If it looks ok, store the min & max charging current */ mMaximumChargingCurrentLimitFromCommand = maximumChargeCurrent; SetMinimumChargeCurrent(minimumChargeCurrent); // TODO persist these to KVS - // TODO: Generate events + ComputeMaxChargeCurrentLimit(); - NotifyApplicationStateChange(); - - return this->ComputeMaxChargeCurrentLimit(); + return HandleStateMachineEvent(EVSEStateMachineEvent::ChargingEnabledEvent); } /** * @brief Called when EVSE cluster receives EnableDischarging command * * @param dischargingEnabledUntil (can be null to indefinite discharging) - * @param maximumChargeCurrent (in mA) + * @param maximumDischargeCurrent (in mA) */ Status EnergyEvseDelegate::EnableDischarging(const DataModel::Nullable & dischargingEnabledUntil, const int64_t & maximumDischargeCurrent) { ChipLogProgress(AppServer, "EnergyEvseDelegate::EnableDischarging() called."); - /* update SupplyState */ - SetSupplyState(SupplyStateEnum::kDischargingEnabled); + // TODO save the maxDischarging Current + // TODO Do something with timestamp - // TODO: Generate events + return HandleStateMachineEvent(EVSEStateMachineEvent::DischargingEnabledEvent); +} - NotifyApplicationStateChange(); +/** + * @brief Routine to help schedule a timer callback to check if the EVSE should go disabled + * + * If the clock is sync'd we can work out when to call back to check when to disable the EVSE + * automatically. If the clock isn't sync'd the we just set a timer to check once every 30s. + * + * We first check the SupplyState to check if it is EnabledCharging or EnabledDischarging + * Then if the EnabledCharging/DischargingUntil is not Null, then we compute a delay to come + * back and check. + */ +Status EnergyEvseDelegate::ScheduleCheckOnEnabledTimeout() +{ + + uint32_t chipEpoch = 0; + DataModel::Nullable enabledUntilTime; + if (mSupplyState == SupplyStateEnum::kChargingEnabled) + { + enabledUntilTime = GetChargingEnabledUntil(); + } + else if (mSupplyState == SupplyStateEnum::kDischargingEnabled) + { + enabledUntilTime = GetDischargingEnabledUntil(); + } + else + { + // In all other states the EVSE is disabled + return Status::Success; + } + + if (enabledUntilTime.IsNull()) + { + /* This is enabled indefinitely so don't schedule a callback */ + return Status::Success; + } + + CHIP_ERROR err = GetEpochTS(chipEpoch); + if (err == CHIP_NO_ERROR) + { + /* time is sync'd */ + int32_t delta = static_cast(enabledUntilTime.Value() - chipEpoch); + if (delta > 0) + { + /* The timer hasn't expired yet - set a timer to check in the future */ + ChipLogDetail(AppServer, "Setting EVSE Enable check timer for %ld seconds", static_cast(delta)); + DeviceLayer::SystemLayer().StartTimer(System::Clock::Seconds32(delta), EvseCheckTimerExpiry, this); + } + else + { + /* we have gone past the enabledUntilTime - so we need to disable */ + ChipLogDetail(AppServer, "EVSE enable time expired, disabling charging"); + Disable(); + } + } + else if (err == CHIP_ERROR_REAL_TIME_NOT_SYNCED) + { + /* Real time isn't sync'd -lets check again in 30 seconds - otherwise keep the charger enabled */ + DeviceLayer::SystemLayer().StartTimer(System::Clock::Seconds32(kPeriodicCheckIntervalRealTimeClockNotSynced), + EvseCheckTimerExpiry, this); + } return Status::Success; } +void EnergyEvseDelegate::EvseCheckTimerExpiry(System::Layer * systemLayer, void * delegate) +{ + EnergyEvseDelegate * dg = reinterpret_cast(delegate); + + dg->ScheduleCheckOnEnabledTimeout(); +} + /** * @brief Called when EVSE cluster receives StartDiagnostics command + * + * NOTE: Application code needs to call HwDiagnosticsComplete + * once diagnostics have been completed. */ Status EnergyEvseDelegate::StartDiagnostics() { /* For EVSE manufacturers to customize */ ChipLogProgress(AppServer, "EnergyEvseDelegate::StartDiagnostics()"); - /* update SupplyState to indicate we are now in Diagnostics mode */ - SetSupplyState(SupplyStateEnum::kDisabledDiagnostics); - - // TODO: Generate events - - // TODO: Notify Application to implement Diagnostics + if (mSupplyState != SupplyStateEnum::kDisabled) + { + ChipLogError(AppServer, "EVSE: cannot be put into diagnostics mode if it is not Disabled!"); + return Status::Failure; + } - NotifyApplicationStateChange(); + // Update the SupplyState - this will automatically callback the Application StateChanged callback + SetSupplyState(SupplyStateEnum::kDisabledDiagnostics); return Status::Success; } @@ -259,7 +277,7 @@ Status EnergyEvseDelegate::HwSetMaxHardwareCurrentLimit(int64_t currentmA) /* there is no attribute to store this so store in private variable */ mMaxHardwareCurrentLimit = currentmA; - return this->ComputeMaxChargeCurrentLimit(); + return ComputeMaxChargeCurrentLimit(); } /** @@ -281,7 +299,7 @@ Status EnergyEvseDelegate::HwSetCircuitCapacity(int64_t currentmA) mCircuitCapacity = currentmA; MatterReportingAttributeChangeCallback(mEndpointId, EnergyEvse::Id, CircuitCapacity::Id); - return this->ComputeMaxChargeCurrentLimit(); + return ComputeMaxChargeCurrentLimit(); } /** @@ -306,40 +324,99 @@ Status EnergyEvseDelegate::HwSetCableAssemblyLimit(int64_t currentmA) /* there is no attribute to store this so store in private variable */ mCableAssemblyCurrentLimit = currentmA; - return this->ComputeMaxChargeCurrentLimit(); + return ComputeMaxChargeCurrentLimit(); } /** * @brief Called by EVSE Hardware to indicate if EV is detected * - * The only allowed states that the EVSE hardware can set are: + * The only allowed states that the EVSE hardware can tell us about are: * kNotPluggedIn * kPluggedInNoDemand * kPluggedInDemand * + * The actual overall state is more complex and includes faults, + * enable & disable charging or discharging etc. + * * @param StateEnum - the state of the EV being plugged in and asking for demand etc */ -Status EnergyEvseDelegate::HwSetState(StateEnum state) +Status EnergyEvseDelegate::HwSetState(StateEnum newState) { - switch (state) + switch (newState) { case StateEnum::kNotPluggedIn: - // TODO - work out logic here - mHwState = state; + switch (mHwState) + { + case StateEnum::kNotPluggedIn: + // No change + break; + case StateEnum::kPluggedInNoDemand: + case StateEnum::kPluggedInDemand: + /* EVSE has been unplugged now */ + mHwState = newState; + HandleStateMachineEvent(EVSEStateMachineEvent::EVNotDetectedEvent); + break; + + default: + // invalid value for mHwState + ChipLogError(AppServer, "HwSetState newstate(kNotPluggedIn) - Invalid value for mHwState"); + mHwState = newState; // set it to the new state anyway + break; + } break; + case StateEnum::kPluggedInNoDemand: - // TODO - work out logic here - mHwState = state; + switch (mHwState) + { + case StateEnum::kNotPluggedIn: + /* EV was unplugged, now is plugged in */ + mHwState = newState; + HandleStateMachineEvent(EVSEStateMachineEvent::EVPluggedInEvent); + break; + case StateEnum::kPluggedInNoDemand: + // No change + break; + case StateEnum::kPluggedInDemand: + /* EV was plugged in and wanted demand, now doesn't want demand */ + mHwState = newState; + HandleStateMachineEvent(EVSEStateMachineEvent::EVNoDemandEvent); + break; + default: + // invalid value for mHwState + ChipLogError(AppServer, "HwSetState newstate(kPluggedInNoDemand) - Invalid value for mHwState"); + mHwState = newState; // set it to the new state anyway + break; + } break; case StateEnum::kPluggedInDemand: - // TODO - work out logic here - mHwState = state; + switch (mHwState) + { + case StateEnum::kNotPluggedIn: + /* EV was unplugged, now is plugged in and wants demand */ + mHwState = newState; + HandleStateMachineEvent(EVSEStateMachineEvent::EVPluggedInEvent); + HandleStateMachineEvent(EVSEStateMachineEvent::EVDemandEvent); + break; + case StateEnum::kPluggedInNoDemand: + /* EV was plugged in and didn't want demand, now does want demand */ + mHwState = newState; + HandleStateMachineEvent(EVSEStateMachineEvent::EVDemandEvent); + break; + case StateEnum::kPluggedInDemand: + // No change + break; + default: + // invalid value for mHwState + ChipLogError(AppServer, "HwSetState newstate(kPluggedInDemand) - Invalid value for mHwState"); + mHwState = newState; // set it to the new state anyway + break; + } break; default: /* All other states should be managed by the Delegate */ - // TODO (assert?) - break; + ChipLogError(AppServer, "HwSetState received invalid enum from caller"); + return Status::Failure; } return Status::Success; @@ -350,29 +427,35 @@ Status EnergyEvseDelegate::HwSetState(StateEnum state) * * @param FaultStateEnum - the fault condition detected */ -Status EnergyEvseDelegate::HwSetFault(FaultStateEnum fault) +Status EnergyEvseDelegate::HwSetFault(FaultStateEnum newFaultState) { ChipLogProgress(AppServer, "EnergyEvseDelegate::Fault()"); - if (fault == FaultStateEnum::kNoError) + if (mFaultState == newFaultState) { - /* Update State to previous state */ - // TODO: need to work out the logic here! + ChipLogError(AppServer, "No change in fault state, ignoring call"); + return Status::Failure; + } + + /** Before we do anything we log the fault + * any change in FaultState reports previous fault and new fault + * and the state prior to the fault being raised */ + SendFaultEvent(newFaultState); + + /* Updated FaultState before we call into the handlers */ + SetFaultState(newFaultState); - /* Update SupplyState to previous state */ + if (newFaultState == FaultStateEnum::kNoError) + { + /* Fault has been cleared */ + HandleStateMachineEvent(EVSEStateMachineEvent::FaultCleared); } else { - /* Update State & SupplyState */ - SetState(StateEnum::kFault); - SetSupplyState(SupplyStateEnum::kDisabledError); + /* a new Fault has been raised */ + HandleStateMachineEvent(EVSEStateMachineEvent::FaultRaised); } - /* Update FaultState */ - SetFaultState(fault); - - // TODO: Generate events - return Status::Success; } @@ -436,10 +519,329 @@ Status EnergyEvseDelegate::HwSetVehicleID(const CharSpan & newValue) return Status::Success; } +/** + * @brief Called by EVSE Hardware to indicate that it has finished its diagnostics test + */ +Status EnergyEvseDelegate::HwDiagnosticsComplete() +{ + if (mSupplyState != SupplyStateEnum::kDisabledDiagnostics) + { + ChipLogError(AppServer, "Incorrect state to be completing diagnostics"); + return Status::Failure; + } + + /* Restore the SupplyState to Disabled (per spec) - client will need to + * re-enable charging or discharging to get out of this state */ + SetSupplyState(SupplyStateEnum::kDisabled); + + return Status::Success; +} /* --------------------------------------------------------------------------- * Functions below are private helper functions internal to the delegate */ +/** + * @brief Main EVSE state machine + * + * This routine handles state transition events to determine behaviour + * + * + */ +Status EnergyEvseDelegate::HandleStateMachineEvent(EVSEStateMachineEvent event) +{ + switch (event) + { + case EVSEStateMachineEvent::EVPluggedInEvent: + ChipLogDetail(AppServer, "EVSE: EV PluggedIn event"); + return HandleEVPluggedInEvent(); + break; + case EVSEStateMachineEvent::EVNotDetectedEvent: + ChipLogDetail(AppServer, "EVSE: EV NotDetected event"); + return HandleEVNotDetectedEvent(); + break; + case EVSEStateMachineEvent::EVNoDemandEvent: + ChipLogDetail(AppServer, "EVSE: EV NoDemand event"); + return HandleEVNoDemandEvent(); + break; + case EVSEStateMachineEvent::EVDemandEvent: + ChipLogDetail(AppServer, "EVSE: EV Demand event"); + return HandleEVDemandEvent(); + break; + case EVSEStateMachineEvent::ChargingEnabledEvent: + ChipLogDetail(AppServer, "EVSE: ChargingEnabled event"); + return HandleChargingEnabledEvent(); + break; + case EVSEStateMachineEvent::DischargingEnabledEvent: + ChipLogDetail(AppServer, "EVSE: DischargingEnabled event"); + return HandleDischargingEnabledEvent(); + break; + case EVSEStateMachineEvent::DisabledEvent: + ChipLogDetail(AppServer, "EVSE: Disabled event"); + return HandleDisabledEvent(); + break; + case EVSEStateMachineEvent::FaultRaised: + ChipLogDetail(AppServer, "EVSE: FaultRaised event"); + return HandleFaultRaised(); + break; + case EVSEStateMachineEvent::FaultCleared: + ChipLogDetail(AppServer, "EVSE: FaultCleared event"); + return HandleFaultCleared(); + break; + default: + return Status::Failure; + } + return Status::Success; +} + +Status EnergyEvseDelegate::HandleEVPluggedInEvent() +{ + /* check if we are already plugged in or not */ + if (mState == StateEnum::kNotPluggedIn) + { + /* EV was not plugged in - start a new session */ + // TODO get energy meter readings + mSession.StartSession(0, 0); + SendEVConnectedEvent(); + + /* Set the state to either PluggedInNoDemand or PluggedInDemand as indicated by mHwState */ + SetState(mHwState); + } + // else we are already plugged in - ignore + return Status::Success; +} + +Status EnergyEvseDelegate::HandleEVNotDetectedEvent() +{ + if (mState == StateEnum::kPluggedInCharging || mState == StateEnum::kPluggedInDischarging) + { + /* + * EV was transferring current - unusual to get to this case without + * first having the state set to kPluggedInNoDemand or kPluggedInDemand + */ + SendEnergyTransferStoppedEvent(EnergyTransferStoppedReasonEnum::kOther); + } + + SendEVNotDetectedEvent(); + SetState(StateEnum::kNotPluggedIn); + return Status::Success; +} + +Status EnergyEvseDelegate::HandleEVNoDemandEvent() +{ + if (mState == StateEnum::kPluggedInCharging || mState == StateEnum::kPluggedInDischarging) + { + /* + * EV was transferring current - EV decided to stop + */ + mSession.RecalculateSessionDuration(); + SendEnergyTransferStoppedEvent(EnergyTransferStoppedReasonEnum::kEVStopped); + } + /* We must still be plugged in to get here - so no need to check if we are plugged in! */ + SetState(StateEnum::kPluggedInNoDemand); + return Status::Success; +} +Status EnergyEvseDelegate::HandleEVDemandEvent() +{ + /* Check to see if the supply is enabled for charging / discharging*/ + switch (mSupplyState) + { + case SupplyStateEnum::kChargingEnabled: + ComputeMaxChargeCurrentLimit(); + SetState(StateEnum::kPluggedInCharging); + SendEnergyTransferStartedEvent(); + break; + case SupplyStateEnum::kDischargingEnabled: + // TODO ComputeMaxDischargeCurrentLimit() - Needs to be implemented + SetState(StateEnum::kPluggedInDischarging); + SendEnergyTransferStartedEvent(); + break; + case SupplyStateEnum::kDisabled: + case SupplyStateEnum::kDisabledError: + case SupplyStateEnum::kDisabledDiagnostics: + /* We must be plugged in, and the event is asking for demand + * but we can't charge or discharge now - leave it as kPluggedInDemand */ + SetState(StateEnum::kPluggedInDemand); + break; + default: + break; + } + return Status::Success; +} + +Status EnergyEvseDelegate::CheckFaultOrDiagnostic() +{ + if (mFaultState != FaultStateEnum::kNoError) + { + ChipLogError(AppServer, "EVSE: Trying to handle command when fault is present"); + return Status::Failure; + } + + if (mSupplyState == SupplyStateEnum::kDisabledDiagnostics) + { + ChipLogError(AppServer, "EVSE: Trying to handle command when in diagnostics mode"); + return Status::Failure; + } + return Status::Success; +} + +Status EnergyEvseDelegate::HandleChargingEnabledEvent() +{ + /* Check there is no Fault or Diagnostics condition */ + Status status = CheckFaultOrDiagnostic(); + if (status != Status::Success) + { + return status; + } + + /* update SupplyState to say that charging is now enabled */ + SetSupplyState(SupplyStateEnum::kChargingEnabled); + + switch (mState) + { + case StateEnum::kNotPluggedIn: + case StateEnum::kPluggedInNoDemand: + break; + case StateEnum::kPluggedInDemand: + ComputeMaxChargeCurrentLimit(); + SetState(StateEnum::kPluggedInCharging); + SendEnergyTransferStartedEvent(); + break; + case StateEnum::kPluggedInCharging: + break; + case StateEnum::kPluggedInDischarging: + /* Switched from discharging to charging */ + SendEnergyTransferStoppedEvent(EnergyTransferStoppedReasonEnum::kEVSEStopped); + + ComputeMaxChargeCurrentLimit(); + SetState(StateEnum::kPluggedInCharging); + SendEnergyTransferStartedEvent(); + break; + default: + break; + } + + ScheduleCheckOnEnabledTimeout(); + + return Status::Success; +} +Status EnergyEvseDelegate::HandleDischargingEnabledEvent() +{ + /* Check there is no Fault or Diagnostics condition */ + Status status = CheckFaultOrDiagnostic(); + if (status != Status::Success) + { + return status; + } + /* update SupplyState to say that charging is now enabled */ + SetSupplyState(SupplyStateEnum::kDischargingEnabled); + + switch (mState) + { + case StateEnum::kNotPluggedIn: + case StateEnum::kPluggedInNoDemand: + break; + case StateEnum::kPluggedInDemand: + // TODO call ComputeMaxDischargeCurrentLimit() + SetState(StateEnum::kPluggedInDischarging); + SendEnergyTransferStartedEvent(); + break; + case StateEnum::kPluggedInCharging: + /* Switched from charging to discharging */ + SendEnergyTransferStoppedEvent(EnergyTransferStoppedReasonEnum::kEVSEStopped); + + // TODO call ComputeMaxDischargeCurrentLimit() + SetState(StateEnum::kPluggedInDischarging); + SendEnergyTransferStartedEvent(); + break; + case StateEnum::kPluggedInDischarging: + default: + break; + } + + ScheduleCheckOnEnabledTimeout(); + + return Status::Success; +} +Status EnergyEvseDelegate::HandleDisabledEvent() +{ + /* Check there is no Fault or Diagnostics condition */ + Status status = CheckFaultOrDiagnostic(); + if (status != Status::Success) + { + return status; + } + + /* update SupplyState to say that charging is now enabled */ + SetSupplyState(SupplyStateEnum::kDisabled); + + switch (mState) + { + case StateEnum::kNotPluggedIn: + case StateEnum::kPluggedInNoDemand: + case StateEnum::kPluggedInDemand: + break; + case StateEnum::kPluggedInCharging: + case StateEnum::kPluggedInDischarging: + SendEnergyTransferStoppedEvent(EnergyTransferStoppedReasonEnum::kEVSEStopped); + SetState(mHwState); + break; + default: + break; + } + + return Status::Success; +} + +/** + * @brief This handles the new fault + * + * Note that if multiple faults happen and this is called repeatedly + * We only save the previous State and SupplyState if its the first raising + * of the fault, so we can restore the state back once all faults have cleared +)*/ +Status EnergyEvseDelegate::HandleFaultRaised() +{ + /* Save the current State and SupplyState so we can restore them if the fault clears */ + if (mStateBeforeFault == StateEnum::kUnknownEnumValue) + { + /* No existing fault - save this value to restore it later if it clears */ + mStateBeforeFault = mState; + } + + if (mSupplyStateBeforeFault == SupplyStateEnum::kUnknownEnumValue) + { + /* No existing fault */ + mSupplyStateBeforeFault = mSupplyState; + } + + /* Update State & SupplyState */ + SetState(StateEnum::kFault); + SetSupplyState(SupplyStateEnum::kDisabledError); + + return Status::Success; +} +Status EnergyEvseDelegate::HandleFaultCleared() +{ + /* Check that something strange hasn't happened */ + if ((mStateBeforeFault == StateEnum::kUnknownEnumValue) || (mSupplyStateBeforeFault == SupplyStateEnum::kUnknownEnumValue)) + { + ChipLogError(AppServer, "EVSE: Something wrong trying to clear fault"); + return Status::Failure; + } + + /* Restore the State and SupplyState back to old values once all the faults have cleared + * Changing the State should notify the application, so it can continue charging etc + */ + SetState(mStateBeforeFault); + SetSupplyState(mSupplyStateBeforeFault); + + /* put back the sentinel to catch new faults if more are raised */ + mStateBeforeFault = StateEnum::kUnknownEnumValue; + mSupplyStateBeforeFault = SupplyStateEnum::kUnknownEnumValue; + + return Status::Success; +} + /** * @brief Called to compute the safe charging current limit * @@ -507,6 +909,172 @@ Status EnergyEvseDelegate::NotifyApplicationStateChange() return Status::Success; } +Status EnergyEvseDelegate::GetEVSEEnergyMeterValue(ChargingDischargingType meterType, int64_t & aMeterValue) +{ + EVSECbInfo cbInfo; + + cbInfo.type = EVSECallbackType::EnergyMeterReadingRequested; + + cbInfo.EnergyMeterReadingRequest.meterType = meterType; + cbInfo.EnergyMeterReadingRequest.energyMeterValuePtr = &aMeterValue; + + if (mCallbacks.handler != nullptr) + { + mCallbacks.handler(&cbInfo, mCallbacks.arg); + } + + return Status::Success; +} + +Status EnergyEvseDelegate::SendEVConnectedEvent() +{ + Events::EVConnected::Type event; + EventNumber eventNumber; + + if (mSession.mSessionID.IsNull()) + { + ChipLogError(AppServer, "SessionID is Null"); + return Status::Failure; + } + + event.sessionID = mSession.mSessionID.Value(); + + CHIP_ERROR err = LogEvent(event, mEndpointId, eventNumber); + if (CHIP_NO_ERROR != err) + { + ChipLogError(AppServer, "Unable to send notify event: %" CHIP_ERROR_FORMAT, err.Format()); + return Status::Failure; + } + return Status::Success; +} + +Status EnergyEvseDelegate::SendEVNotDetectedEvent() +{ + Events::EVNotDetected::Type event; + EventNumber eventNumber; + + if (mSession.mSessionID.IsNull()) + { + ChipLogError(AppServer, "SessionID is Null"); + return Status::Failure; + } + + event.sessionID = mSession.mSessionID.Value(); + event.state = mState; + event.sessionDuration = mSession.mSessionDuration.Value(); + event.sessionEnergyCharged = mSession.mSessionEnergyCharged.Value(); + event.sessionEnergyDischarged = MakeOptional(mSession.mSessionEnergyDischarged.Value()); + + CHIP_ERROR err = LogEvent(event, mEndpointId, eventNumber); + if (CHIP_NO_ERROR != err) + { + ChipLogError(AppServer, "Unable to send notify event: %" CHIP_ERROR_FORMAT, err.Format()); + return Status::Failure; + } + return Status::Success; +} + +Status EnergyEvseDelegate::SendEnergyTransferStartedEvent() +{ + Events::EnergyTransferStarted::Type event; + EventNumber eventNumber; + + if (mSession.mSessionID.IsNull()) + { + ChipLogError(AppServer, "SessionID is Null"); + return Status::Failure; + } + + event.sessionID = mSession.mSessionID.Value(); + event.state = mState; + /** + * A positive value indicates the EV has been enabled for charging and the value is + * taken directly from the MaximumChargeCurrent attribute. + * A negative value indicates that the EV has been enabled for discharging and the value can be taken + * from the MaximumDischargeCurrent attribute with its sign inverted. + */ + + if (mState == StateEnum::kPluggedInCharging) + { + /* Sample the energy meter for charging */ + GetEVSEEnergyMeterValue(ChargingDischargingType::kCharging, mMeterValueAtEnergyTransferStart); + event.maximumCurrent = mMaximumChargeCurrent; + } + else if (mState == StateEnum::kPluggedInDischarging) + { + /* Sample the energy meter for discharging */ + GetEVSEEnergyMeterValue(ChargingDischargingType::kDischarging, mMeterValueAtEnergyTransferStart); + + /* discharging should have a negative current */ + event.maximumCurrent = -mMaximumDischargeCurrent; + } + + CHIP_ERROR err = LogEvent(event, mEndpointId, eventNumber); + if (CHIP_NO_ERROR != err) + { + ChipLogError(AppServer, "Unable to send notify event: %" CHIP_ERROR_FORMAT, err.Format()); + return Status::Failure; + } + + return Status::Success; +} +Status EnergyEvseDelegate::SendEnergyTransferStoppedEvent(EnergyTransferStoppedReasonEnum reason) +{ + Events::EnergyTransferStopped::Type event; + EventNumber eventNumber; + + if (mSession.mSessionID.IsNull()) + { + ChipLogError(AppServer, "SessionID is Null"); + return Status::Failure; + } + + event.sessionID = mSession.mSessionID.Value(); + event.state = mState; + event.reason = reason; + int64_t meterValueNow = 0; + + if (mState == StateEnum::kPluggedInCharging) + { + GetEVSEEnergyMeterValue(ChargingDischargingType::kCharging, meterValueNow); + event.energyTransferred = meterValueNow - mMeterValueAtEnergyTransferStart; + } + else if (mState == StateEnum::kPluggedInDischarging) + { + GetEVSEEnergyMeterValue(ChargingDischargingType::kDischarging, meterValueNow); + + /* discharging should have a negative value */ + event.energyTransferred = mMeterValueAtEnergyTransferStart - meterValueNow; + } + + CHIP_ERROR err = LogEvent(event, mEndpointId, eventNumber); + if (CHIP_NO_ERROR != err) + { + ChipLogError(AppServer, "Unable to send notify event: %" CHIP_ERROR_FORMAT, err.Format()); + return Status::Failure; + } + return Status::Success; +} + +Status EnergyEvseDelegate::SendFaultEvent(FaultStateEnum newFaultState) +{ + Events::Fault::Type event; + EventNumber eventNumber; + + event.sessionID = mSession.mSessionID; // Note here the event sessionID can be Null! + event.state = mState; // This is the state prior to the fault being raised + event.faultStatePreviousState = mFaultState; + event.faultStateCurrentState = newFaultState; + + CHIP_ERROR err = LogEvent(event, mEndpointId, eventNumber); + if (CHIP_NO_ERROR != err) + { + ChipLogError(AppServer, "Unable to send notify event: %" CHIP_ERROR_FORMAT, err.Format()); + return Status::Failure; + } + return Status::Success; +} + /** * Attribute methods */ @@ -529,6 +1097,7 @@ CHIP_ERROR EnergyEvseDelegate::SetState(StateEnum newValue) { ChipLogDetail(AppServer, "State updated to %d", static_cast(mState)); MatterReportingAttributeChangeCallback(mEndpointId, EnergyEvse::Id, State::Id); + NotifyApplicationStateChange(); } return CHIP_NO_ERROR; @@ -554,6 +1123,7 @@ CHIP_ERROR EnergyEvseDelegate::SetSupplyState(SupplyStateEnum newValue) { ChipLogDetail(AppServer, "SupplyState updated to %d", static_cast(mSupplyState)); MatterReportingAttributeChangeCallback(mEndpointId, EnergyEvse::Id, SupplyState::Id); + NotifyApplicationStateChange(); } return CHIP_NO_ERROR; } @@ -588,17 +1158,30 @@ DataModel::Nullable EnergyEvseDelegate::GetChargingEnabledUntil() return mChargingEnabledUntil; } -CHIP_ERROR EnergyEvseDelegate::SetChargingEnabledUntil(uint32_t newValue) +CHIP_ERROR EnergyEvseDelegate::SetChargingEnabledUntil(DataModel::Nullable newValue) { DataModel::Nullable oldValue = mChargingEnabledUntil; - mChargingEnabledUntil = MakeNullable(newValue); - if ((oldValue.IsNull()) || (oldValue.Value() != newValue)) + mChargingEnabledUntil = newValue; + if (oldValue != newValue) { - ChipLogDetail(AppServer, "ChargingEnabledUntil updated to %lu", - static_cast(mChargingEnabledUntil.Value())); + if (newValue.IsNull()) + { + ChipLogDetail(AppServer, "ChargingEnabledUntil updated to Null"); + } + else + { + ChipLogDetail(AppServer, "ChargingEnabledUntil updated to %lu", + static_cast(mChargingEnabledUntil.Value())); + } + + // Write new value to persistent storage. + ConcreteAttributePath path = ConcreteAttributePath(mEndpointId, EnergyEvse::Id, ChargingEnabledUntil::Id); + GetSafeAttributePersistenceProvider()->WriteScalarValue(path, mChargingEnabledUntil); + MatterReportingAttributeChangeCallback(mEndpointId, EnergyEvse::Id, ChargingEnabledUntil::Id); } + return CHIP_NO_ERROR; } @@ -608,17 +1191,29 @@ DataModel::Nullable EnergyEvseDelegate::GetDischargingEnabledUntil() return mDischargingEnabledUntil; } -CHIP_ERROR EnergyEvseDelegate::SetDischargingEnabledUntil(uint32_t newValue) +CHIP_ERROR EnergyEvseDelegate::SetDischargingEnabledUntil(DataModel::Nullable newValue) { DataModel::Nullable oldValue = mDischargingEnabledUntil; - mDischargingEnabledUntil = MakeNullable(newValue); - if ((oldValue.IsNull()) || (oldValue.Value() != newValue)) + mDischargingEnabledUntil = newValue; + if (oldValue != newValue) { - ChipLogDetail(AppServer, "DischargingEnabledUntil updated to %lu", - static_cast(mDischargingEnabledUntil.Value())); + if (newValue.IsNull()) + { + ChipLogDetail(AppServer, "DischargingEnabledUntil updated to Null"); + } + else + { + ChipLogDetail(AppServer, "DischargingEnabledUntil updated to %lu", + static_cast(mDischargingEnabledUntil.Value())); + } + // Write new value to persistent storage. + ConcreteAttributePath path = ConcreteAttributePath(mEndpointId, EnergyEvse::Id, DischargingEnabledUntil::Id); + GetSafeAttributePersistenceProvider()->WriteScalarValue(path, mDischargingEnabledUntil); + MatterReportingAttributeChangeCallback(mEndpointId, EnergyEvse::Id, DischargingEnabledUntil::Id); } + return CHIP_NO_ERROR; } @@ -736,6 +1331,13 @@ CHIP_ERROR EnergyEvseDelegate::SetUserMaximumChargeCurrent(int64_t newValue) if (oldValue != newValue) { ChipLogDetail(AppServer, "UserMaximumChargeCurrent updated to %ld", static_cast(mUserMaximumChargeCurrent)); + + ComputeMaxChargeCurrentLimit(); + + // Write new value to persistent storage. + ConcreteAttributePath path = ConcreteAttributePath(mEndpointId, EnergyEvse::Id, UserMaximumChargeCurrent::Id); + GetSafeAttributePersistenceProvider()->WriteScalarValue(path, mUserMaximumChargeCurrent); + MatterReportingAttributeChangeCallback(mEndpointId, EnergyEvse::Id, UserMaximumChargeCurrent::Id); } @@ -761,6 +1363,11 @@ CHIP_ERROR EnergyEvseDelegate::SetRandomizationDelayWindow(uint32_t newValue) { ChipLogDetail(AppServer, "RandomizationDelayWindow updated to %lu", static_cast(mRandomizationDelayWindow)); + + // Write new value to persistent storage. + ConcreteAttributePath path = ConcreteAttributePath(mEndpointId, EnergyEvse::Id, RandomizationDelayWindow::Id); + GetSafeAttributePersistenceProvider()->WriteScalarValue(path, mRandomizationDelayWindow); + MatterReportingAttributeChangeCallback(mEndpointId, EnergyEvse::Id, RandomizationDelayWindow::Id); } return CHIP_NO_ERROR; @@ -798,14 +1405,25 @@ DataModel::Nullable EnergyEvseDelegate::GetApproximateEVEfficiency() return mApproximateEVEfficiency; } -CHIP_ERROR EnergyEvseDelegate::SetApproximateEVEfficiency(uint16_t newValue) +CHIP_ERROR EnergyEvseDelegate::SetApproximateEVEfficiency(DataModel::Nullable newValue) { DataModel::Nullable oldValue = mApproximateEVEfficiency; - mApproximateEVEfficiency = MakeNullable(newValue); - if ((oldValue.IsNull()) || (oldValue.Value() != newValue)) + mApproximateEVEfficiency = newValue; + if ((oldValue != newValue)) { - ChipLogDetail(AppServer, "ApproximateEVEfficiency updated to %d", mApproximateEVEfficiency.Value()); + if (newValue.IsNull()) + { + ChipLogDetail(AppServer, "ApproximateEVEfficiency updated to Null"); + } + else + { + ChipLogDetail(AppServer, "ApproximateEVEfficiency updated to %d", mApproximateEVEfficiency.Value()); + } + // Write new value to persistent storage. + ConcreteAttributePath path = ConcreteAttributePath(mEndpointId, EnergyEvse::Id, ApproximateEVEfficiency::Id); + GetSafeAttributePersistenceProvider()->WriteScalarValue(path, mApproximateEVEfficiency); + MatterReportingAttributeChangeCallback(mEndpointId, EnergyEvse::Id, ApproximateEVEfficiency::Id); } @@ -831,17 +1449,149 @@ DataModel::Nullable EnergyEvseDelegate::GetVehicleID() /* Session SESS attributes */ DataModel::Nullable EnergyEvseDelegate::GetSessionID() { - return mSessionID; + return mSession.mSessionID; } DataModel::Nullable EnergyEvseDelegate::GetSessionDuration() { - return mSessionDuration; + mSession.RecalculateSessionDuration(); + return mSession.mSessionDuration; } DataModel::Nullable EnergyEvseDelegate::GetSessionEnergyCharged() { - return mSessionEnergyCharged; + return mSession.mSessionEnergyCharged; } DataModel::Nullable EnergyEvseDelegate::GetSessionEnergyDischarged() { - return mSessionEnergyDischarged; + return mSession.mSessionEnergyDischarged; +} + +/** + * @brief Helper function to get current timestamp in Epoch format + * + * @param chipEpoch reference to hold return timestamp + */ +CHIP_ERROR GetEpochTS(uint32_t & chipEpoch) +{ + chipEpoch = 0; + + System::Clock::Milliseconds64 cTMs; + CHIP_ERROR err = System::SystemClock().GetClock_RealTimeMS(cTMs); + + /* If the GetClock_RealTimeMS returns CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE, then + * This platform cannot ever report real time ! + * This should not be certifiable since getting time is a Mandatory + * feature of EVSE Cluster + */ + VerifyOrDie(err != CHIP_ERROR_UNSUPPORTED_CHIP_FEATURE); + + if (err != CHIP_NO_ERROR) + { + ChipLogError(Zcl, "EVSE: Unable to get current time - err:%" CHIP_ERROR_FORMAT, err.Format()); + return err; + } + + auto unixEpoch = std::chrono::duration_cast(cTMs).count(); + if (!UnixEpochToChipEpochTime(unixEpoch, chipEpoch)) + { + ChipLogError(Zcl, "EVSE: unable to convert Unix Epoch time to Matter Epoch Time"); + return err; + } + + return CHIP_NO_ERROR; +} + +/** + * @brief This function samples the start-time, and energy meter to hold the session info + * + * @param chargingMeterValue - The current value of the energy meter (charging) in mWh + * @param dischargingMeterValue - The current value of the energy meter (discharging) in mWh + */ +void EvseSession::StartSession(int64_t chargingMeterValue, int64_t dischargingMeterValue) +{ + /* Get Timestamp */ + uint32_t chipEpoch = 0; + CHIP_ERROR err = GetEpochTS(chipEpoch); + if (err != CHIP_NO_ERROR) + { + /* Note that the error will be also be logged inside GetErrorTS() - + * adding context here to help debugging */ + ChipLogError(AppServer, "EVSE: Unable to get current time when starting session - err:%" CHIP_ERROR_FORMAT, err.Format()); + return; + } + mStartTime = chipEpoch; + + mSessionEnergyChargedAtStart = chargingMeterValue; + mSessionEnergyDischargedAtStart = dischargingMeterValue; + + if (mSessionID.IsNull()) + { + mSessionID = MakeNullable(static_cast(0)); + } + else + { + uint32_t sessionID = mSessionID.Value() + 1; + mSessionID = MakeNullable(sessionID); + } + + /* Reset other session values */ + mSessionDuration = MakeNullable(static_cast(0)); + mSessionEnergyCharged = MakeNullable(static_cast(0)); + mSessionEnergyDischarged = MakeNullable(static_cast(0)); + + MatterReportingAttributeChangeCallback(mEndpointId, EnergyEvse::Id, SessionID::Id); + MatterReportingAttributeChangeCallback(mEndpointId, EnergyEvse::Id, SessionDuration::Id); + MatterReportingAttributeChangeCallback(mEndpointId, EnergyEvse::Id, SessionEnergyCharged::Id); + MatterReportingAttributeChangeCallback(mEndpointId, EnergyEvse::Id, SessionEnergyDischarged::Id); + + // Write values to persistent storage. + ConcreteAttributePath path = ConcreteAttributePath(mEndpointId, EnergyEvse::Id, SessionID::Id); + GetSafeAttributePersistenceProvider()->WriteScalarValue(path, mSessionID); + + // TODO persist mStartTime + // TODO persist mSessionEnergyChargedAtStart + // TODO persist mSessionEnergyDischargedAtStart +} + +/** + * @brief This function updates the session attrs to allow read attributes to return latest values + */ +void EvseSession::RecalculateSessionDuration() +{ + /* Get Timestamp */ + uint32_t chipEpoch = 0; + CHIP_ERROR err = GetEpochTS(chipEpoch); + if (err != CHIP_NO_ERROR) + { + /* Note that the error will be also be logged inside GetErrorTS() - + * adding context here to help debugging */ + ChipLogError(AppServer, "EVSE: Unable to get current time when updating session duration - err:%" CHIP_ERROR_FORMAT, + err.Format()); + return; + } + + uint32_t duration = chipEpoch - mStartTime; + mSessionDuration = MakeNullable(duration); + MatterReportingAttributeChangeCallback(mEndpointId, EnergyEvse::Id, SessionDuration::Id); +} + +/** + * @brief This function updates the EnergyCharged meter value + * + * @param chargingMeterValue - The value of the energy meter (charging) in mWh + */ +void EvseSession::UpdateEnergyCharged(int64_t chargingMeterValue) +{ + mSessionEnergyCharged = MakeNullable(chargingMeterValue - mSessionEnergyChargedAtStart); + MatterReportingAttributeChangeCallback(mEndpointId, EnergyEvse::Id, SessionEnergyCharged::Id); +} + +/** + * @brief This function updates the EnergyDischarged meter value + * + * @param dischargingMeterValue - The value of the energy meter (discharging) in mWh + */ +void EvseSession::UpdateEnergyDischarged(int64_t dischargingMeterValue) +{ + mSessionEnergyDischarged = MakeNullable(dischargingMeterValue - mSessionEnergyDischargedAtStart); + MatterReportingAttributeChangeCallback(mEndpointId, EnergyEvse::Id, SessionEnergyDischarged::Id); } diff --git a/examples/energy-management-app/energy-management-common/src/EnergyEvseManager.cpp b/examples/energy-management-app/energy-management-common/src/EnergyEvseManager.cpp index 0d84d8856212e0..710b13f9d342b8 100644 --- a/examples/energy-management-app/energy-management-common/src/EnergyEvseManager.cpp +++ b/examples/energy-management-app/energy-management-common/src/EnergyEvseManager.cpp @@ -17,14 +17,96 @@ */ #include +#include using namespace chip::app; using namespace chip::app::Clusters; using namespace chip::app::Clusters::EnergyEvse; +CHIP_ERROR EnergyEvseManager::LoadPersistentAttributes() +{ + + SafeAttributePersistenceProvider * aProvider = GetSafeAttributePersistenceProvider(); + EndpointId aEndpointId = mDelegate->GetEndpointId(); + CHIP_ERROR err; + + // Restore ChargingEnabledUntil value + DataModel::Nullable tempChargingEnabledUntil; + err = aProvider->ReadScalarValue(ConcreteAttributePath(aEndpointId, EnergyEvse::Id, Attributes::ChargingEnabledUntil::Id), + tempChargingEnabledUntil); + if (err == CHIP_NO_ERROR) + { + ChipLogDetail(AppServer, "EVSE: successfully loaded ChargingEnabledUntil from NVM"); + mDelegate->SetChargingEnabledUntil(tempChargingEnabledUntil); + } + else + { + ChipLogError(AppServer, "EVSE: Unable to restore persisted ChargingEnabledUntil value"); + } + + // Restore DischargingEnabledUntil value + DataModel::Nullable tempDischargingEnabledUntil; + err = aProvider->ReadScalarValue(ConcreteAttributePath(aEndpointId, EnergyEvse::Id, Attributes::DischargingEnabledUntil::Id), + tempDischargingEnabledUntil); + if (err == CHIP_NO_ERROR) + { + ChipLogDetail(AppServer, "EVSE: successfully loaded DischargingEnabledUntil from NVM"); + mDelegate->SetDischargingEnabledUntil(tempDischargingEnabledUntil); + } + else + { + ChipLogError(AppServer, "EVSE: Unable to restore persisted DischargingEnabledUntil value"); + } + + // Restore UserMaximumChargeCurrent value + int64_t tempUserMaximumChargeCurrent; + err = aProvider->ReadScalarValue(ConcreteAttributePath(aEndpointId, EnergyEvse::Id, Attributes::UserMaximumChargeCurrent::Id), + tempUserMaximumChargeCurrent); + if (err == CHIP_NO_ERROR) + { + ChipLogDetail(AppServer, "EVSE: successfully loaded UserMaximumChargeCurrent from NVM"); + mDelegate->SetUserMaximumChargeCurrent(tempUserMaximumChargeCurrent); + } + else + { + ChipLogError(AppServer, "EVSE: Unable to restore persisted UserMaximumChargeCurrent value"); + } + + // Restore RandomizationDelayWindow value + uint32_t tempRandomizationDelayWindow; + err = aProvider->ReadScalarValue(ConcreteAttributePath(aEndpointId, EnergyEvse::Id, Attributes::RandomizationDelayWindow::Id), + tempRandomizationDelayWindow); + if (err == CHIP_NO_ERROR) + { + ChipLogDetail(AppServer, "EVSE: successfully loaded RandomizationDelayWindow from NVM"); + mDelegate->SetRandomizationDelayWindow(tempRandomizationDelayWindow); + } + else + { + ChipLogError(AppServer, "EVSE: Unable to restore persisted RandomizationDelayWindow value"); + } + + // Restore ApproximateEVEfficiency value + DataModel::Nullable tempApproxEVEfficiency; + err = aProvider->ReadScalarValue(ConcreteAttributePath(aEndpointId, EnergyEvse::Id, Attributes::ApproximateEVEfficiency::Id), + tempApproxEVEfficiency); + if (err == CHIP_NO_ERROR) + { + ChipLogDetail(AppServer, "EVSE: successfully loaded ApproximateEVEfficiency from NVM"); + mDelegate->SetApproximateEVEfficiency(tempApproxEVEfficiency); + } + else + { + ChipLogError(AppServer, "EVSE: Unable to restore persisted ApproximateEVEfficiency value"); + } + + return CHIP_NO_ERROR; // It is ok to have no value loaded here +} + CHIP_ERROR EnergyEvseManager::Init() { - return Instance::Init(); + ReturnErrorOnFailure(Instance::Init()); + return LoadPersistentAttributes(); } void EnergyEvseManager::Shutdown() diff --git a/examples/energy-management-app/linux/README.md b/examples/energy-management-app/linux/README.md index 9671ab073b12a8..be4e265e45b37d 100644 --- a/examples/energy-management-app/linux/README.md +++ b/examples/energy-management-app/linux/README.md @@ -13,10 +13,17 @@ To cross-compile this example on x64 host and run on **NXP i.MX 8M Mini** - [CHIP Linux Energy Management Example](#chip-linux-energy-management-example) - [Building](#building) - - [Commandline Arguments](#commandline-arguments) + - [Commandline arguments](#commandline-arguments) - [Running the Complete Example on Raspberry Pi 4](#running-the-complete-example-on-raspberry-pi-4) - - [Running RPC console](#running-rpc-console) + - [Running RPC Console](#running-rpc-console) - [Device Tracing](#device-tracing) + - [Python Test Cases](#python-test-cases) + - [Running the test cases:](#running-the-test-cases) + - [CHIP-REPL Interaction](#chip-repl-interaction) + - [Building chip-repl:](#building-chip-repl) + - [Activating python virtual env](#activating-python-virtual-env) + - [Interacting with CHIP-REPL and the example app](#interacting-with-chip-repl-and-the-example-app) + - [Using chip-repl to Fake a charging session](#using-chip-repl-to-fake-a-charging-session)
@@ -141,3 +148,361 @@ Obtain tracing json file. $ ./{PIGWEED_REPO}/pw_trace_tokenized/py/pw_trace_tokenized/get_trace.py -s localhost:33000 \ -o {OUTPUT_FILE} -t {ELF_FILE} {PIGWEED_REPO}/pw_trace_tokenized/pw_trace_protos/trace_rpc.proto ``` + +## Python Test Cases + +When you want to test this cluster you can use chip-repl or chip-tool by hand. +CHIP-REPL is slightly easier to interact with when dealing with some of the +complex structures. + +There are several test scripts provided for EVSE (in +[src/python_testing](src/python_testing)): + +- `TC_EEVSE_2_2`: This validates the primary functionality +- `TC_EEVSE_2_3`: This validates Get/Set/Clear target commands +- `TC_EEVSE_2_4`: This validates Faults +- `TC_EEVSE_2_5`: This validates EVSE diagnostic command (optional) + +These scripts require the use of Test Event Triggers via the GeneralDiagnostics +cluster on Endpoint 0. This requires an `enableKey` (16 bytes) and a set of +reserved int64_t test event trigger codes. + +By default the test event support is not enabled, and when compiling the example +app you need to add `chip_enable_energy_evse_trigger=true` to the gn args. + + $ gn gen out/debug + $ ninja -C out/debug --args='chip_enable_energy_evse_trigger=true' + +Once the application is built you also need to tell it at runtime what the +chosen enable key is using the `--enable-key` command line option. + + $ ./chip-energy-management-app --enable-key 000102030405060708090a0b0c0d0e0f + +### Running the test cases: + +From the top-level of the connectedhomeip repo type: + +```bash + $ python src/python_testing/TC_EEVSE_2_2.py --endpoint 1 -m on-network -n 1234 -p 20202021 -d 3840 --hex-arg enableKey:000102030405060708090a0b0c0d0e0f +``` + +- Note that the `--endpoint 1` must be used with the example, since the EVSE + cluster is on endpoint 1. The `--hex-arg enableKey:` value must match + the `--enable-key ` used on chip-energy-management-app args. + +## CHIP-REPL Interaction + +- See chip-repl documentation in + [Matter_REPL_Intro](../../../docs/guides/repl/Matter_REPL_Intro.ipynb) + +### Building chip-repl: + +```bash + $ ./build_python.sh -i +``` + +### Activating python virtual env + +- You need to repeat this step each time you start a new shell. + +```bash + $ source /bin/activate +``` + +### Interacting with CHIP-REPL and the example app + +- Step 1: Launch the example app + +```bash + $ ./chip-energy-management-app --enable-key 000102030405060708090a0b0c0d0e0f +``` + +- Step 2: Launch CHIP-REPL + +```bash + $ chip-repl +``` + +- Step 3: (In chip-repl) Commissioning OnNetwork + +```python + devCtrl.CommissionOnNetwork(1234,20202021) # Commission with NodeID 1234 +Established secure session with Device +Commissioning complete +Out[2]: +``` + +- Step 4: (In chip-repl) Read EVSE attributes + +```python + # Read from NodeID 1234, Endpoint 1, all attributes on EnergyEvse cluster + await devCtrl.ReadAttribute(1234,[(1, chip.clusters.EnergyEvse)]) +``` + +``` +{ +│ 1: { +│ │ : { +│ │ │ : 3790455237, +│ │ │ : Null, +│ │ │ : , +│ │ │ : Null, +│ │ │ : Null, +│ │ │ : 0, +│ │ │ : Null, +│ │ │ : Null, +│ │ │ : [ +... │ │ ], +│ │ │ : 6000, +│ │ │ : Null, +│ │ │ : 758415333, +│ │ │ : 0, +│ │ │ : 1, +│ │ │ : [ +... +│ │ │ ], +│ │ │ : , +│ │ │ : Null, +│ │ │ : Null, +│ │ │ : Null, +│ │ │ : Null, +│ │ │ : Null, +│ │ │ : [ +... │ │ ], +│ │ │ : Null, +│ │ │ : 0, +│ │ │ : Null, +│ │ │ : , +│ │ │ : 600, +│ │ │ : 0, +│ │ │ : 1, +│ │ │ : 80000, +│ │ │ : 2 +│ │ } +│ } +} + +``` + +- Step 5: Setting up a subscription so that attributes updates are sent + automatically + +```python + reportingTimingParams = (3, 60) # MinInterval = 3s, MaxInterval = 60s + subscription = await devCtrl.ReadAttribute(1234,[(1, chip.clusters.EnergyEvse)], reportInterval=reportingTimingParams) +``` + +- Step 6: Send an `EnableCharging` command which lasts for 60 seconds The + `EnableCharging` takes an optional `chargingEnabledUntil` parameter which + allows the charger to automatically disable itself at some preset time in + the future. Note that it uses Epoch_s (which is from Jan 1 2000) which is a + uint32_t in seconds. + +```python + from datetime import datetime, timezone, timedelta + epoch_end = int((datetime.now(tz=timezone.utc) + timedelta(seconds=60) - datetime(2000, 1, 1, 0, 0, 0, 0, timezone.utc)).total_seconds()) + + await devCtrl.SendCommand(1234, 1, chip.clusters.EnergyEvse.Commands.EnableCharging(chargingEnabledUntil=epoch_end,minimumChargeCurrent=2000,maximumChargeCurrent=25000),timedRequestTimeoutMs=3000) +``` + +The output should look like: + +``` +Attribute Changed: +{ +│ 'Endpoint': 1, +│ 'Attribute': , +│ 'Value': +} +Attribute Changed: +{ +│ 'Endpoint': 1, +│ 'Attribute': , +│ 'Value': 2000 +} +Attribute Changed: +{ +│ 'Endpoint': 1, +│ 'Attribute': , +│ 'Value': 758416066 +} +``` + +After 60 seconds the charging should automatically become disabled: + +``` +Attribute Changed: +{ +│ 'Endpoint': 1, +│ 'Attribute': , +│ 'Value': +} +Attribute Changed: +{ +│ 'Endpoint': 1, +│ 'Attribute': , +│ 'Value': 0 +} +Attribute Changed: +{ +│ 'Endpoint': 1, +│ 'Attribute': , +│ 'Value': 0 +} +Attribute Changed: +{ +│ 'Endpoint': 1, +│ 'Attribute': , +│ 'Value': 0 +} +``` + +Note that you can omit the `chargingEnabledUntil` argument and it will charge +indefinitely. + +### Using chip-repl to Fake a charging session + +If you haven't implemented a real EVSE but want to simulate plugging in an EV +then you can use a few of the test event triggers to simulate these scenarios. + +The test event triggers values can be found in: +[EnergyEvseTestEventTriggerDelegate.h](../../../src/app/clusters/energy-evse-server/EnergyEvseTestEventTriggerDelegate.h) + +- 0x0099000000000000 - Simulates the EVSE being installed on a 32A supply +- 0x0099000000000002 - Simulates the EVSE being plugged in (this should + generate an `EVConnected` event) +- 0x0099000000000004 - Simulates the EVSE requesting power + +To send a test event trigger to the app, use the following commands (in +chip-repl): + +```python + # send 1st event trigger to 'install' the EVSE on a 32A supply + await devCtrl.SendCommand(1234, 0, chip.clusters.GeneralDiagnostics.Commands.TestEventTrigger(enableKey=bytes([b for b in range(16)]), eventTrigger=0x0099000000000000)) + + # send 2nd event trigger to plug the EV in + await devCtrl.SendCommand(1234, 0, chip.clusters.GeneralDiagnostics.Commands.TestEventTrigger(enableKey=bytes([b for b in range(16)]), eventTrigger=0x0099000000000002)) + +``` + +Now send the enable charging command (omit the `chargingEnabledUntil` arg this +time): + +```python + await devCtrl.SendCommand(1234, 1, chip.clusters.EnergyEvse.Commands.EnableCharging(minimumChargeCurrent=2000,maximumChargeCurrent=25000),timedRequestTimeoutMs=3000) +``` + +Now send the test event trigger to simulate the EV asking for demand: + +```python + # send 2nd event trigger to plug the EV in + await devCtrl.SendCommand(1234, 0, chip.clusters.GeneralDiagnostics.Commands.TestEventTrigger(enableKey=bytes([b for b in range(16)]), eventTrigger=0x0099000000000004)) + + # Read the events + await devCtrl.ReadEvent(1234,[(1, chip.clusters.EnergyEvse,1)]) +``` + +``` +[ +│ EventReadResult( +│ │ Header=EventHeader( +│ │ │ EndpointId=1, +│ │ │ ClusterId=153, +│ │ │ EventId=0, +│ │ │ EventNumber=65538, +│ │ │ Priority=, +│ │ │ Timestamp=1705102500069, +│ │ │ TimestampType= +│ │ ), +│ │ Status=, +│ │ Data=EVConnected( +│ │ │ sessionID=0 +│ │ ) +│ ), +│ EventReadResult( +│ │ Header=EventHeader( +│ │ │ EndpointId=1, +│ │ │ ClusterId=153, +│ │ │ EventId=2, +│ │ │ EventNumber=65539, +│ │ │ Priority=, +│ │ │ Timestamp=1705102801764, +│ │ │ TimestampType= +│ │ ), +│ │ Status=, +│ │ Data=EnergyTransferStarted( +│ │ │ sessionID=0, +│ │ │ state=, +│ │ │ maximumCurrent=25000 +│ │ ) +│ ) +] +``` + +- We can see that the `EventNumber 65538` was sent when the vehicle was + plugged in, and a new `sessionID=0` was created. +- We can also see that the `EnergyTransferStarted` was sent in + `EventNumber 65539` + +What happens when we unplug the vehicle? + +```python + await devCtrl.SendCommand(1234, 0, chip.clusters.GeneralDiagnostics.Commands.TestEventTrigger(enableKey=bytes([b for b in range(16)]), eventTrigger=0x0099000000000001)) +``` + +When we re-read the events: + +``` +[ +│ EventReadResult( +│ │ Header=EventHeader( +│ │ │ EndpointId=1, +│ │ │ ClusterId=153, +│ │ │ EventId=3, +│ │ │ EventNumber=65540, +│ │ │ Priority=, +│ │ │ Timestamp=1705102996749, +│ │ │ TimestampType= +│ │ ), +│ │ Status=, +│ │ Data=EnergyTransferStopped( +│ │ │ sessionID=0, +│ │ │ state=, +│ │ │ reason=, +│ │ │ energyTransferred=0 +│ │ ) +│ ), +│ EventReadResult( +│ │ Header=EventHeader( +│ │ │ EndpointId=1, +│ │ │ ClusterId=153, +│ │ │ EventId=1, +│ │ │ EventNumber=65541, +│ │ │ Priority=, +│ │ │ Timestamp=1705102996749, +│ │ │ TimestampType= +│ │ ), +│ │ Status=, +│ │ Data=EVNotDetected( +│ │ │ sessionID=0, +│ │ │ state=, +│ │ │ sessionDuration=0, +│ │ │ sessionEnergyCharged=0, +│ │ │ sessionEnergyDischarged=0 +│ │ ) +│ ) +] + +``` + +- In `EventNumber 65540` we had an `EnergyTransferStopped` event with reason + `kOther`. + + This was a rather abrupt end to a charging session (normally we would see + the EVSE or EV decide to stop charging), but this demonstrates the cable + being pulled out without a graceful charging shutdown. + +- In `EventNumber 65541` we had an `EvNotDetected` event showing that the + state was `kPluggedInCharging` prior to the EV being not detected (normally + in a graceful shutdown this would be `kPluggedInNoDemand` or + `kPluggedInDemand`). diff --git a/examples/energy-management-app/linux/include/CHIPProjectAppConfig.h b/examples/energy-management-app/linux/include/CHIPProjectAppConfig.h index f22628d0f69def..e091fa9cf7c91c 100644 --- a/examples/energy-management-app/linux/include/CHIPProjectAppConfig.h +++ b/examples/energy-management-app/linux/include/CHIPProjectAppConfig.h @@ -32,15 +32,13 @@ #define CHIP_DEVICE_CONFIG_ENABLE_COMMISSIONER_DISCOVERY 0 -// Bulbs do not typically use this - enabled so we can use shell to discover commissioners #define CHIP_DEVICE_CONFIG_ENABLE_COMMISSIONER_DISCOVERY_CLIENT 1 #define CHIP_DEVICE_CONFIG_ENABLE_EXTENDED_DISCOVERY 1 #define CHIP_DEVICE_CONFIG_ENABLE_COMMISSIONABLE_DEVICE_TYPE 1 -// TODO We don’t have one yet - but we’d need to change this -#define CHIP_DEVICE_CONFIG_DEVICE_TYPE 257 // 0x0101 = 257 = Dimmable Bulb +#define CHIP_DEVICE_CONFIG_DEVICE_TYPE 0x050C // Energy EVSE #define CHIP_DEVICE_CONFIG_ENABLE_COMMISSIONABLE_DEVICE_NAME 1 diff --git a/examples/energy-management-app/linux/main.cpp b/examples/energy-management-app/linux/main.cpp index dddc6f53d76b20..faf7651875fa94 100644 --- a/examples/energy-management-app/linux/main.cpp +++ b/examples/energy-management-app/linux/main.cpp @@ -35,43 +35,47 @@ using namespace chip; using namespace chip::app; using namespace chip::app::Clusters; -static EnergyEvseDelegate * gDelegate = nullptr; -static EnergyEvseManager * gInstance = nullptr; -static EVSEManufacturer * gEvseManufacturer = nullptr; +static std::unique_ptr gDelegate; +static std::unique_ptr gInstance; +static std::unique_ptr gEvseManufacturer; + +EVSEManufacturer * EnergyEvse::GetEvseManufacturer() +{ + return gEvseManufacturer.get(); +} void ApplicationInit() { CHIP_ERROR err; - if ((gDelegate != nullptr) || (gInstance != nullptr) || (gEvseManufacturer != nullptr)) + if (gDelegate || gInstance || gEvseManufacturer) { ChipLogError(AppServer, "EVSE Instance or Delegate, EvseManufacturer already exist."); return; } - gDelegate = new EnergyEvseDelegate(); - if (gDelegate == nullptr) + gDelegate = std::make_unique(); + if (!gDelegate) { ChipLogError(AppServer, "Failed to allocate memory for EnergyEvseDelegate"); return; } /* Manufacturer may optionally not support all features, commands & attributes */ - gInstance = - new EnergyEvseManager(EndpointId(ENERGY_EVSE_ENDPOINT), *gDelegate, - BitMask( - EnergyEvse::Feature::kChargingPreferences, EnergyEvse::Feature::kPlugAndCharge, - EnergyEvse::Feature::kRfid, EnergyEvse::Feature::kSoCReporting, EnergyEvse::Feature::kV2x), - BitMask(OptionalAttributes::kSupportsUserMaximumChargingCurrent, - OptionalAttributes::kSupportsRandomizationWindow, - OptionalAttributes::kSupportsApproximateEvEfficiency), - BitMask(OptionalCommands::kSupportsStartDiagnostics)); - - if (gInstance == nullptr) + gInstance = std::make_unique( + EndpointId(ENERGY_EVSE_ENDPOINT), *gDelegate, + BitMask(EnergyEvse::Feature::kChargingPreferences, EnergyEvse::Feature::kPlugAndCharge, + EnergyEvse::Feature::kRfid, EnergyEvse::Feature::kSoCReporting, + EnergyEvse::Feature::kV2x), + BitMask(OptionalAttributes::kSupportsUserMaximumChargingCurrent, + OptionalAttributes::kSupportsRandomizationWindow, + OptionalAttributes::kSupportsApproximateEvEfficiency), + BitMask(OptionalCommands::kSupportsStartDiagnostics)); + + if (!gInstance) { ChipLogError(AppServer, "Failed to allocate memory for EnergyEvseManager"); - delete gDelegate; - gDelegate = nullptr; + gDelegate.reset(); return; } @@ -79,36 +83,29 @@ void ApplicationInit() if (err != CHIP_NO_ERROR) { ChipLogError(AppServer, "Init failed on gInstance"); - delete gInstance; - delete gDelegate; - gInstance = nullptr; - gDelegate = nullptr; + gInstance.reset(); + gDelegate.reset(); return; } /* Now create EVSEManufacturer*/ - gEvseManufacturer = new EVSEManufacturer(); - if (gEvseManufacturer == nullptr) + gEvseManufacturer = std::make_unique(gInstance.get()); + if (!gEvseManufacturer) { ChipLogError(AppServer, "Failed to allocate memory for EvseManufacturer"); - delete gInstance; - delete gDelegate; - gInstance = nullptr; - gDelegate = nullptr; + gInstance.reset(); + gDelegate.reset(); return; } /* Call Manufacturer specific init */ - err = gEvseManufacturer->Init(gInstance); + err = gEvseManufacturer->Init(); if (err != CHIP_NO_ERROR) { ChipLogError(AppServer, "Init failed on gEvseManufacturer"); - delete gEvseManufacturer; - delete gInstance; - delete gDelegate; - gEvseManufacturer = nullptr; - gInstance = nullptr; - gDelegate = nullptr; + gEvseManufacturer.reset(); + gInstance.reset(); + gDelegate.reset(); return; } } @@ -118,17 +115,12 @@ void ApplicationShutdown() ChipLogDetail(AppServer, "Energy Management App: ApplicationShutdown()"); /* Shutdown the EVSEManufacturer*/ - gEvseManufacturer->Shutdown(gInstance); + if (gEvseManufacturer) + gEvseManufacturer->Shutdown(); /* Shutdown the Instance - deregister attribute & command handler */ - gInstance->Shutdown(); - - delete gEvseManufacturer; - delete gInstance; - delete gDelegate; - gEvseManufacturer = nullptr; - gInstance = nullptr; - gDelegate = nullptr; + if (gInstance) + gInstance->Shutdown(); } int main(int argc, char * argv[]) diff --git a/examples/platform/linux/AppMain.cpp b/examples/platform/linux/AppMain.cpp index 86382f75665c06..56098c7c490548 100644 --- a/examples/platform/linux/AppMain.cpp +++ b/examples/platform/linux/AppMain.cpp @@ -83,6 +83,9 @@ #if CHIP_DEVICE_CONFIG_ENABLE_BOOLEAN_STATE_CONFIGURATION_TRIGGER #include #endif +#if CHIP_DEVICE_CONFIG_ENABLE_ENERGY_EVSE_TRIGGER +#include +#endif #include #include @@ -561,6 +564,13 @@ void ChipLinuxAppMainLoop(AppMainLoopImplementation * impl) }; otherDelegate = &booleanStateConfigurationTestEventTriggerDelegate; #endif +#if CHIP_DEVICE_CONFIG_ENABLE_ENERGY_EVSE_TRIGGER + static EnergyEvseTestEventTriggerDelegate energyEvseTestEventTriggerDelegate{ + ByteSpan(LinuxDeviceOptions::GetInstance().testEventTriggerEnableKey), otherDelegate + }; + otherDelegate = &energyEvseTestEventTriggerDelegate; +#endif + // For general testing of TestEventTrigger, we have a common "core" event trigger delegate. static SampleTestEventTriggerDelegate testEventTriggerDelegate; VerifyOrDie(testEventTriggerDelegate.Init(ByteSpan(LinuxDeviceOptions::GetInstance().testEventTriggerEnableKey), diff --git a/examples/platform/linux/BUILD.gn b/examples/platform/linux/BUILD.gn index 4a4099c4150bf5..fbbf7e1833894e 100644 --- a/examples/platform/linux/BUILD.gn +++ b/examples/platform/linux/BUILD.gn @@ -21,8 +21,8 @@ import("${chip_root}/src/tracing/tracing_args.gni") declare_args() { chip_enable_smoke_co_trigger = false - chip_enable_boolean_state_configuration_trigger = false + chip_enable_energy_evse_trigger = false } config("app-main-config") { @@ -43,6 +43,10 @@ source_set("boolean-state-configuration-test-event-trigger") { sources = [ "${chip_root}/src/app/clusters/boolean-state-configuration-server/BooleanStateConfigurationTestEventTriggerDelegate.h" ] } +source_set("energy-evse-test-event-trigger") { + sources = [ "${chip_root}/src/app/clusters/energy-evse-server/EnergyEvseTestEventTriggerDelegate.h" ] +} + source_set("app-main") { defines = [ "ENABLE_TRACING=${matter_enable_tracing_support}" ] sources = [ @@ -66,6 +70,7 @@ source_set("app-main") { public_deps = [ ":boolean-state-configuration-test-event-trigger", + ":energy-evse-test-event-trigger", ":smco-test-event-trigger", "${chip_root}/src/lib", "${chip_root}/src/platform/logging:force_stdio", @@ -104,6 +109,7 @@ source_set("app-main") { defines += [ "CHIP_DEVICE_CONFIG_ENABLE_SMOKE_CO_TRIGGER=${chip_enable_smoke_co_trigger}", "CHIP_DEVICE_CONFIG_ENABLE_BOOLEAN_STATE_CONFIGURATION_TRIGGER=${chip_enable_boolean_state_configuration_trigger}", + "CHIP_DEVICE_CONFIG_ENABLE_ENERGY_EVSE_TRIGGER=${chip_enable_energy_evse_trigger}", ] public_configs = [ ":app-main-config" ] diff --git a/src/app/chip_data_model.gni b/src/app/chip_data_model.gni index a22e4811433802..4924ebfa67835d 100644 --- a/src/app/chip_data_model.gni +++ b/src/app/chip_data_model.gni @@ -336,6 +336,13 @@ template("chip_data_model") { "${_app_root}/clusters/${cluster}/${cluster}.cpp", "${_app_root}/clusters/${cluster}/${cluster}.h", ] + } else if (cluster == "energy-evse-server") { + sources += [ + "${_app_root}/clusters/${cluster}/${cluster}.cpp", + "${_app_root}/clusters/${cluster}/${cluster}.h", + "${_app_root}/clusters/${cluster}/EnergyEvseTestEventTriggerDelegate.cpp", + "${_app_root}/clusters/${cluster}/EnergyEvseTestEventTriggerDelegate.h", + ] } else { sources += [ "${_app_root}/clusters/${cluster}/${cluster}.cpp" ] } diff --git a/src/app/clusters/energy-evse-server/EnergyEvseTestEventTriggerDelegate.cpp b/src/app/clusters/energy-evse-server/EnergyEvseTestEventTriggerDelegate.cpp new file mode 100644 index 00000000000000..78fb87085f3b10 --- /dev/null +++ b/src/app/clusters/energy-evse-server/EnergyEvseTestEventTriggerDelegate.cpp @@ -0,0 +1,42 @@ +/* + * + * 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 "EnergyEvseTestEventTriggerDelegate.h" + +using namespace chip::app::Clusters::EnergyEvse; + +namespace chip { + +bool EnergyEvseTestEventTriggerDelegate::DoesEnableKeyMatch(const ByteSpan & enableKey) const +{ + return !mEnableKey.empty() && mEnableKey.data_equal(enableKey); +} + +CHIP_ERROR EnergyEvseTestEventTriggerDelegate::HandleEventTrigger(uint64_t eventTrigger) +{ + if (HandleEnergyEvseTestEventTrigger(eventTrigger)) + { + return CHIP_NO_ERROR; + } + if (mOtherDelegate != nullptr) + { + return mOtherDelegate->HandleEventTrigger(eventTrigger); + } + return CHIP_ERROR_INVALID_ARGUMENT; +} + +} // namespace chip diff --git a/src/app/clusters/energy-evse-server/EnergyEvseTestEventTriggerDelegate.h b/src/app/clusters/energy-evse-server/EnergyEvseTestEventTriggerDelegate.h new file mode 100644 index 00000000000000..3c1dff4f5e7f81 --- /dev/null +++ b/src/app/clusters/energy-evse-server/EnergyEvseTestEventTriggerDelegate.h @@ -0,0 +1,98 @@ +/* + * + * 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. + */ + +#pragma once + +#include +#include + +namespace chip { + +/* + * These Test EventTrigger values are specified in the TC_EEVSE test plan + * and are defined conditions used in test events. + * + * They are sent along with the enableKey (manufacturer defined secret) + * in the General Diagnostic cluster TestEventTrigger command + */ +enum class EnergyEvseTrigger : uint64_t +{ + // Scenarios + // Basic Functionality Test Event | Simulate installation with _{A_CIRCUIT_CAPACITY}_=32A and + // _{A_USER_MAXIMUM_CHARGE_CURRENT}_=32A + kBasicFunctionality = 0x0099000000000000, + // Basic Functionality Test Event Clear | End simulation of installation + kBasicFunctionalityClear = 0x0099000000000001, + // EV Plugged-in Test Event | Simulate plugging the EV into the EVSE using a cable of 63A capacity + kEVPluggedIn = 0x0099000000000002, + // EV Plugged-in Test Event Clear | Simulate unplugging the EV + kEVPluggedInClear = 0x0099000000000003, + // EV Charge Demand Test Event | Simulate the EV presenting charge demand to the EVSE + kEVChargeDemand = 0x0099000000000004, + // EV Charge Demand Test Event Clear | Simulate the EV becoming fully charged + kEVChargeDemandClear = 0x0099000000000005, + // EVSE has a GroundFault fault + kEVSEGroundFault = 0x0099000000000010, + // EVSE has a OverTemperature fault + kEVSEOverTemperatureFault = 0x0099000000000011, + // EVSE faults have cleared + kEVSEFaultClear = 0x0099000000000012, + // EVSE Diagnostics Complete | Simulate diagnostics have been completed and return to normal + kEVSEDiagnosticsComplete = 0x0099000000000020, +}; + +class EnergyEvseTestEventTriggerDelegate : public TestEventTriggerDelegate +{ +public: + /** + * This class expects the enableKey ByteSpan to be valid forever. + * Typically this feature is only enabled in certification testing + * and uses a static secret key in the device for testing (e.g. in factory data) + */ + explicit EnergyEvseTestEventTriggerDelegate(const ByteSpan & enableKey, TestEventTriggerDelegate * otherDelegate) : + mEnableKey(enableKey), mOtherDelegate(otherDelegate) + {} + + /* This function returns True if the enableKey received in the TestEventTrigger command + * matches the value passed into the constructor. + */ + bool DoesEnableKeyMatch(const ByteSpan & enableKey) const override; + + /** This function must return True if the eventTrigger is recognised and handled + * It must return False to allow a higher level TestEvent handler to check other + * clusters that may handle it. + */ + CHIP_ERROR HandleEventTrigger(uint64_t eventTrigger) override; + +private: + ByteSpan mEnableKey; + TestEventTriggerDelegate * mOtherDelegate; +}; + +} // namespace chip + +/** + * @brief User handler for handling the test event trigger + * + * @note If TestEventTrigger is enabled, it needs to be implemented in the app + * + * @param eventTrigger Event trigger to handle + * + * @retval true on success + * @retval false if error happened + */ +bool HandleEnergyEvseTestEventTrigger(uint64_t eventTrigger); diff --git a/src/app/clusters/energy-evse-server/energy-evse-server.cpp b/src/app/clusters/energy-evse-server/energy-evse-server.cpp index 60f9e24be0f962..bc07ba07eef55a 100644 --- a/src/app/clusters/energy-evse-server/energy-evse-server.cpp +++ b/src/app/clusters/energy-evse-server/energy-evse-server.cpp @@ -23,6 +23,7 @@ using namespace chip; using namespace chip::app; +using namespace chip::app::DataModel; using namespace chip::app::Clusters; using namespace chip::app::Clusters::EnergyEvse; using namespace chip::app::Clusters::EnergyEvse::Attributes; @@ -166,7 +167,7 @@ CHIP_ERROR Instance::Write(const ConcreteDataAttributePath & aPath, AttributeVal } uint16_t newValue; ReturnErrorOnFailure(aDecoder.Decode(newValue)); - ReturnErrorOnFailure(mDelegate.SetApproximateEVEfficiency(newValue)); + ReturnErrorOnFailure(mDelegate.SetApproximateEVEfficiency(MakeNullable(newValue))); return CHIP_NO_ERROR; } diff --git a/src/app/clusters/energy-evse-server/energy-evse-server.h b/src/app/clusters/energy-evse-server/energy-evse-server.h index 5df2f0149eb1ed..a194551607c6f4 100644 --- a/src/app/clusters/energy-evse-server/energy-evse-server.h +++ b/src/app/clusters/energy-evse-server/energy-evse-server.h @@ -48,6 +48,7 @@ class Delegate virtual ~Delegate() = default; void SetEndpointId(EndpointId aEndpoint) { mEndpointId = aEndpoint; } + EndpointId GetEndpointId() { return mEndpointId; } /** * @brief Delegate should implement a handler to disable the EVSE. @@ -116,9 +117,9 @@ class Delegate // ------------------------------------------------------------------ // Set attribute methods - virtual CHIP_ERROR SetUserMaximumChargeCurrent(int64_t aNewValue) = 0; - virtual CHIP_ERROR SetRandomizationDelayWindow(uint32_t aNewValue) = 0; - virtual CHIP_ERROR SetApproximateEVEfficiency(uint16_t aNewValue) = 0; + virtual CHIP_ERROR SetUserMaximumChargeCurrent(int64_t aNewValue) = 0; + virtual CHIP_ERROR SetRandomizationDelayWindow(uint32_t aNewValue) = 0; + virtual CHIP_ERROR SetApproximateEVEfficiency(DataModel::Nullable aNewValue) = 0; protected: EndpointId mEndpointId = 0; diff --git a/src/python_testing/TC_EEVSE_2_2.py b/src/python_testing/TC_EEVSE_2_2.py new file mode 100644 index 00000000000000..8ebc78a22e94cd --- /dev/null +++ b/src/python_testing/TC_EEVSE_2_2.py @@ -0,0 +1,319 @@ +# +# Copyright (c) 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. + + +import logging +import time +from datetime import datetime, timedelta, timezone + +import chip.clusters as Clusters +from chip.clusters.Types import NullValue +from matter_testing_support import MatterBaseTest, TestStep, async_test_body, default_matter_test_main +from mobly import asserts +from TC_EEVSE_Utils import EEVSEBaseTestHelper, EventChangeCallback + +logger = logging.getLogger(__name__) + + +class TC_EEVSE_2_2(MatterBaseTest, EEVSEBaseTestHelper): + + def desc_TC_EEVSE_2_2(self) -> str: + """Returns a description of this test""" + return "5.1.3. [TC-EEVSE-2.2] Primary functionality with DUT as Server" + + def pics_TC_EEVSE_2_2(self): + """ This function returns a list of PICS for this test case that must be True for the test to be run""" + # In this case - there is no feature flags needed to run this test case + return None + + def steps_TC_EEVSE_2_2(self) -> list[TestStep]: + steps = [ + TestStep("1", "Commissioning, already done", is_commissioning=True), + TestStep("2", "TH reads TestEventTriggersEnabled attribute from General Diagnostics Cluster. Verify that TestEventTriggersEnabled attribute has a value of 1 (True)"), + TestStep("3", "TH sends TestEventTrigger command to General Diagnostics Cluster on Endpoint 0 with EnableKey field set to PIXIT.EEVSE.TEST_EVENT_TRIGGER_KEY and EventTrigger field set to PIXIT.EEVSE.TEST_EVENT_TRIGGER for Basic Functionality Test Event"), + TestStep("3a", "After a few seconds TH reads from the DUT the State attribute. Verify value is 0x00 (NotPluggedIn)"), + TestStep("3b", "TH reads from the DUT the SupplyState attribute. Verify value is 0x00 (Disabled)"), + TestStep("3c", "TH reads from the DUT the FaultState attribute. Verify value is 0x00 (NoError)"), + TestStep("4", "TH sends TestEventTrigger command to General Diagnostics Cluster on Endpoint 0 with EnableKey field set to PIXIT.EEVSE.TEST_EVENT_TRIGGER_KEY and EventTrigger field set to PIXIT.EEVSE.TEST_EVENT_TRIGGER for EV Plugged-in Test Event. Verify Event EEVSE.S.E00(EVConnected) sent"), + TestStep("4a", "TH reads from the DUT the State attribute. Verify value is 0x01 (PluggedInNoDemand)"), + TestStep("4b", "TH reads from the DUT the SessionID attribute. Value is noted for later"), + TestStep("5", "TH sends command EnableCharging with ChargingEnabledUntil=2 minutes in the future, minimumChargeCurrent=6000, maximumChargeCurrent=60000"), + TestStep("6", "TH sends TestEventTrigger command to General Diagnostics Cluster on Endpoint 0 with EnableKey field set to PIXIT.EEVSE.TEST_EVENT_TRIGGER_KEY and EventTrigger field set to PIXIT.EEVSE.TEST_EVENT_TRIGGER for EV Charge Demand Test Event. Verify Event EEVSE.S.E02(EnergyTransferStarted) sent."), + TestStep("6a", "TH reads from the DUT the State attribute. Verify value is 0x3 (PluggedInCharging)"), + TestStep("6b", "TH reads from the DUT the SupplyState attribute. Verify value is 0x1 (ChargingEnabled)"), + TestStep("6c", "TH reads from the DUT the ChargingEnabledUntil attribute. Verify value is the commanded value"), + TestStep("6d", "TH reads from the DUT the MinimumChargeCurrent attribute. Verify value is the commanded value (6000)"), + TestStep("6e", "TH reads from the DUT the MaximumChargeCurrent attribute. Verify value is the min(command value (60000), CircuitCapacity)"), + TestStep("7", "Wait 2 minutes. Verify Event EEVSE.S.E03(EnergyTransferStopped) sent with reason EvseStopped"), + TestStep("7a", "TH reads from the DUT the State attribute. Verify value is 0x02 (PluggedInDemand)"), + TestStep("7b", "TH reads from the DUT the SupplyState attribute. Verify value is 0x00 (Disabled)"), + TestStep("8", "TH sends command EnableCharging with ChargingEnabledUntil=NULL, minimumChargeCurrent = 6000, maximumChargeCurrent=12000"), + TestStep("8a", "TH reads from the DUT the State attribute. Verify value is 0x03 (PluggedInCharging)"), + TestStep("8b", "TH reads from the DUT the SupplyState attribute. Verify value is 1 (ChargingEnabled)"), + TestStep("8c", "TH reads from the DUT the ChargingEnabledUntil attribute. Verify value is the commanded value (NULL)"), + TestStep("8d", "TH reads from the DUT the MinimumChargeCurrent attribute. Verify value is the commanded value (6000)"), + TestStep("8e", "TH reads from the DUT the MaximumChargeCurrent attribute. Verify value is the MIN(command value (60000), CircuitCapacity)"), + TestStep("9", "If the optional attribute is supported TH writes to the DUT UserMaximumChargeCurrent=6000"), + TestStep("9a", "After a few seconds TH reads from the DUT the MaximumChargeCurrent. Verify value is UserMaximumChargeCurrent value (6000)"), + TestStep("10", "TH sends TestEventTrigger command to General Diagnostics Cluster on Endpoint 0 with EnableKey field set to PIXIT.EEVSE.TEST_EVENT_TRIGGER_KEY and EventTrigger field set to PIXIT.EEVSE.TEST_EVENT_TRIGGER for EV Charge Demand Test Event Clear. Verify Event EEVSE.S.E03(EnergyTransferStopped) sent with reason EvStopped"), + TestStep("10a", "TH reads from the DUT the State attribute. Verify value is 0x01 (PluggedInNoDemand)"), + TestStep("11", "TH sends TestEventTrigger command to General Diagnostics Cluster on Endpoint 0 with EnableKey field set to PIXIT.EEVSE.TEST_EVENT_TRIGGER_KEY and EventTrigger field set to PIXIT.EEVSE.TEST_EVENT_TRIGGER for EV Charge Demand Test Event. Verify Event EEVSE.S.E02(EnergyTransferStarted) sent."), + TestStep("11a", "TH reads from the DUT the State attribute. Verify value is 0x03 (PluggedInCharging)"), + TestStep("12", "TH sends TestEventTrigger command to General Diagnostics Cluster on Endpoint 0 with EnableKey field set to PIXIT.EEVSE.TEST_EVENT_TRIGGER_KEY and EventTrigger field set to PIXIT.EEVSE.TEST_EVENT_TRIGGER for EV Charge Demand Test Event Clear. Verify Event EEVSE.S.E03(EnergyTransferStopped) sent with reason EvStopped"), + TestStep("12a", "TH reads from the DUT the State attribute. Verify value is 0x01 (PluggedInNoDemand)"), + TestStep("13", "TH sends TestEventTrigger command to General Diagnostics Cluster on Endpoint 0 with EnableKey field set to PIXIT.EEVSE.TEST_EVENT_TRIGGER_KEY and EventTrigger field set to PIXIT.EEVSE.TEST_EVENT_TRIGGER for EV Plugged-in Test Event Clear. Verify Event EEVSE.S.E01(EVNotDetected) sent"), + TestStep("13a", "TH reads from the DUT the State attribute. Verify value is 0x00 (NotPluggedIn)"), + TestStep("13b", "TH reads from the DUT the SupplyState attribute. Verify value is 0x01 (ChargingEnabled)"), + TestStep("13c", "TH reads from the DUT the SessionID attribute. Verify value is the same value noted in 4b"), + TestStep("13d", "TH reads from the DUT the SessionDuration attribute. Verify value is greater than 120 (and match the time taken for the tests from step 4 to step 13)"), + TestStep("14", "TH sends TestEventTrigger command to General Diagnostics Cluster on Endpoint 0 with EnableKey field set to PIXIT.EEVSE.TEST_EVENT_TRIGGER_KEY and EventTrigger field set to PIXIT.EEVSE.TEST_EVENT_TRIGGER for EV Plugged-in Test Event. Verify Event EEVSE.S.E00(EVConnected) sent"), + TestStep("14a", "TH sends TestEventTrigger command to General Diagnostics Cluster on Endpoint 0 with EnableKey field set to PIXIT.EEVSE.TEST_EVENT_TRIGGER_KEY and EventTrigger field set to PIXIT.EEVSE.TEST_EVENT_TRIGGER for EV Charge Demand Test Event. Verify Event EEVSE.S.E02(EnergyTransferStarted) sent."), + TestStep("14b", "TH reads from the DUT the SessionID attribute. Verify value is 1 more than the value noted in 4b"), + TestStep("15", "TH sends command Disable. Verify Event EEVSE.S.E03(EnergyTransferStopped) sent with reason EvseStopped"), + TestStep("15a", "TH reads from the DUT the SupplyState attribute. Verify value is 0x00 (Disabled)"), + TestStep("16", "TH sends TestEventTrigger command to General Diagnostics Cluster on Endpoint 0 with EnableKey field set to PIXIT.EEVSE.TEST_EVENT_TRIGGER_KEY and EventTrigger field set to PIXIT.EEVSE.TEST_EVENT_TRIGGER for EV Charge Demand Test Event Clear."), + TestStep("17", "TH sends TestEventTrigger command to General Diagnostics Cluster on Endpoint 0 with EnableKey field set to PIXIT.EEVSE.TEST_EVENT_TRIGGER_KEY and EventTrigger field set to PIXIT.EEVSE.TEST_EVENT_TRIGGER for EV Plugged-in Test Event Clear. Verify Event EEVSE.S.E01(EVNotDetected) sent"), + TestStep("18", "TH sends TestEventTrigger command to General Diagnostics Cluster on Endpoint 0 with EnableKey field set to PIXIT.EEVSE.TEST_EVENT_TRIGGER_KEY and EventTrigger field set to PIXIT.EEVSE.TEST_EVENT_TRIGGER for Basic Functionality Test Event Clear."), + ] + + return steps + + @async_test_body + async def test_TC_EEVSE_2_2(self): + + self.step("1") + # Commission DUT - already done + + # Subscribe to Events and when they are sent push them to a queue for checking later + events_callback = EventChangeCallback(Clusters.EnergyEvse) + await events_callback.start(self.default_controller, + self.dut_node_id, + self.matter_test_config.endpoint) + + self.step("2") + await self.check_test_event_triggers_enabled() + + self.step("3") + await self.send_test_event_trigger_basic() + + # After a few seconds... + time.sleep(1) + + self.step("3a") + await self.check_evse_attribute("State", Clusters.EnergyEvse.Enums.StateEnum.kNotPluggedIn) + + self.step("3b") + await self.check_evse_attribute("SupplyState", Clusters.EnergyEvse.Enums.SupplyStateEnum.kDisabled) + + self.step("3c") + await self.check_evse_attribute("FaultState", Clusters.EnergyEvse.Enums.FaultStateEnum.kNoError) + + self.step("4") + await self.send_test_event_trigger_pluggedin() + event_data = events_callback.WaitForEventReport(Clusters.EnergyEvse.Events.EVConnected) + + self.step("4a") + await self.check_evse_attribute("State", Clusters.EnergyEvse.Enums.StateEnum.kPluggedInNoDemand) + + self.step("4b") + # Save Session ID for later and check it against the value in the event + session_id = await self.read_evse_attribute_expect_success(attribute="SessionID") + self.validate_ev_connected_event(event_data, session_id) + + self.step("5") + charging_duration = 5 # TODO test plan spec says 120s - reduced for now + min_charge_current = 6000 + max_charge_current = 60000 + expected_state = Clusters.EnergyEvse.Enums.StateEnum.kPluggedInCharging + # get epoch time for ChargeUntil variable (2 minutes from now) + utc_time_charging_end = datetime.now(tz=timezone.utc) + timedelta(seconds=charging_duration) + + # Matter epoch is 0 hours, 0 minutes, 0 seconds on Jan 1, 2000 UTC + epoch_time = int((utc_time_charging_end - datetime(2000, 1, 1, 0, 0, 0, 0, timezone.utc)).total_seconds()) + await self.send_enable_charge_command(endpoint=1, charge_until=epoch_time, min_charge=min_charge_current, max_charge=max_charge_current) + + self.step("6") + await self.send_test_event_trigger_charge_demand() + event_data = events_callback.WaitForEventReport(Clusters.EnergyEvse.Events.EnergyTransferStarted) + + self.step("6a") + await self.check_evse_attribute("State", expected_state) + + self.step("6b") + await self.check_evse_attribute("SupplyState", Clusters.EnergyEvse.Enums.SupplyStateEnum.kChargingEnabled) + + self.step("6c") + await self.check_evse_attribute("ChargingEnabledUntil", epoch_time) + + self.step("6d") + await self.check_evse_attribute("MinimumChargeCurrent", min_charge_current) + + self.step("6e") + circuit_capacity = await self.read_evse_attribute_expect_success(attribute="CircuitCapacity") + expected_max_charge = min(max_charge_current, circuit_capacity) + await self.check_evse_attribute("MaximumChargeCurrent", expected_max_charge) + + self.validate_energy_transfer_started_event(event_data, session_id, expected_state, expected_max_charge) + + self.step("7") + # Sleep for the charging duration plus a couple of seconds to check it has stopped + time.sleep(charging_duration + 2) + # check EnergyTransferredStoped (EvseStopped) + event_data = events_callback.WaitForEventReport(Clusters.EnergyEvse.Events.EnergyTransferStopped) + expected_reason = Clusters.EnergyEvse.Enums.EnergyTransferStoppedReasonEnum.kEVSEStopped + self.validate_energy_transfer_stopped_event(event_data, session_id, expected_state, expected_reason) + + self.step("7a") + await self.check_evse_attribute("State", Clusters.EnergyEvse.Enums.StateEnum.kPluggedInDemand) + + self.step("7b") + await self.check_evse_attribute("SupplyState", Clusters.EnergyEvse.Enums.SupplyStateEnum.kDisabled) + + self.step("8") + charge_until = NullValue + min_charge_current = 6000 + max_charge_current = 12000 + + await self.send_enable_charge_command(charge_until=charge_until, min_charge=min_charge_current, max_charge=max_charge_current) + event_data = events_callback.WaitForEventReport(Clusters.EnergyEvse.Events.EnergyTransferStarted) + + self.step("8a") + await self.check_evse_attribute("State", Clusters.EnergyEvse.Enums.StateEnum.kPluggedInCharging) + + self.step("8b") + await self.check_evse_attribute("SupplyState", Clusters.EnergyEvse.Enums.SupplyStateEnum.kChargingEnabled) + + self.step("8c") + await self.check_evse_attribute("ChargingEnabledUntil", charge_until) + + self.step("8d") + await self.check_evse_attribute("MinimumChargeCurrent", min_charge_current) + + self.step("8e") + circuit_capacity = await self.read_evse_attribute_expect_success(attribute="CircuitCapacity") + expected_max_charge = min(max_charge_current, circuit_capacity) + await self.check_evse_attribute("MaximumChargeCurrent", expected_max_charge) + + # from step 8 above - validate event + self.validate_energy_transfer_started_event(event_data, session_id, expected_state, expected_max_charge) + + self.step("9") + # This will only work if the optional UserMaximumChargeCurrent attribute is supported + supported_attributes = await self.get_supported_energy_evse_attributes() + if Clusters.EnergyEvse.Attributes.UserMaximumChargeCurrent.attribute_id in supported_attributes: + logging.info("UserMaximumChargeCurrent is supported...") + user_max_charge_current = 6000 + await self.write_user_max_charge(1, user_max_charge_current) + + self.step("9a") + time.sleep(3) + + expected_max_charge = min(user_max_charge_current, circuit_capacity) + await self.check_evse_attribute("MaximumChargeCurrent", expected_max_charge) + else: + logging.info("UserMaximumChargeCurrent is NOT supported... skipping.") + + self.step("10") + await self.send_test_event_trigger_charge_demand_clear() + event_data = events_callback.WaitForEventReport(Clusters.EnergyEvse.Events.EnergyTransferStopped) + expected_reason = Clusters.EnergyEvse.Enums.EnergyTransferStoppedReasonEnum.kEVStopped + self.validate_energy_transfer_stopped_event(event_data, session_id, expected_state, expected_reason) + + self.step("10a") + await self.check_evse_attribute("State", Clusters.EnergyEvse.Enums.StateEnum.kPluggedInNoDemand) + + self.step("11") + await self.send_test_event_trigger_charge_demand() + # Check we get EnergyTransferStarted again + await self.send_enable_charge_command(charge_until=charge_until, min_charge=min_charge_current, max_charge=max_charge_current) + event_data = events_callback.WaitForEventReport(Clusters.EnergyEvse.Events.EnergyTransferStarted) + self.validate_energy_transfer_started_event(event_data, session_id, expected_state, expected_max_charge) + + self.step("11a") + await self.check_evse_attribute("State", Clusters.EnergyEvse.Enums.StateEnum.kPluggedInCharging) + + self.step("12") + await self.send_test_event_trigger_charge_demand_clear() + event_data = events_callback.WaitForEventReport(Clusters.EnergyEvse.Events.EnergyTransferStopped) + expected_reason = Clusters.EnergyEvse.Enums.EnergyTransferStoppedReasonEnum.kEVStopped + self.validate_energy_transfer_stopped_event(event_data, session_id, expected_state, expected_reason) + + self.step("12a") + await self.check_evse_attribute("State", Clusters.EnergyEvse.Enums.StateEnum.kPluggedInNoDemand) + + self.step("13") + await self.send_test_event_trigger_pluggedin_clear() + event_data = events_callback.WaitForEventReport(Clusters.EnergyEvse.Events.EVNotDetected) + expected_state = Clusters.EnergyEvse.Enums.StateEnum.kPluggedInNoDemand + self.validate_ev_not_detected_event(event_data, session_id, expected_state, expected_duration=0, expected_charged=0) + + self.step("13a") + await self.check_evse_attribute("State", Clusters.EnergyEvse.Enums.StateEnum.kNotPluggedIn) + + self.step("13b") + await self.check_evse_attribute("SupplyState", Clusters.EnergyEvse.Enums.SupplyStateEnum.kChargingEnabled) + + self.step("13c") + await self.check_evse_attribute("SessionID", session_id) + + self.step("13d") + session_duration = await self.read_evse_attribute_expect_success(attribute="SessionDuration") + asserts.assert_greater_equal(session_duration, charging_duration, + f"Unexpected 'SessionDuration' value - expected >= {charging_duration}, was {session_duration}") + + self.step("14") + await self.send_test_event_trigger_pluggedin() + # New plug in means session ID should increase by 1 + session_id = session_id + 1 + + # Check we get a new EVConnected event with updated session ID + event_data = events_callback.WaitForEventReport(Clusters.EnergyEvse.Events.EVConnected) + self.validate_ev_connected_event(event_data, session_id) + + self.step("14a") + await self.send_test_event_trigger_charge_demand() + expected_state = Clusters.EnergyEvse.Enums.StateEnum.kPluggedInCharging # This is the value at the event time + event_data = events_callback.WaitForEventReport(Clusters.EnergyEvse.Events.EnergyTransferStarted) + self.validate_energy_transfer_started_event(event_data, session_id, expected_state, expected_max_charge) + + self.step("14b") + await self.check_evse_attribute("SessionID", session_id) + + self.step("15") + await self.send_disable_command() + expected_state = Clusters.EnergyEvse.Enums.StateEnum.kPluggedInCharging # This is the value prior to stopping + event_data = events_callback.WaitForEventReport(Clusters.EnergyEvse.Events.EnergyTransferStopped) + expected_reason = Clusters.EnergyEvse.Enums.EnergyTransferStoppedReasonEnum.kEVSEStopped + self.validate_energy_transfer_stopped_event(event_data, session_id, expected_state, expected_reason) + + self.step("15a") + await self.check_evse_attribute("SupplyState", Clusters.EnergyEvse.Enums.SupplyStateEnum.kDisabled) + + self.step("16") + await self.send_test_event_trigger_charge_demand_clear() + + self.step("17") + await self.send_test_event_trigger_pluggedin_clear() + event_data = events_callback.WaitForEventReport(Clusters.EnergyEvse.Events.EVNotDetected) + expected_state = Clusters.EnergyEvse.Enums.StateEnum.kPluggedInNoDemand + self.validate_ev_not_detected_event(event_data, session_id, expected_state, expected_duration=0, expected_charged=0) + + self.step("18") + await self.send_test_event_trigger_basic_clear() + + +if __name__ == "__main__": + default_matter_test_main() diff --git a/src/python_testing/TC_EEVSE_2_4.py b/src/python_testing/TC_EEVSE_2_4.py new file mode 100644 index 00000000000000..9379e23332e813 --- /dev/null +++ b/src/python_testing/TC_EEVSE_2_4.py @@ -0,0 +1,185 @@ +# +# Copyright (c) 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. + + +import logging +import time + +import chip.clusters as Clusters +from chip.clusters.Types import NullValue +from matter_testing_support import MatterBaseTest, TestStep, async_test_body, default_matter_test_main +from TC_EEVSE_Utils import EEVSEBaseTestHelper, EventChangeCallback + +logger = logging.getLogger(__name__) + + +class TC_EEVSE_2_4(MatterBaseTest, EEVSEBaseTestHelper): + + def desc_TC_EEVSE_2_4(self) -> str: + """Returns a description of this test""" + return "5.1.XXX. [TC-EEVSE-2.4] Fault test functionality with DUT as Server" + + def pics_TC_EEVSE_2_4(self): + """ This function returns a list of PICS for this test case that must be True for the test to be run""" + # In this case - there is no feature flags needed to run this test case + return None + + def steps_TC_EEVSE_2_4(self) -> list[TestStep]: + steps = [ + TestStep("1", "Commissioning, already done", is_commissioning=True), + TestStep("2", "TH reads TestEventTriggersEnabled attribute from General Diagnostics Cluster. Verify that TestEventTriggersEnabled attribute has a value of 1 (True)"), + TestStep("3", "TH sends TestEventTrigger command to General Diagnostics Cluster on Endpoint 0 with EnableKey field set to PIXIT.EEVSE.TEST_EVENT_TRIGGER_KEY and EventTrigger field set to PIXIT.EEVSE.TEST_EVENT_TRIGGER for Basic Functionality Test Event"), + TestStep("3a", "After a few seconds TH reads from the DUT the State attribute. Verify value is 0x00 (NotPluggedIn)"), + TestStep("3b", "TH reads from the DUT the SupplyState attribute. Verify value is 0x00 (Disabled)"), + TestStep("3c", "TH reads from the DUT the FaultState attribute. Verify value is 0x00 (NoError)"), + TestStep("4", "TH sends TestEventTrigger command to General Diagnostics Cluster on Endpoint 0 with EnableKey field set to PIXIT.EEVSE.TEST_EVENT_TRIGGER_KEY and EventTrigger field set to PIXIT.EEVSE.TEST_EVENT_TRIGGER for EV Plugged-in Test Event. Verify Event EEVSE.S.E00(EVConnected) sent"), + TestStep("4a", "TH reads from the DUT the State attribute. Verify value is 0x01 (PluggedInNoDemand)"), + TestStep("4b", "TH reads from the DUT the SessionID attribute. Value is saved for later"), + TestStep("5", "TH sends command EnableCharging with ChargingEnabledUntil=Null, minimumChargeCurrent=6000, maximumChargeCurrent=60000"), + TestStep("6", "TH sends TestEventTrigger command to General Diagnostics Cluster on Endpoint 0 with EnableKey field set to PIXIT.EEVSE.TEST_EVENT_TRIGGER_KEY and EventTrigger field set to PIXIT.EEVSE.TEST_EVENT_TRIGGER for EV Charge Demand Test Event. Verify Event EEVSE.S.E02(EnergyTransferStarted) sent."), + TestStep("6a", "TH reads from the DUT the State attribute. Verify value is 0x3 (PluggedInCharging)"), + TestStep("6b", "TH reads from the DUT the SupplyState attribute. Verify value is 0x1 (ChargingEnabled)"), + TestStep("7", "TH sends TestEventTrigger command to General Diagnostics Cluster on Endpoint 0 with EnableKey field set to PIXIT.EEVSE.TEST_EVENT_TRIGGER_KEY and EventTrigger field set to PIXIT.EEVSE.TEST_EVENT_TRIGGER for EVSE Ground Fault Test Event. Verify Event EEVSE.S.E04(Fault) sent with SessionID matching value in step 4b, FaultStatePreviousFaultState = 0x00 (NoError), FaultStateCurrentFaultState = 0x07 (GroundFault)"), + TestStep("7a", "TH reads from the DUT the State attribute. Verify value is 0x6 (Fault)"), + TestStep("7b", "TH reads from the DUT the SupplyState attribute. Verify value is 0x4 (DisabledError)"), + TestStep("8", "TH sends TestEventTrigger command to General Diagnostics Cluster on Endpoint 0 with EnableKey field set to PIXIT.EEVSE.TEST_EVENT_TRIGGER_KEY and EventTrigger field set to PIXIT.EEVSE.TEST_EVENT_TRIGGER for EVSE Over Temperature Fault Test Event. Verify Event EEVSE.S.E04(Fault) sent with SessionID matching value in step 4b, FaultStatePreviousFaultState = 0x07 (GroundFault), FaultStateCurrentFaultState = 0x0F (OverTemperature)"), + TestStep("8a", "TH reads from the DUT the State attribute. Verify value is 0x6 (Fault)"), + TestStep("8b", "TH reads from the DUT the SupplyState attribute. Verify value is 0x4 (DisabledError)"), + TestStep("9", "TH sends TestEventTrigger command to General Diagnostics Cluster on Endpoint 0 with EnableKey field set to PIXIT.EEVSE.TEST_EVENT_TRIGGER_KEY and EventTrigger field set to PIXIT.EEVSE.TEST_EVENT_TRIGGER for EVSE Fault Test Event Clear. Verify Event EEVSE.S.E04(Fault) sent with SessionID matching value in step 4b, FaultStatePreviousFaultState = 0x0F (OverTemperature), FaultStateCurrentFaultState = 0x00 (NoError)"), + TestStep("9a", "TH reads from the DUT the State attribute. Verify value is 0x3 (PluggedInCharging)"), + TestStep("9b", "TH reads from the DUT the SupplyState attribute. Verify value is 0x1 (ChargingEnabled)"), + TestStep("10", "TH sends TestEventTrigger command to General Diagnostics Cluster on Endpoint 0 with EnableKey field set to PIXIT.EEVSE.TEST_EVENT_TRIGGER_KEY and EventTrigger field set to PIXIT.EEVSE.TEST_EVENT_TRIGGER for EV Charge Demand Test Event Clear."), + TestStep("11", "TH sends TestEventTrigger command to General Diagnostics Cluster on Endpoint 0 with EnableKey field set to PIXIT.EEVSE.TEST_EVENT_TRIGGER_KEY and EventTrigger field set to PIXIT.EEVSE.TEST_EVENT_TRIGGER for EV Plugged-in Test Event Clear. Verify Event EEVSE.S.E01(EVNotDetected) sent"), + TestStep("12", "TH sends TestEventTrigger command to General Diagnostics Cluster on Endpoint 0 with EnableKey field set to PIXIT.EEVSE.TEST_EVENT_TRIGGER_KEY and EventTrigger field set to PIXIT.EEVSE.TEST_EVENT_TRIGGER for Basic Functionality Test Event Clear."), + ] + + return steps + + @async_test_body + async def test_TC_EEVSE_2_4(self): + self.step("1") + # Commission DUT - already done + + # Subscribe to Events and when they are sent push them to a queue for checking later + events_callback = EventChangeCallback(Clusters.EnergyEvse) + await events_callback.start(self.default_controller, + self.dut_node_id, + self.matter_test_config.endpoint) + + self.step("2") + await self.check_test_event_triggers_enabled() + + self.step("3") + await self.send_test_event_trigger_basic() + + # After a few seconds... + time.sleep(3) + + self.step("3a") + await self.check_evse_attribute("State", Clusters.EnergyEvse.Enums.StateEnum.kNotPluggedIn) + + self.step("3b") + await self.check_evse_attribute("SupplyState", Clusters.EnergyEvse.Enums.SupplyStateEnum.kDisabled) + + self.step("3c") + await self.check_evse_attribute("FaultState", Clusters.EnergyEvse.Enums.FaultStateEnum.kNoError) + + self.step("4") + await self.send_test_event_trigger_pluggedin() + event_data = events_callback.WaitForEventReport(Clusters.EnergyEvse.Events.EVConnected) + + self.step("4a") + await self.check_evse_attribute("State", Clusters.EnergyEvse.Enums.StateEnum.kPluggedInNoDemand) + + self.step("4b") + # Save Session ID for later and check it against the value in the event + session_id = await self.read_evse_attribute_expect_success(attribute="SessionID") + self.validate_ev_connected_event(event_data, session_id) + + self.step("5") + charge_until = NullValue + min_charge_current = 6000 + max_charge_current = 60000 + await self.send_enable_charge_command(charge_until=charge_until, min_charge=min_charge_current, max_charge=max_charge_current) + + self.step("6") + await self.send_test_event_trigger_charge_demand() + event_data = events_callback.WaitForEventReport(Clusters.EnergyEvse.Events.EnergyTransferStarted) + + self.step("6a") + await self.check_evse_attribute("State", Clusters.EnergyEvse.Enums.StateEnum.kPluggedInCharging) + + self.step("6b") + await self.check_evse_attribute("SupplyState", Clusters.EnergyEvse.Enums.SupplyStateEnum.kChargingEnabled) + + self.step("7") + await self.send_test_event_trigger_evse_ground_fault() + event_data = events_callback.WaitForEventReport(Clusters.EnergyEvse.Events.Fault) + expected_state = Clusters.EnergyEvse.Enums.StateEnum.kPluggedInCharging + previous_fault = Clusters.EnergyEvse.Enums.FaultStateEnum.kNoError + current_fault = Clusters.EnergyEvse.Enums.FaultStateEnum.kGroundFault + self.validate_evse_fault_event(event_data, session_id, expected_state, previous_fault, current_fault) + + self.step("7a") + await self.check_evse_attribute("State", Clusters.EnergyEvse.Enums.StateEnum.kFault) + + self.step("7b") + await self.check_evse_attribute("SupplyState", Clusters.EnergyEvse.Enums.SupplyStateEnum.kDisabledError) + + self.step("8") + await self.send_test_event_trigger_evse_over_temperature_fault() + event_data = events_callback.WaitForEventReport(Clusters.EnergyEvse.Events.Fault) + expected_state = Clusters.EnergyEvse.Enums.StateEnum.kFault + previous_fault = Clusters.EnergyEvse.Enums.FaultStateEnum.kGroundFault + current_fault = Clusters.EnergyEvse.Enums.FaultStateEnum.kOverTemperature + self.validate_evse_fault_event(event_data, session_id, expected_state, previous_fault, current_fault) + + self.step("8a") + await self.check_evse_attribute("State", Clusters.EnergyEvse.Enums.StateEnum.kFault) + + self.step("8b") + await self.check_evse_attribute("SupplyState", Clusters.EnergyEvse.Enums.SupplyStateEnum.kDisabledError) + + self.step("9") + await self.send_test_event_trigger_evse_fault_clear() + event_data = events_callback.WaitForEventReport(Clusters.EnergyEvse.Events.Fault) + expected_state = Clusters.EnergyEvse.Enums.StateEnum.kFault + previous_fault = Clusters.EnergyEvse.Enums.FaultStateEnum.kOverTemperature + current_fault = Clusters.EnergyEvse.Enums.FaultStateEnum.kNoError + self.validate_evse_fault_event(event_data, session_id, expected_state, previous_fault, current_fault) + + self.step("9a") + await self.check_evse_attribute("State", Clusters.EnergyEvse.Enums.StateEnum.kPluggedInCharging) + + self.step("9b") + await self.check_evse_attribute("SupplyState", Clusters.EnergyEvse.Enums.SupplyStateEnum.kChargingEnabled) + + self.step("10") + await self.send_test_event_trigger_charge_demand_clear() + event_data = events_callback.WaitForEventReport(Clusters.EnergyEvse.Events.EnergyTransferStopped) + + self.step("11") + await self.send_test_event_trigger_pluggedin_clear() + event_data = events_callback.WaitForEventReport(Clusters.EnergyEvse.Events.EVNotDetected) + expected_state = Clusters.EnergyEvse.Enums.StateEnum.kPluggedInNoDemand + self.validate_ev_not_detected_event(event_data, session_id, expected_state, expected_duration=0, expected_charged=0) + + self.step("12") + await self.send_test_event_trigger_basic_clear() + + +if __name__ == "__main__": + default_matter_test_main() diff --git a/src/python_testing/TC_EEVSE_2_5.py b/src/python_testing/TC_EEVSE_2_5.py new file mode 100644 index 00000000000000..34ca0151935f93 --- /dev/null +++ b/src/python_testing/TC_EEVSE_2_5.py @@ -0,0 +1,137 @@ +# +# Copyright (c) 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. + + +import logging + +import chip.clusters as Clusters +from chip.clusters.Types import NullValue +from chip.interaction_model import Status +from matter_testing_support import MatterBaseTest, TestStep, async_test_body, default_matter_test_main +from TC_EEVSE_Utils import EEVSEBaseTestHelper, EventChangeCallback + +logger = logging.getLogger(__name__) + + +class TC_EEVSE_2_5(MatterBaseTest, EEVSEBaseTestHelper): + + def desc_TC_EEVSE_2_5(self) -> str: + """Returns a description of this test""" + return "5.1.XXX. [TC-EEVSE-2.4] Fault test functionality with DUT as Server" + + def pics_TC_EEVSE_2_5(self): + """ This function returns a list of PICS for this test case that must be True for the test to be run""" + # In this case - there is no feature flags needed to run this test case + return None + + def steps_TC_EEVSE_2_5(self) -> list[TestStep]: + steps = [ + TestStep("1", "Commissioning, already done", is_commissioning=True), + TestStep("2", "TH reads TestEventTriggersEnabled attribute from General Diagnostics Cluster. Verify that TestEventTriggersEnabled attribute has a value of 1 (True)"), + TestStep("3", "TH sends TestEventTrigger command to General Diagnostics Cluster on Endpoint 0 with EnableKey field set to PIXIT.EEVSE.TEST_EVENT_TRIGGER_KEY and EventTrigger field set to PIXIT.EEVSE.TEST_EVENT_TRIGGER for Basic Functionality Test Event"), + TestStep("3a", "TH reads from the DUT the State attribute. Verify value is 0x00 (NotPluggedIn)"), + TestStep("3b", "TH reads from the DUT the SupplyState attribute. Verify value is 0x00 (Disabled)"), + TestStep("3c", "TH reads from the DUT the FaultState attribute. Verify value is 0x00 (NoError)"), + TestStep("4", "TH sends command EnableCharging with ChargingEnabledUntil=Null, minimumChargeCurrent=6000, maximumChargeCurrent=60000"), + TestStep("4a", "TH reads from the DUT the State attribute. Verify value is 0x00 (NotPluggedIn)"), + TestStep("4b", "TH reads from the DUT the SupplyState attribute. Verify value is 0x01 (ChargingEnabled)"), + TestStep("5", "TH sends command StartDiagnostics. Verify that command is rejected with Failure"), + TestStep("6", "TH sends command Disable."), + TestStep("6a", "TH reads from the DUT the State attribute. Verify value is 0x00 (NotPluggedIn)"), + TestStep("6b", "TH reads from the DUT the SupplyState attribute. Verify value is 0x00 (Disabled)"), + TestStep("7", "TH sends command StartDiagnostics. Verify that command is accepted with Success"), + TestStep("7a", "TH reads from the DUT the SupplyState attribute. Verify value is 0x04 (DisabledDiagnostics)"), + TestStep("8", "A few seconds later TH sends TestEventTrigger command to General Diagnostics Cluster on Endpoint 0 with EnableKey field set to PIXIT.EEVSE.TEST_EVENT_TRIGGER_KEY and EventTrigger field set to PIXIT.EEVSE.TEST_EVENT_TRIGGER for EVSE Diagnostics Complete Event"), + TestStep("8a", "TH reads from the DUT the State attribute. Verify value is 0x00 (NotPluggedIn)"), + TestStep("8b", "TH reads from the DUT the SupplyState attribute. Verify value is 0x00 (Disabled)"), + TestStep("9", "TH sends TestEventTrigger command to General Diagnostics Cluster on Endpoint 0 with EnableKey field set to PIXIT.EEVSE.TEST_EVENT_TRIGGER_KEY and EventTrigger field set to PIXIT.EEVSE.TEST_EVENT_TRIGGER for Basic Functionality Test Event Clear."), + ] + + return steps + + @async_test_body + async def test_TC_EEVSE_2_5(self): + self.step("1") + # Commission DUT - already done + + # Subscribe to Events and when they are sent push them to a queue for checking later + events_callback = EventChangeCallback(Clusters.EnergyEvse) + await events_callback.start(self.default_controller, + self.dut_node_id, + self.matter_test_config.endpoint) + + self.step("2") + await self.check_test_event_triggers_enabled() + + self.step("3") + await self.send_test_event_trigger_basic() + + self.step("3a") + await self.check_evse_attribute("State", Clusters.EnergyEvse.Enums.StateEnum.kNotPluggedIn) + + self.step("3b") + await self.check_evse_attribute("SupplyState", Clusters.EnergyEvse.Enums.SupplyStateEnum.kDisabled) + + self.step("3c") + await self.check_evse_attribute("FaultState", Clusters.EnergyEvse.Enums.FaultStateEnum.kNoError) + + self.step("4") + charge_until = NullValue + min_charge_current = 6000 + max_charge_current = 60000 + await self.send_enable_charge_command(charge_until=charge_until, min_charge=min_charge_current, max_charge=max_charge_current) + + self.step("4a") + await self.check_evse_attribute("State", Clusters.EnergyEvse.Enums.StateEnum.kNotPluggedIn) + + self.step("4b") + await self.check_evse_attribute("SupplyState", Clusters.EnergyEvse.Enums.SupplyStateEnum.kChargingEnabled) + + self.step("5") + # Check we get a failure because the state needs to be Disabled to run a Diagnostic + await self.send_start_diagnostics_command(expected_status=Status.Failure) + + self.step("6") + await self.send_disable_command() + + self.step("6a") + await self.check_evse_attribute("State", Clusters.EnergyEvse.Enums.StateEnum.kNotPluggedIn) + + self.step("6b") + await self.check_evse_attribute("SupplyState", Clusters.EnergyEvse.Enums.SupplyStateEnum.kDisabled) + + self.step("7") + await self.send_start_diagnostics_command() + + self.step("7a") + await self.check_evse_attribute("SupplyState", Clusters.EnergyEvse.Enums.SupplyStateEnum.kDisabledDiagnostics) + + self.step("8") + await self.send_test_event_trigger_evse_diagnostics_complete() + + self.step("8a") + await self.check_evse_attribute("State", Clusters.EnergyEvse.Enums.StateEnum.kNotPluggedIn) + + self.step("8b") + # It should stay disabled after a diagnostics session + await self.check_evse_attribute("SupplyState", Clusters.EnergyEvse.Enums.SupplyStateEnum.kDisabled) + + self.step("9") + await self.send_test_event_trigger_basic_clear() + + +if __name__ == "__main__": + default_matter_test_main() diff --git a/src/python_testing/TC_EEVSE_Utils.py b/src/python_testing/TC_EEVSE_Utils.py new file mode 100644 index 00000000000000..fb6bfa1f714d13 --- /dev/null +++ b/src/python_testing/TC_EEVSE_Utils.py @@ -0,0 +1,209 @@ +# +# Copyright (c) 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. + + +import logging +import queue + +import chip.clusters as Clusters +from chip.clusters import ClusterObjects as ClusterObjects +from chip.clusters.Attribute import EventReadResult, SubscriptionTransaction +from chip.interaction_model import InteractionModelError, Status +from mobly import asserts + +logger = logging.getLogger(__name__) + + +class EventChangeCallback: + def __init__(self, expected_cluster: ClusterObjects): + self._q = queue.Queue() + self._expected_cluster = expected_cluster + + async def start(self, dev_ctrl, node_id: int, endpoint: int): + self._subscription = await dev_ctrl.ReadEvent(node_id, + events=[(endpoint, self._expected_cluster, True)], reportInterval=(1, 5), + fabricFiltered=False, keepSubscriptions=True, autoResubscribe=False) + self._subscription.SetEventUpdateCallback(self.__call__) + + def __call__(self, res: EventReadResult, transaction: SubscriptionTransaction): + if res.Status == Status.Success and res.Header.ClusterId == self._expected_cluster.id: + logging.info( + f'Got subscription report for event on cluster {self._expected_cluster}: {res.Data}') + self._q.put(res) + + def WaitForEventReport(self, expected_event: ClusterObjects.ClusterEvent): + try: + res = self._q.get(block=True, timeout=10) + except queue.Empty: + asserts.fail("Failed to receive a report for the event {}".format(expected_event)) + + asserts.assert_equal(res.Header.ClusterId, expected_event.cluster_id, "Expected cluster ID not found in event report") + asserts.assert_equal(res.Header.EventId, expected_event.event_id, "Expected event ID not found in event report") + return res.Data + + +class EEVSEBaseTestHelper: + + async def read_evse_attribute_expect_success(self, endpoint: int = None, attribute: str = ""): + full_attr = getattr(Clusters.EnergyEvse.Attributes, attribute) + cluster = Clusters.Objects.EnergyEvse + return await self.read_single_attribute_check_success(endpoint=endpoint, cluster=cluster, attribute=full_attr) + + async def check_evse_attribute(self, attribute, expected_value, endpoint: int = None): + value = await self.read_evse_attribute_expect_success(endpoint=endpoint, attribute=attribute) + asserts.assert_equal(value, expected_value, + f"Unexpected '{attribute}' value - expected {expected_value}, was {value}") + + async def get_supported_energy_evse_attributes(self, endpoint: int = None): + return await self.read_evse_attribute_expect_success(endpoint, "AttributeList") + + async def write_user_max_charge(self, endpoint: int = None, user_max_charge: int = 0): + if endpoint is None: + endpoint = self.matter_test_config.endpoint + result = await self.default_controller.WriteAttribute(self.dut_node_id, + [(endpoint, + Clusters.EnergyEvse.Attributes.UserMaximumChargeCurrent(user_max_charge))]) + asserts.assert_equal(result[0].Status, Status.Success, "UserMaximumChargeCurrent write failed") + + async def send_enable_charge_command(self, endpoint: int = None, charge_until: int = None, timedRequestTimeoutMs: int = 3000, + min_charge: int = None, max_charge: int = None, expected_status: Status = Status.Success): + try: + await self.send_single_cmd(cmd=Clusters.EnergyEvse.Commands.EnableCharging( + chargingEnabledUntil=charge_until, + minimumChargeCurrent=min_charge, + maximumChargeCurrent=max_charge), + endpoint=endpoint, + timedRequestTimeoutMs=timedRequestTimeoutMs) + + except InteractionModelError as e: + asserts.assert_equal(e.status, expected_status, "Unexpected error returned") + + async def send_disable_command(self, endpoint: int = None, timedRequestTimeoutMs: int = 3000, expected_status: Status = Status.Success): + try: + await self.send_single_cmd(cmd=Clusters.EnergyEvse.Commands.Disable(), + endpoint=endpoint, + timedRequestTimeoutMs=timedRequestTimeoutMs) + + except InteractionModelError as e: + asserts.assert_equal(e.status, expected_status, "Unexpected error returned") + + async def send_start_diagnostics_command(self, endpoint: int = None, timedRequestTimeoutMs: int = 3000, + expected_status: Status = Status.Success): + try: + await self.send_single_cmd(cmd=Clusters.EnergyEvse.Commands.StartDiagnostics(), + endpoint=endpoint, + timedRequestTimeoutMs=timedRequestTimeoutMs) + + except InteractionModelError as e: + asserts.assert_equal(e.status, expected_status, "Unexpected error returned") + + async def send_test_event_triggers(self, enableKey: bytes = None, eventTrigger=0x0099000000000000): + # get the test event enable key or assume the default + # This can be passed in on command line using + # --hex-arg enableKey:000102030405060708090a0b0c0d0e0f + if enableKey is None: + if 'enableKey' not in self.matter_test_config.global_test_params: + enableKey = bytes([b for b in range(16)]) + else: + enableKey = self.matter_test_config.global_test_params['enableKey'] + + try: + # GeneralDiagnosics cluster is meant to be on Endpoint 0 (Root) + await self.send_single_cmd(endpoint=0, + cmd=Clusters.GeneralDiagnostics.Commands.TestEventTrigger( + enableKey, + eventTrigger) + ) + + except InteractionModelError as e: + asserts.fail(f"Unexpected error returned - {e.status}") + + async def check_test_event_triggers_enabled(self): + full_attr = Clusters.GeneralDiagnostics.Attributes.TestEventTriggersEnabled + cluster = Clusters.Objects.GeneralDiagnostics + # GeneralDiagnosics cluster is meant to be on Endpoint 0 (Root) + test_event_enabled = await self.read_single_attribute_check_success(endpoint=0, cluster=cluster, attribute=full_attr) + asserts.assert_equal(test_event_enabled, True, "TestEventTriggersEnabled is False") + + async def send_test_event_trigger_basic(self): + await self.send_test_event_triggers(eventTrigger=0x0099000000000000) + + async def send_test_event_trigger_basic_clear(self): + await self.send_test_event_triggers(eventTrigger=0x0099000000000001) + + async def send_test_event_trigger_pluggedin(self): + await self.send_test_event_triggers(eventTrigger=0x0099000000000002) + + async def send_test_event_trigger_pluggedin_clear(self): + await self.send_test_event_triggers(eventTrigger=0x0099000000000003) + + async def send_test_event_trigger_charge_demand(self): + await self.send_test_event_triggers(eventTrigger=0x0099000000000004) + + async def send_test_event_trigger_charge_demand_clear(self): + await self.send_test_event_triggers(eventTrigger=0x0099000000000005) + + async def send_test_event_trigger_evse_ground_fault(self): + await self.send_test_event_triggers(eventTrigger=0x0099000000000010) + + async def send_test_event_trigger_evse_over_temperature_fault(self): + await self.send_test_event_triggers(eventTrigger=0x0099000000000011) + + async def send_test_event_trigger_evse_fault_clear(self): + await self.send_test_event_triggers(eventTrigger=0x0099000000000012) + + async def send_test_event_trigger_evse_diagnostics_complete(self): + await self.send_test_event_triggers(eventTrigger=0x0099000000000020) + + def validate_energy_transfer_started_event(self, event_data, session_id, expected_state, expected_max_charge): + asserts.assert_equal(session_id, event_data.sessionID, + f"EnergyTransferStarted event session ID was {event_data.sessionID}, expected {session_id}") + asserts.assert_equal(expected_state, event_data.state, + f"EnergyTransferStarted event State was {event_data.state} expected {expected_state}") + asserts.assert_equal(expected_max_charge, event_data.maximumCurrent, + f"EnergyTransferStarted event maximumCurrent was {event_data.maximumCurrent}, expected {expected_max_charge}") + + def validate_energy_transfer_stopped_event(self, event_data, session_id, expected_state, expected_reason): + asserts.assert_equal(session_id, event_data.sessionID, + f"EnergyTransferStopped event session ID was {event_data.sessionID}, expected {session_id}") + asserts.assert_equal(expected_state, event_data.state, + f"EnergyTransferStopped event State was {event_data.state} expected {expected_state}") + asserts.assert_equal(expected_reason, event_data.reason, + f"EnergyTransferStopped event reason was {event_data.reason}, expected {expected_reason}") + + def validate_ev_connected_event(self, event_data, session_id): + asserts.assert_equal(session_id, event_data.sessionID, + f"EvConnected event session ID was {event_data.sessionID}, expected {session_id}") + + def validate_ev_not_detected_event(self, event_data, session_id, expected_state, expected_duration, expected_charged): + asserts.assert_equal(session_id, event_data.sessionID, + f"EvNotDetected event session ID was {event_data.sessionID}, expected {session_id}") + asserts.assert_equal(expected_state, event_data.state, + f"EvNotDetected event event State was {event_data.state} expected {expected_state}") + asserts.assert_greater_equal(event_data.sessionDuration, expected_duration, + f"EvNotDetected event sessionDuration was {event_data.sessionDuration}, expected >= {expected_duration}") + asserts.assert_greater_equal(event_data.sessionEnergyCharged, expected_charged, + f"EvNotDetected event sessionEnergyCharged was {event_data.sessionEnergyCharged}, expected >= {expected_charged}") + + def validate_evse_fault_event(self, event_data, session_id, expected_state, previous_fault, current_fault): + asserts.assert_equal(session_id, event_data.sessionID, + f"Fault event session ID was {event_data.sessionID}, expected {session_id}") + asserts.assert_equal(expected_state, event_data.state, + f"Fault event State was {event_data.state} expected {expected_state}") + asserts.assert_equal(event_data.faultStatePreviousState, previous_fault, + f"Fault event faultStatePreviousState was {event_data.faultStatePreviousState}, expected {previous_fault}") + asserts.assert_equal(event_data.faultStateCurrentState, current_fault, + f"Fault event faultStateCurrentState was {event_data.faultStateCurrentState}, expected {current_fault}")