// SPDX-License-Identifier: Apache-2.0 // Copyright Pionix GmbH and Contributors to EVerest #include "generated/types/evse_manager.hpp" #include #include #include #include namespace { using namespace module; using namespace types::evse_manager; // ---------------------------------------------------------------------------- // test classes // class that provides access to internal state from the Charger class struct ChargerDerived : public Charger { using Charger::Charger; using Charger::get_enable_disable_source_table; using Charger::get_shared_context; using Charger::run_state_machine; // updated when a non-zero connector is used to enable_disable() constexpr const auto& connector_enabled() { return get_shared_context().connector_enabled; } constexpr const auto& current_state() { return get_shared_context().current_state; } constexpr void current_state(EvseState state) { get_shared_context().current_state = state; } constexpr const auto& flag_disable_requested() { return get_shared_context().flag_disable_requested; } }; // class that creates a consistent starting state for tests struct ChargerTest : public testing::Test { // charger requirements std::unique_ptr charger_bsp; std::unique_ptr charger_error_handling; std::vector> charger_powermeter_billing; std::unique_ptr charger_store; // error handling requirements std::unique_ptr error_handler_bsp; std::vector> error_handler_hlc; std::vector> error_handler_connector_lock; std::vector> error_handler_ac_rcd; std::unique_ptr error_handler_evse; std::vector> error_handler_imd; std::vector> error_handler_powersupply; std::vector> error_handler_powermeter; std::vector> error_handler_over_voltage_monitor; std::unique_ptr charger; ChargerTest() : charger_error_handling(std::make_unique( error_handler_bsp, error_handler_hlc, error_handler_connector_lock, error_handler_ac_rcd, error_handler_evse, error_handler_imd, error_handler_powersupply, error_handler_powermeter, error_handler_over_voltage_monitor, false)) { } void SetUp() override { reset_last_event(); charger = std::make_unique( charger_bsp, charger_error_handling, charger_powermeter_billing, charger_store, types::evse_board_support::Connector_type::IEC62196Type2Socket, "EVSETEST"); charger->signal_simple_event.connect(&ChargerTest::session_event, this); } void TearDown() override { charger.reset(nullptr); } void session_event(SessionEventEnum event) { last_event = event; } static constexpr SessionEventEnum default_event{SessionEventEnum::SessionFinished}; static constexpr EnableDisableSource default_source{Enable_source::Unspecified, Enable_state::Unassigned, 10000}; SessionEventEnum last_event{default_event}; constexpr void reset_last_event() { last_event = default_event; } }; // ---------------------------------------------------------------------------- // tests for enable_disable() // interesting variables: // - enable_disable_source_table (not directly available) // - shared_context.connector_enabled - charger->connector_enabled() // - shared_context.current_state - charger->current_state() // - signal_simple_event - last_event TEST_F(ChargerTest, EnableDisableSourceInit) { // check the default values on startup // this is the starting point for all tests const auto last_source = charger->get_last_enable_disable_source(); EXPECT_EQ(last_source, default_source); EXPECT_EQ(last_event, default_event); EXPECT_TRUE(charger->connector_enabled()); EXPECT_EQ(charger->current_state(), Charger::EvseState::Idle); } TEST_F(ChargerTest, EnableDisableSourceInitPlusStateEnabled) { charger->current_state(Charger::EvseState::Idle); // check the default values on startup // this is the starting point for all tests auto last_source = charger->get_last_enable_disable_source(); EXPECT_EQ(last_source, default_source); EXPECT_EQ(last_event, default_event); EXPECT_TRUE(charger->connector_enabled()); EXPECT_EQ(charger->current_state(), Charger::EvseState::Idle); reset_last_event(); constexpr EnableDisableSource enable_default{Enable_source::Unspecified, Enable_state::Enable, 10000}; charger->enable_disable_initial_state_publish(); last_source = charger->get_last_enable_disable_source(); EXPECT_EQ(last_source, enable_default); EXPECT_EQ(last_event, SessionEventEnum::Enabled); EXPECT_TRUE(charger->connector_enabled()); EXPECT_EQ(charger->current_state(), Charger::EvseState::Idle); // already enabled so no event reset_last_event(); constexpr EnableDisableSource enable_source{Enable_source::CSMS, Enable_state::Enable, 100}; EXPECT_TRUE(charger->enable_disable(0, enable_source)); last_source = charger->get_last_enable_disable_source(); EXPECT_EQ(last_source, enable_source); EXPECT_EQ(last_event, default_event); } TEST_F(ChargerTest, EnableDisableSourceInitPlusStateDisabled) { charger->current_state(Charger::EvseState::Disabled); // check the default values on startup // this is the starting point for all tests auto last_source = charger->get_last_enable_disable_source(); EXPECT_EQ(last_source, default_source); EXPECT_EQ(last_event, default_event); EXPECT_TRUE(charger->connector_enabled()); EXPECT_EQ(charger->current_state(), Charger::EvseState::Disabled); reset_last_event(); constexpr EnableDisableSource disable_default{Enable_source::Unspecified, Enable_state::Disable, 10000}; charger->enable_disable_initial_state_publish(); last_source = charger->get_last_enable_disable_source(); EXPECT_EQ(last_source, disable_default); EXPECT_EQ(last_event, SessionEventEnum::Disabled); EXPECT_TRUE(charger->connector_enabled()); EXPECT_EQ(charger->current_state(), Charger::EvseState::Disabled); // already disabled so no event reset_last_event(); constexpr EnableDisableSource disable_source{Enable_source::CSMS, Enable_state::Disable, 100}; EXPECT_FALSE(charger->enable_disable(0, disable_source)); last_source = charger->get_last_enable_disable_source(); EXPECT_EQ(last_source, disable_source); EXPECT_EQ(last_event, default_event); } TEST_F(ChargerTest, EnableDisableSourceConnectorEnabled0) { // connector_enabled must only change when a non-zero connector ID is used ASSERT_TRUE(charger->connector_enabled()); constexpr EnableDisableSource enable_source{Enable_source::CSMS, Enable_state::Enable, 100}; constexpr EnableDisableSource disable_source{Enable_source::CSMS, Enable_state::Disable, 100}; // test with connector ID 0: connector_enabled must not change EXPECT_FALSE(charger->enable_disable(0, disable_source)); EXPECT_TRUE(charger->connector_enabled()); EXPECT_EQ(charger->current_state(), Charger::EvseState::Disabled); EXPECT_TRUE(charger->enable_disable(0, enable_source)); EXPECT_TRUE(charger->connector_enabled()); // tricky case, connector_enabled is true but the change was to connector 0 // this is what the original code does ... EXPECT_EQ(charger->current_state(), Charger::EvseState::Idle); // force connector_enabled false EXPECT_FALSE(charger->enable_disable(1, disable_source)); EXPECT_FALSE(charger->connector_enabled()); // test with connector ID 0: connector_enabled must not change EXPECT_FALSE(charger->enable_disable(0, disable_source)); EXPECT_FALSE(charger->connector_enabled()); EXPECT_EQ(charger->current_state(), Charger::EvseState::Disabled); EXPECT_TRUE(charger->enable_disable(0, enable_source)); EXPECT_FALSE(charger->connector_enabled()); // enable on connector 0 does not change state to Idle EXPECT_EQ(charger->current_state(), Charger::EvseState::Disabled); } TEST_F(ChargerTest, EnableDisableSourceConnectorEnabled1) { // connector_enabled must only change when a non-zero connector ID is used ASSERT_TRUE(charger->connector_enabled()); constexpr EnableDisableSource enable_source{Enable_source::CSMS, Enable_state::Enable, 100}; constexpr EnableDisableSource disable_source{Enable_source::CSMS, Enable_state::Disable, 100}; // test with connector ID 1: connector_enabled must change EXPECT_FALSE(charger->enable_disable(1, disable_source)); EXPECT_FALSE(charger->connector_enabled()); EXPECT_EQ(charger->current_state(), Charger::EvseState::Disabled); EXPECT_TRUE(charger->enable_disable(1, enable_source)); EXPECT_TRUE(charger->connector_enabled()); EXPECT_EQ(charger->current_state(), Charger::EvseState::Idle); EXPECT_FALSE(charger->enable_disable(1, disable_source)); EXPECT_FALSE(charger->connector_enabled()); EXPECT_EQ(charger->current_state(), Charger::EvseState::Disabled); } constexpr EnableDisableSource enable_source_10{Enable_source::CSMS, Enable_state::Enable, 10}; constexpr EnableDisableSource enable_source_100{Enable_source::CSMS, Enable_state::Enable, 100}; constexpr EnableDisableSource enable_source_1000{Enable_source::CSMS, Enable_state::Enable, 1000}; constexpr EnableDisableSource disable_source_10{Enable_source::CSMS, Enable_state::Disable, 10}; constexpr EnableDisableSource disable_source_100{Enable_source::CSMS, Enable_state::Disable, 100}; constexpr EnableDisableSource disable_source_1000{Enable_source::CSMS, Enable_state::Disable, 1000}; constexpr EnableDisableSource unassigned_source_10{Enable_source::CSMS, Enable_state::Unassigned, 10}; constexpr EnableDisableSource unassigned_source_100{Enable_source::CSMS, Enable_state::Unassigned, 100}; constexpr EnableDisableSource unassigned_source_1000{Enable_source::CSMS, Enable_state::Unassigned, 1000}; TEST_F(ChargerTest, EnableDisableTableSingleSource) { // enable_disable settings are added to a table // parse_enable_disable_source_table() processes the table and updates // active_enable_disable_source which is available via get_last_enable_disable_source() // enable_disable() updates the table and calls parse_enable_disable_source_table() // EnableDisableTable // Evaluation will be done based on priorities. 0 is the highest priority, 10000 the lowest // If all sources are unassigned, the connector is enabled // If two sources have the same priority, "disabled" has priority over "enabled" const int connector_id = 1; // use a consistent value const auto& enable_disable_source_table = charger->get_enable_disable_source_table(); EXPECT_TRUE(enable_disable_source_table.empty()); EXPECT_EQ(enable_disable_source_table.size(), 0); auto last_source = charger->get_last_enable_disable_source(); EXPECT_EQ(last_source, default_source); // check source change of state EXPECT_TRUE(charger->enable_disable(connector_id, enable_source_100)); EXPECT_EQ(enable_disable_source_table.size(), 1); last_source = charger->get_last_enable_disable_source(); EXPECT_EQ(last_source, enable_source_100); EXPECT_FALSE(charger->enable_disable(connector_id, disable_source_100)); EXPECT_EQ(enable_disable_source_table.size(), 1); last_source = charger->get_last_enable_disable_source(); EXPECT_EQ(last_source, disable_source_100); EXPECT_TRUE(charger->enable_disable(connector_id, unassigned_source_100)); EXPECT_EQ(enable_disable_source_table.size(), 1); last_source = charger->get_last_enable_disable_source(); // unexpected EXPECT_EQ(last_source, default_source); // check source change of priority EXPECT_TRUE(charger->enable_disable(connector_id, enable_source_1000)); EXPECT_EQ(enable_disable_source_table.size(), 1); last_source = charger->get_last_enable_disable_source(); EXPECT_EQ(last_source, enable_source_1000); EXPECT_TRUE(charger->enable_disable(connector_id, enable_source_100)); EXPECT_EQ(enable_disable_source_table.size(), 1); last_source = charger->get_last_enable_disable_source(); EXPECT_EQ(last_source, enable_source_100); EXPECT_TRUE(charger->enable_disable(connector_id, enable_source_10)); EXPECT_EQ(enable_disable_source_table.size(), 1); last_source = charger->get_last_enable_disable_source(); EXPECT_EQ(last_source, enable_source_10); EXPECT_FALSE(charger->enable_disable(connector_id, disable_source_1000)); EXPECT_EQ(enable_disable_source_table.size(), 1); last_source = charger->get_last_enable_disable_source(); EXPECT_EQ(last_source, disable_source_1000); EXPECT_FALSE(charger->enable_disable(connector_id, disable_source_100)); EXPECT_EQ(enable_disable_source_table.size(), 1); last_source = charger->get_last_enable_disable_source(); EXPECT_EQ(last_source, disable_source_100); EXPECT_FALSE(charger->enable_disable(connector_id, disable_source_10)); EXPECT_EQ(enable_disable_source_table.size(), 1); last_source = charger->get_last_enable_disable_source(); EXPECT_EQ(last_source, disable_source_10); // last_source is always the default since Unassigned // entries are ignored EXPECT_TRUE(charger->enable_disable(connector_id, unassigned_source_1000)); EXPECT_EQ(enable_disable_source_table.size(), 1); last_source = charger->get_last_enable_disable_source(); EXPECT_EQ(last_source, default_source); EXPECT_TRUE(charger->enable_disable(connector_id, unassigned_source_100)); EXPECT_EQ(enable_disable_source_table.size(), 1); last_source = charger->get_last_enable_disable_source(); EXPECT_EQ(last_source, default_source); EXPECT_TRUE(charger->enable_disable(connector_id, unassigned_source_10)); EXPECT_EQ(enable_disable_source_table.size(), 1); last_source = charger->get_last_enable_disable_source(); EXPECT_EQ(last_source, default_source); } TEST_F(ChargerTest, EnableDisableTableSourceMulti) { // enable_disable settings are added to a table // parse_enable_disable_source_table() processes the table and updates // active_enable_disable_source which is available via get_last_enable_disable_source() // enable_disable() updates the table and calls parse_enable_disable_source_table() // EnableDisableTable // Evaluation will be done based on priorities. 0 is the highest priority, 10000 the lowest // If all sources are unassigned, the connector is enabled // If two sources have the same priority, "disabled" has priority over "enabled" const int connector_id = 1; // use a consistent value const auto& enable_disable_source_table = charger->get_enable_disable_source_table(); EXPECT_TRUE(enable_disable_source_table.empty()); EXPECT_EQ(enable_disable_source_table.size(), 0); auto last_source = charger->get_last_enable_disable_source(); EXPECT_EQ(last_source, default_source); // check multiple sources are added to the table EXPECT_TRUE(charger->enable_disable(connector_id, enable_source_10)); EXPECT_EQ(enable_disable_source_table.size(), 1); last_source = charger->get_last_enable_disable_source(); EXPECT_EQ(last_source, enable_source_10); EXPECT_TRUE(charger->enable_disable(connector_id, {Enable_source::FirmwareUpdate, Enable_state::Enable, 11})); EXPECT_EQ(enable_disable_source_table.size(), 2); last_source = charger->get_last_enable_disable_source(); EXPECT_EQ(last_source, enable_source_10); EXPECT_TRUE(charger->enable_disable(connector_id, {Enable_source::LocalAPI, Enable_state::Enable, 11})); EXPECT_EQ(enable_disable_source_table.size(), 3); last_source = charger->get_last_enable_disable_source(); EXPECT_EQ(last_source, enable_source_10); EXPECT_TRUE(charger->enable_disable(connector_id, {Enable_source::LocalKeyLock, Enable_state::Enable, 11})); EXPECT_EQ(enable_disable_source_table.size(), 4); last_source = charger->get_last_enable_disable_source(); EXPECT_EQ(last_source, enable_source_10); EXPECT_TRUE(charger->enable_disable(connector_id, {Enable_source::MobileApp, Enable_state::Enable, 11})); EXPECT_EQ(enable_disable_source_table.size(), 5); last_source = charger->get_last_enable_disable_source(); EXPECT_EQ(last_source, enable_source_10); EXPECT_TRUE(charger->enable_disable(connector_id, {Enable_source::RemoteKeyLock, Enable_state::Enable, 11})); EXPECT_EQ(enable_disable_source_table.size(), 6); last_source = charger->get_last_enable_disable_source(); EXPECT_EQ(last_source, enable_source_10); const EnableDisableSource disable_source{Enable_source::ServiceTechnician, Enable_state::Disable, 11}; EXPECT_TRUE(charger->enable_disable(connector_id, disable_source)); EXPECT_EQ(enable_disable_source_table.size(), 7); last_source = charger->get_last_enable_disable_source(); EXPECT_EQ(last_source, enable_source_10); EXPECT_TRUE(charger->enable_disable(connector_id, {Enable_source::Unspecified, Enable_state::Enable, 11})); EXPECT_EQ(enable_disable_source_table.size(), 8); last_source = charger->get_last_enable_disable_source(); EXPECT_EQ(last_source, enable_source_10); // update CSMS - next highest expected - disable_source EXPECT_FALSE(charger->enable_disable(connector_id, enable_source_100)); EXPECT_EQ(enable_disable_source_table.size(), 8); last_source = charger->get_last_enable_disable_source(); EXPECT_EQ(last_source, disable_source); } TEST_F(ChargerTest, EnableDisableTablePriority) { // enable_disable settings are added to a table // parse_enable_disable_source_table() processes the table and updates // active_enable_disable_source which is available via get_last_enable_disable_source() // enable_disable() updates the table and calls parse_enable_disable_source_table() // EnableDisableTable // Evaluation will be done based on priorities. 0 is the highest priority, 10000 the lowest // If all sources are unassigned, the connector is enabled // If two sources have the same priority, "disabled" has priority over "enabled" const int connector_id = 1; // use a consistent value const auto& enable_disable_source_table = charger->get_enable_disable_source_table(); EXPECT_TRUE(enable_disable_source_table.empty()); EXPECT_EQ(enable_disable_source_table.size(), 0); auto last_source = charger->get_last_enable_disable_source(); EXPECT_EQ(last_source, default_source); // check priority is being respected (via different sources) // base priority is 10 - higher values must be ignored EXPECT_FALSE(charger->enable_disable(connector_id, disable_source_10)); EXPECT_EQ(enable_disable_source_table.size(), 1); last_source = charger->get_last_enable_disable_source(); EXPECT_EQ(last_source, disable_source_10); // ignored because disable has higher priority over enabled const EnableDisableSource next_expected{Enable_source::FirmwareUpdate, Enable_state::Enable, 10}; EXPECT_FALSE(charger->enable_disable(connector_id, next_expected)); EXPECT_EQ(enable_disable_source_table.size(), 2); last_source = charger->get_last_enable_disable_source(); EXPECT_EQ(last_source, disable_source_10); // ignored because unassigned is ignored EXPECT_FALSE(charger->enable_disable(connector_id, {Enable_source::LocalAPI, Enable_state::Unassigned, 9})); EXPECT_EQ(enable_disable_source_table.size(), 3); last_source = charger->get_last_enable_disable_source(); EXPECT_EQ(last_source, disable_source_10); // ignored because priority is lower EXPECT_FALSE(charger->enable_disable(connector_id, {Enable_source::LocalAPI, Enable_state::Disable, 12})); EXPECT_EQ(enable_disable_source_table.size(), 3); last_source = charger->get_last_enable_disable_source(); EXPECT_EQ(last_source, disable_source_10); // ignored because priority is even lower EXPECT_FALSE(charger->enable_disable(connector_id, {Enable_source::LocalAPI, Enable_state::Enable, 200})); EXPECT_EQ(enable_disable_source_table.size(), 3); last_source = charger->get_last_enable_disable_source(); EXPECT_EQ(last_source, disable_source_10); // overrides CSMS so it is ignored - expected is EXPECT_TRUE(charger->enable_disable(connector_id, unassigned_source_10)); EXPECT_EQ(enable_disable_source_table.size(), 3); last_source = charger->get_last_enable_disable_source(); EXPECT_EQ(last_source, next_expected); // disabled takes priority const EnableDisableSource last_expected{Enable_source::LocalAPI, Enable_state::Disable, 10}; EXPECT_FALSE(charger->enable_disable(connector_id, last_expected)); EXPECT_EQ(enable_disable_source_table.size(), 3); last_source = charger->get_last_enable_disable_source(); EXPECT_EQ(last_source, last_expected); // higher priority const EnableDisableSource higher_expected{Enable_source::FirmwareUpdate, Enable_state::Enable, 5}; EXPECT_TRUE(charger->enable_disable(connector_id, higher_expected)); EXPECT_EQ(enable_disable_source_table.size(), 3); last_source = charger->get_last_enable_disable_source(); EXPECT_EQ(last_source, higher_expected); } TEST_F(ChargerTest, EnableDisableSourceEnable0) { constexpr EnableDisableSource sourceA{Enable_source::CSMS, Enable_state::Unassigned, 100}; constexpr EnableDisableSource sourceB{Enable_source::FirmwareUpdate, Enable_state::Enable, 90}; // enable from default state EXPECT_TRUE(charger->enable_disable(0, sourceA)); auto last_source = charger->get_last_enable_disable_source(); // Unassigned updates do not change the result from get_last_enable_disable_source() EXPECT_EQ(last_source, default_source); // There should not be an update event - default state is enabled EXPECT_EQ(last_event, default_event); EXPECT_EQ(charger->current_state(), Charger::EvseState::Idle); EXPECT_TRUE(charger->connector_enabled()); // default state reset_last_event(); EXPECT_TRUE(charger->enable_disable(0, sourceB)); last_source = charger->get_last_enable_disable_source(); // Unassigned updates do not change the result from get_last_enable_disable_source() EXPECT_EQ(last_source, sourceB); // There should not be an update event - enabled -> enabled EXPECT_EQ(last_event, default_event); EXPECT_EQ(charger->current_state(), Charger::EvseState::Idle); EXPECT_TRUE(charger->connector_enabled()); // default state } TEST_F(ChargerTest, EnableDisableSourceEnable1A) { constexpr EnableDisableSource sourceA{Enable_source::CSMS, Enable_state::Disable, 100}; constexpr EnableDisableSource sourceB{Enable_source::FirmwareUpdate, Enable_state::Enable, 90}; // force a complete disable state EXPECT_FALSE(charger->enable_disable(1, sourceA)); auto last_source = charger->get_last_enable_disable_source(); // Unassigned updates do not change the result from get_last_enable_disable_source() EXPECT_EQ(last_source, sourceA); EXPECT_EQ(last_event, SessionEventEnum::Disabled); EXPECT_EQ(charger->current_state(), Charger::EvseState::Disabled); EXPECT_FALSE(charger->connector_enabled()); // enable on connector 1 reset_last_event(); EXPECT_TRUE(charger->enable_disable(1, sourceB)); last_source = charger->get_last_enable_disable_source(); EXPECT_EQ(last_source, sourceB); EXPECT_EQ(last_event, SessionEventEnum::Enabled); EXPECT_EQ(charger->current_state(), Charger::EvseState::Idle); EXPECT_TRUE(charger->connector_enabled()); } TEST_F(ChargerTest, EnableDisableSourceEnable1B) { constexpr EnableDisableSource sourceA{Enable_source::CSMS, Enable_state::Disable, 100}; constexpr EnableDisableSource sourceB{Enable_source::FirmwareUpdate, Enable_state::Enable, 90}; // force a complete disable state EXPECT_FALSE(charger->enable_disable(1, sourceA)); auto last_source = charger->get_last_enable_disable_source(); // Unassigned updates do not change the result from get_last_enable_disable_source() EXPECT_EQ(last_source, sourceA); EXPECT_EQ(last_event, SessionEventEnum::Disabled); EXPECT_EQ(charger->current_state(), Charger::EvseState::Disabled); EXPECT_FALSE(charger->connector_enabled()); // enable on connector 0 reset_last_event(); EXPECT_TRUE(charger->enable_disable(0, sourceB)); last_source = charger->get_last_enable_disable_source(); EXPECT_EQ(last_source, sourceB); EXPECT_EQ(last_event, SessionEventEnum::Enabled); EXPECT_EQ(charger->current_state(), Charger::EvseState::Disabled); EXPECT_FALSE(charger->connector_enabled()); // enable on connector 1 reset_last_event(); EXPECT_TRUE(charger->enable_disable(1, sourceB)); last_source = charger->get_last_enable_disable_source(); EXPECT_EQ(last_source, sourceB); // enable -> enable so no event EXPECT_EQ(last_event, default_event); EXPECT_EQ(charger->current_state(), Charger::EvseState::Idle); EXPECT_TRUE(charger->connector_enabled()); } TEST_F(ChargerTest, EnableDisableSourceDisable0) { constexpr EnableDisableSource sourceA{Enable_source::CSMS, Enable_state::Unassigned, 100}; constexpr EnableDisableSource sourceB{Enable_source::FirmwareUpdate, Enable_state::Disable, 100}; // disable from default state EXPECT_TRUE(charger->enable_disable(0, sourceA)); auto last_source = charger->get_last_enable_disable_source(); // Unassigned updates do not change the result from get_last_enable_disable_source() EXPECT_EQ(last_source, default_source); // This is possibly an error EXPECT_EQ(last_event, default_event); EXPECT_EQ(charger->current_state(), Charger::EvseState::Idle); reset_last_event(); EXPECT_FALSE(charger->enable_disable(0, sourceB)); last_source = charger->get_last_enable_disable_source(); EXPECT_EQ(last_source, sourceB); EXPECT_EQ(last_event, SessionEventEnum::Disabled); EXPECT_EQ(charger->current_state(), Charger::EvseState::Disabled); } TEST_F(ChargerTest, EnableDisableSourceDisable1) { constexpr EnableDisableSource sourceA{Enable_source::CSMS, Enable_state::Unassigned, 100}; constexpr EnableDisableSource sourceB{Enable_source::FirmwareUpdate, Enable_state::Disable, 100}; // disable from default state EXPECT_TRUE(charger->enable_disable(0, sourceA)); auto last_source = charger->get_last_enable_disable_source(); // Unassigned updates do not change the result from get_last_enable_disable_source() EXPECT_EQ(last_source, default_source); // This is possibly an error EXPECT_EQ(last_event, default_event); EXPECT_EQ(charger->current_state(), Charger::EvseState::Idle); reset_last_event(); EXPECT_FALSE(charger->enable_disable(0, sourceB)); last_source = charger->get_last_enable_disable_source(); EXPECT_EQ(last_source, sourceB); EXPECT_EQ(last_event, SessionEventEnum::Disabled); EXPECT_EQ(charger->current_state(), Charger::EvseState::Disabled); reset_last_event(); EXPECT_FALSE(charger->enable_disable(1, sourceB)); last_source = charger->get_last_enable_disable_source(); EXPECT_EQ(last_source, sourceB); // disable -> disable so no event EXPECT_EQ(last_event, default_event); EXPECT_EQ(charger->current_state(), Charger::EvseState::Disabled); } TEST_F(ChargerTest, EnableDisableSourceDisableEnable) { constexpr EnableDisableSource sourceA{Enable_source::CSMS, Enable_state::Unassigned, 100}; constexpr EnableDisableSource sourceB{Enable_source::FirmwareUpdate, Enable_state::Disable, 100}; constexpr EnableDisableSource sourceC{Enable_source::LocalAPI, Enable_state::Enable, 90}; // default state EXPECT_TRUE(charger->enable_disable(0, sourceA)); auto last_source = charger->get_last_enable_disable_source(); // Unassigned updates do not change the result from get_last_enable_disable_source() EXPECT_EQ(last_source, default_source); // This is possibly an error EXPECT_EQ(last_event, default_event); EXPECT_EQ(charger->current_state(), Charger::EvseState::Idle); // disable 0 reset_last_event(); EXPECT_FALSE(charger->enable_disable(0, sourceB)); last_source = charger->get_last_enable_disable_source(); EXPECT_EQ(last_source, sourceB); EXPECT_EQ(last_event, SessionEventEnum::Disabled); EXPECT_EQ(charger->current_state(), Charger::EvseState::Disabled); // disable 1 reset_last_event(); EXPECT_FALSE(charger->enable_disable(1, sourceB)); last_source = charger->get_last_enable_disable_source(); EXPECT_EQ(last_source, sourceB); // no event: disable -> disable EXPECT_EQ(last_event, default_event); EXPECT_EQ(charger->current_state(), Charger::EvseState::Disabled); // enable 0 reset_last_event(); EXPECT_TRUE(charger->enable_disable(0, sourceC)); last_source = charger->get_last_enable_disable_source(); EXPECT_EQ(last_source, sourceC); EXPECT_EQ(last_event, SessionEventEnum::Enabled); // remains disabled because the enable was on connector 0 EXPECT_EQ(charger->current_state(), Charger::EvseState::Disabled); // enable 1 reset_last_event(); EXPECT_TRUE(charger->enable_disable(1, sourceC)); last_source = charger->get_last_enable_disable_source(); EXPECT_EQ(last_source, sourceC); // enable->enable hence no event EXPECT_EQ(last_event, default_event); // updated because not connector 0 EXPECT_EQ(charger->current_state(), Charger::EvseState::Idle); } // tests for cancel_transaction() / authorize() interaction TEST_F(ChargerTest, DelayedAuthorizeAfterCancelTransactionIsIgnored) { auto& ctx = charger->get_shared_context(); // Simulate an active charging session ctx.flag_transaction_active = true; ctx.session_active = true; ctx.flag_authorized = true; // External cancellation (e.g. OCPP RemoteStop) types::evse_manager::StopTransactionRequest stop_request; stop_request.reason = types::evse_manager::StopTransactionReason::Remote; EXPECT_TRUE(charger->cancel_transaction(stop_request)); EXPECT_FALSE(ctx.flag_authorized); EXPECT_TRUE(ctx.flag_externally_cancelled); // Delayed authorization response arrives after the cancellation types::authorization::ProvidedIdToken token; token.id_token.value = "DELAYED_TOKEN"; token.id_token.type = types::authorization::IdTokenType::Central; token.authorization_type = types::authorization::AuthorizationType::OCPP; types::authorization::ValidationResult validation_result; validation_result.authorization_status = types::authorization::AuthorizationStatus::Accepted; charger->authorize(true, token, validation_result); // The delayed response must not restore authorization EXPECT_FALSE(ctx.flag_authorized); EXPECT_TRUE(ctx.flag_externally_cancelled); } // Test that disabling while a transaction is active goes through the proper // StoppingCharging->Finished->Disabled sequence instead of jumping directly. TEST_F(ChargerTest, DisableDuringActiveTransaction) { constexpr EnableDisableSource disable_source{Enable_source::CSMS, Enable_state::Disable, 100}; auto& ctx = charger->get_shared_context(); // Simulate an active charging session with contactors closed ctx.current_state = Charger::EvseState::Charging; ctx.flag_transaction_active = true; ctx.session_active = true; ctx.flag_authorized = true; ctx.contactor_open = false; reset_last_event(); EXPECT_FALSE(charger->enable_disable(1, disable_source)); // enable_disable calls run_state_machine synchronously: Charging->StoppingCharging. // Must NOT immediately jump to Disabled — session needs proper teardown. EXPECT_EQ(ctx.current_state, Charger::EvseState::StoppingCharging); EXPECT_TRUE(charger->flag_disable_requested()); EXPECT_EQ(ctx.last_stop_transaction_reason, StopTransactionReason::EVSEDisabled); // Simulate relay opening and transaction already stopped ctx.contactor_open = true; ctx.flag_transaction_active = false; // State machine: StoppingCharging->Finished->Disabled (all in one loop) reset_last_event(); charger->run_state_machine(); EXPECT_EQ(ctx.current_state, Charger::EvseState::Disabled); EXPECT_EQ(last_event, SessionEventEnum::Disabled); } // Test that disabling while in WaitingForAuthentication (no transaction yet) // ends the session and transitions to Disabled without going through StoppingCharging. TEST_F(ChargerTest, DisableDuringWaitingForAuthentication) { constexpr EnableDisableSource disable_source{Enable_source::CSMS, Enable_state::Disable, 100}; auto& ctx = charger->get_shared_context(); // Simulate EV plugged in, waiting for auth — no transaction started yet ctx.current_state = Charger::EvseState::WaitingForAuthentication; ctx.session_active = true; ctx.flag_ev_plugged_in = true; ctx.flag_transaction_active = false; ctx.flag_authorized = false; reset_last_event(); EXPECT_FALSE(charger->enable_disable(1, disable_source)); // run_state_machine is called synchronously inside enable_disable, so by // the time enable_disable returns the state machine has already driven // WaitingForAuthentication -> Finished -> Disabled. EXPECT_EQ(ctx.current_state, Charger::EvseState::Disabled); EXPECT_EQ(last_event, SessionEventEnum::Disabled); } // Test that disabling while in Idle (no session) immediately transitions to Disabled TEST_F(ChargerTest, DisableDuringIdle) { constexpr EnableDisableSource disable_source{Enable_source::CSMS, Enable_state::Disable, 100}; auto& ctx = charger->get_shared_context(); // Simulate EV plugged in, no session active ctx.current_state = Charger::EvseState::Idle; ctx.session_active = false; ctx.flag_ev_plugged_in = true; ctx.flag_transaction_active = false; ctx.flag_authorized = false; reset_last_event(); EXPECT_FALSE(charger->enable_disable(1, disable_source)); // Must immediately transition to Disabled EXPECT_EQ(ctx.current_state, Charger::EvseState::Disabled); EXPECT_EQ(last_event, SessionEventEnum::Disabled); } } // namespace // ---------------------------------------------------------------------------- // the following code is stubs to enable testing Charger in isolation. // If these stubs are needed elsewhere then they could go into separate files // ---------------------------------------------------------------------------- // backtrace stub namespace Everest { void signal_handler(int signo) { } void install_backtrace_handler() { } void request_backtrace(pthread_t id) { } } // namespace Everest namespace module { // ---------------------------------------------------------------------------- // IECStateMachine stub IECStateMachine::IECStateMachine(const std::unique_ptr& r_bsp_, bool lock_connector_in_state_b_) : r_bsp(r_bsp_) { } void IECStateMachine::process_bsp_event(const types::board_support_common::BspEvent& bsp_event) { } void IECStateMachine::allow_power_on(bool value, types::evse_board_support::Reason reason) { } std::optional IECStateMachine::read_pp_ampacity() { return std::nullopt; } void IECStateMachine::switch_three_phases_while_charging(bool n) { } void IECStateMachine::setup(bool has_ventilation) { } void IECStateMachine::set_overcurrent_limit(double amps) { } void IECStateMachine::set_pwm(double value) { } void IECStateMachine::set_cp_state_X1() { } void IECStateMachine::set_cp_state_F() { } void IECStateMachine::enable(bool en) { } void IECStateMachine::connector_force_unlock() { } const std::string cpevent_to_string(CPEvent e) { switch (e) { case CPEvent::CarPluggedIn: return "CarPluggedIn"; case CPEvent::CarRequestedPower: return "CarRequestedPower"; case CPEvent::PowerOn: return "PowerOn"; case CPEvent::PowerOff: return "PowerOff"; case CPEvent::CarRequestedStopPower: return "CarRequestedStopPower"; case CPEvent::CarUnplugged: return "CarUnplugged"; case CPEvent::EFtoBCD: return "EFtoBCD"; case CPEvent::BCDtoEF: return "BCDtoEF"; case CPEvent::BCDtoE: return "BCDtoE"; } throw std::out_of_range("No known string conversion for provided enum of type CPEvent"); } // ---------------------------------------------------------------------------- // ErrorHandling stub ErrorHandling::ErrorHandling(const std::unique_ptr& r_bsp, const std::vector>& r_hlc, const std::vector>& r_connector_lock, const std::vector>& r_ac_rcd, const std::unique_ptr& _p_evse, const std::vector>& _r_imd, const std::vector>& _r_powersupply, const std::vector>& _r_powermeter, const std::vector>& _r_over_voltage_monitor, bool _inoperative_error_use_vendor_id) : r_bsp(r_bsp), r_hlc(r_hlc), r_connector_lock(r_connector_lock), r_ac_rcd(r_ac_rcd), p_evse(p_evse), r_imd(_r_imd), r_powersupply(r_powersupply), r_powermeter(_r_powermeter), r_over_voltage_monitor(_r_over_voltage_monitor), inoperative_error_use_vendor_id(_inoperative_error_use_vendor_id) { } void ErrorHandling::raise_overcurrent_error(const std::string& description) { } void ErrorHandling::clear_overcurrent_error() { } void ErrorHandling::raise_over_voltage_error(Everest::error::Severity severity, const std::string& description) { } void ErrorHandling::clear_over_voltage_error() { } void ErrorHandling::raise_internal_error(const std::string& description) { } void ErrorHandling::clear_internal_error() { } void ErrorHandling::raise_authorization_timeout_error(const std::string& description) { } void ErrorHandling::clear_authorization_timeout_error() { } void ErrorHandling::raise_powermeter_transaction_start_failed_error(const std::string& description) { } void ErrorHandling::clear_powermeter_transaction_start_failed_error() { } void ErrorHandling::raise_isolation_resistance_fault(const std::string& description, const std::string& sub_type) { } void ErrorHandling::clear_isolation_resistance_fault(const std::string& sub_type) { } void ErrorHandling::raise_cable_check_fault(const std::string& description) { } void ErrorHandling::clear_cable_check_fault() { } void ErrorHandling::clear_voltage_plausibility_fault() { } // ---------------------------------------------------------------------------- // SessionLog stub SessionLog::SessionLog() { } SessionLog::~SessionLog() { } void SessionLog::setPath(const std::string& path) { } void SessionLog::setMqtt(const std::function& mqtt_provider) { } void SessionLog::enable() { } std::optional SessionLog::startSession(const std::string& suffix_string) { return {}; } void SessionLog::stopSession() { } void SessionLog::car(bool iso15118, const std::string& msg) { } void SessionLog::car(bool iso15118, const std::string& msg, const std::string& xml, const std::string& xml_hex, const std::string& xml_base64, const std::string& json_str) { } void SessionLog::evse(bool iso15118, const std::string& msg) { } void SessionLog::evse(bool iso15118, const std::string& msg, const std::string& xml, const std::string& xml_hex, const std::string& xml_base64, const std::string& json_str) { } void SessionLog::xmlOutput(bool e) { } void SessionLog::sys(const std::string& msg) { } SessionLog session_log; // ---------------------------------------------------------------------------- // PersistentStore stub PersistentStore::PersistentStore(const std::vector>& r_store, const std::string module_id) : r_store(r_store) { } void PersistentStore::store_session(const std::string& session_uuid) { } void PersistentStore::clear_session() { } std::string PersistentStore::get_session() { return {}; } } // namespace module