Skip to content

Commit

Permalink
Add source to enable/disable commands/events
Browse files Browse the repository at this point in the history
A source can now be specified for an enable/disable command
along with a flag whether it can be modified by other sources.

In some uses cases it is useful if e.g. a LocalAPI source disables
a connector that only the same local source can enable again.
Think of a service technician who has an app to disable the connector
locally. Then it should be prevented that it is re-enabled by the CSMS
until the service technician enables it again via the same app.

Signed-off-by: Cornelius Claussen <[email protected]>
  • Loading branch information
corneliusclaussen committed Jan 23, 2024
1 parent b49a853 commit 84a4278
Show file tree
Hide file tree
Showing 12 changed files with 201 additions and 38 deletions.
8 changes: 8 additions & 0 deletions interfaces/evse_manager.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ cmds:
connector_id:
description: Specifies the ID of the connector to enable. If 0, the whole EVSE should be enabled
type: integer
cmd_source:
description: Source of the enable command
type: object
$ref: /evse_manager#/EnableDisableSource
result:
description: >-
Returns true if evse was enabled (or was enabled before), returns
Expand All @@ -27,6 +31,10 @@ cmds:
connector_id:
description: Specifies the ID of the connector. If 0, the whole EVSE should be disabled
type: integer
cmd_source:
description: Source of the enable command
type: object
$ref: /evse_manager#/EnableDisableSource
result:
description: >-
Returns true if evse was disabled (or was disabled before), returns
Expand Down
86 changes: 70 additions & 16 deletions modules/API/API.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,12 @@ void SessionInfo::set_latest_total_w(double latest_total_w) {
this->latest_total_w = latest_total_w;
}

void SessionInfo::set_enable_disable_source(const std::string& last_source, bool allow_others_to_modify) {
std::lock_guard<std::mutex> lock(this->session_info_mutex);
this->last_enable_disable_source = last_source;
this->enable_disable_other_sources_may_modify = allow_others_to_modify;
}

static void to_json(json& j, const SessionInfo::Error& e) {
j = json{{"type", e.type}, {"description", e.description}, {"severity", e.severity}};
}
Expand All @@ -207,14 +213,17 @@ SessionInfo::operator std::string() {
auto charging_duration_s =
std::chrono::duration_cast<std::chrono::seconds>(this->end_time_point - this->start_time_point);

json session_info = json::object({{"state", state_to_string(this->state)},
{"active_permanent_faults", this->active_permanent_faults},
{"active_errors", this->active_errors},
{"charged_energy_wh", charged_energy_wh},
{"discharged_energy_wh", discharged_energy_wh},
{"latest_total_w", this->latest_total_w},
{"charging_duration_s", charging_duration_s.count()},
{"datetime", Everest::Date::to_rfc3339(now)}});
json session_info =
json::object({{"state", state_to_string(this->state)},
{"active_permanent_faults", this->active_permanent_faults},
{"active_errors", this->active_errors},
{"charged_energy_wh", charged_energy_wh},
{"discharged_energy_wh", discharged_energy_wh},
{"latest_total_w", this->latest_total_w},
{"charging_duration_s", charging_duration_s.count()},
{"datetime", Everest::Date::to_rfc3339(now)},
{"last_enable_disable_source", this->last_enable_disable_source},
{"enable_disable_other_sources_may_modify", this->enable_disable_other_sources_may_modify}});

return session_info.dump();
}
Expand Down Expand Up @@ -305,6 +314,12 @@ void API::init() {

session_info->update_state(session_event.event, error);

if (session_event.source.has_value()) {
session_info->set_enable_disable_source(
types::evse_manager::source_to_string(session_event.source.value().source),
session_event.source.value().other_sources_may_modify);
}

if (session_event.event == types::evse_manager::SessionEventEnum::SessionStarted) {
if (session_event.session_started.has_value()) {
auto session_started = session_event.session_started.value();
Expand Down Expand Up @@ -350,31 +365,70 @@ void API::init() {
std::string cmd_enable = cmd_base + "enable";
this->mqtt.subscribe(cmd_enable, [&evse](const std::string& data) {
auto connector_id = 0;
types::evse_manager::EnableDisableSource enable_source{types::evse_manager::Source::LocalAPI, true};

if (!data.empty()) {
try {
connector_id = std::stoi(data);
EVLOG_warning << "enable: Argument is an integer, using deprecated compatibility mode";
} catch (const std::exception& e) {
// argument is not just an integer, try to parse the full parameter object
}

try {
auto arg = json::parse(data);
if (arg.contains("connector_id")) {
connector_id = arg.at("connector_id");
}
if (arg.contains("source")) {
enable_source.source = types::evse_manager::string_to_source(arg.at("source"));
}
if (arg.contains("other_sources_may_modify")) {
enable_source.other_sources_may_modify = arg.at("other_sources_may_modify");
}
} catch (const std::exception& e) {
EVLOG_error << "Could not parse connector id for enable connector, ignoring command, error: "
<< e.what();
return;
EVLOG_error << "enable: Cannot parse argument, command ignored: " << e.what();
}
} else {
EVLOG_error << "enable: No argument specified, ignoring command";
}
evse->call_enable(connector_id);

evse->call_enable(connector_id, enable_source);
});

std::string cmd_disable = cmd_base + "disable";
this->mqtt.subscribe(cmd_disable, [&evse](const std::string& data) {
auto connector_id = 0;

types::evse_manager::EnableDisableSource disable_source{types::evse_manager::Source::LocalAPI, true};

if (!data.empty()) {
try {
connector_id = std::stoi(data);
EVLOG_warning << "disable: Argument is an integer, using deprecated compatibility mode";
} catch (const std::exception& e) {
EVLOG_error << "Could not parse connector id for disable connector, ignoring command, error: "
<< e.what();
return;
// argument is not just an integer, try to parse the full parameter object
}

try {
auto arg = json::parse(data);
if (arg.contains("connector_id")) {
connector_id = arg.at("connector_id");
}
if (arg.contains("source")) {
disable_source.source = types::evse_manager::string_to_source(arg.at("source"));
}
if (arg.contains("other_sources_may_modify")) {
disable_source.other_sources_may_modify = arg.at("other_sources_may_modify");
}
} catch (const std::exception& e) {
EVLOG_error << "disable: Cannot parse argument, command ignored: " << e.what();
}
} else {
EVLOG_error << "disable: No argument specified, ignoring command";
}
evse->call_disable(connector_id);

evse->call_disable(connector_id, disable_source);
});

std::string cmd_pause_charging = cmd_base + "pause_charging";
Expand Down
4 changes: 4 additions & 0 deletions modules/API/API.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ class SessionInfo {
void set_end_energy_export_wh(int32_t end_energy_export_wh);
void set_latest_energy_export_wh(int32_t latest_export_energy_wh);
void set_latest_total_w(double latest_total_w);
void set_enable_disable_source(const std::string& last_source, bool allow_others_to_modify);

/// \brief Converts this struct into a serialized json object
operator std::string();
Expand Down Expand Up @@ -91,6 +92,9 @@ class SessionInfo {
bool is_state_charging(const SessionInfo::State current_state);

std::string state_to_string(State s);

std::string last_enable_disable_source{"Unspecified"};
bool enable_disable_other_sources_may_modify{true};
};
} // namespace module
// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1
Expand Down
40 changes: 36 additions & 4 deletions modules/API/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ This variable is published every second and contains a json object with informat
"latest_total_w": 0.0,
"state": "Unplugged",
"active_permanent_faults": [],
"active_errors": []
"active_errors": [],
"last_enable_disable_source": "Unspecified",
"enable_disable_allow_others_to_modify": true
}
```

Expand All @@ -71,7 +73,9 @@ Example with permanent faults being active:
"datetime": "2024-01-15T14:58:15.172Z",
"discharged_energy_wh": 0,
"latest_total_w": 0,
"state": "Preparing"
"state": "Preparing",
"last_enable_disable_source": "Unspecified",
"enable_disable_other_sources_may_modify": true
}
```

Expand Down Expand Up @@ -200,10 +204,38 @@ If the OCPP module has not yet published its "is_connected" status or no OCPP mo

## Commands and variables published in response
### everest_api/evse_manager/cmd/enable
Command to enable a connector on the EVSE. They payload should be a positive integer identifying the connector that should be enabled. If the payload is 0 the whole EVSE is enabled.
Command to enable a connector on the EVSE. The payload should be the following json:
```json
{
"connector_id": 1,
"source": "LocalAPI",
"other_sources_may_modify": true
}
```
connector_id is a positive integer identifying the connector that should be enabled. If the connector_id is 0 the whole EVSE is enabled.

The source is an enum of the following source types :

- Unspecified
- LocalAPI
- LocalKeyLock
- ServiceTechnician
- RemoteKeyLock
- MobileApp
- FirmwareUpdate
- CSMS

The bool flag other_sources_may_modify works the following way when set to false:
If a source type A enables the connector, only the source type A may disable it again. The same is true for the opposite: if a source type A disables the connector, only source type A may enable it again.

If other_sources_may_modify is set to true, any source may enable/disable regardless of the history.

This API endpoint has a compatibility mode: If you just specify a positive integer instead of the object described above,
it will use it as connector_id and set source to LocalAPI and other_sources_may_modify to true.


### everest_api/evse_manager/cmd/disable
Command to disable a connector on the EVSE. They payload should be a positive integer identifying the connector that should be disabled. If the payload is 0 the whole EVSE is disabled.
Command to disable a connector on the EVSE. For payload specifications, see cmd/enable.

### everest_api/evse_manager/cmd/pause_charging
If any arbitrary payload is published to this topic charging will be paused by the EVSE.
Expand Down
25 changes: 23 additions & 2 deletions modules/EvseManager/Charger.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1114,8 +1114,17 @@ bool Charger::DeAuthorize() {
return false;
}

bool Charger::disable(int connector_id) {
bool Charger::disable(int connector_id, const types::evse_manager::EnableDisableSource& disable_source) {
std::lock_guard<std::recursive_mutex> lock(stateMutex);
if (not last_enable_disable_source.other_sources_may_modify and
disable_source.source not_eq last_enable_disable_source.source) {
// we do not disable here as the last source did not allow modification by other sources
EVLOG_warning << "Ignoring disable command, source is not allowed to enable";
return false;
}

last_enable_disable_source = disable_source;

if (connector_id != 0) {
connectorEnabled = false;
}
Expand All @@ -1124,8 +1133,16 @@ bool Charger::disable(int connector_id) {
return true;
}

bool Charger::enable(int connector_id) {
bool Charger::enable(int connector_id, const types::evse_manager::EnableDisableSource& enable_source) {
std::lock_guard<std::recursive_mutex> lock(stateMutex);
if (not last_enable_disable_source.other_sources_may_modify and
enable_source.source not_eq last_enable_disable_source.source) {
// we do not disable here as the last source did not allow modification by other sources
EVLOG_warning << "Ignoring enable command, source is not allowed to enable";
return false;
}

last_enable_disable_source = enable_source;
if (connector_id != 0) {
connectorEnabled = true;
}
Expand Down Expand Up @@ -1472,4 +1489,8 @@ void Charger::clear_errors_on_unplug() {
error_handling->clear_powermeter_transaction_start_failed_error();
}

types::evse_manager::EnableDisableSource Charger::get_last_enable_disable_source() {
return last_enable_disable_source;
}

} // namespace module
8 changes: 6 additions & 2 deletions modules/EvseManager/Charger.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,8 @@ class Charger {
bool ac_hlc_enabled, bool ac_hlc_use_5percent, bool ac_enforce_hlc, bool ac_with_soc_timeout,
float soft_over_current_tolerance_percent, float soft_over_current_measurement_noise_A);

bool enable(int connector_id);
bool disable(int connector_id);
bool enable(int connector_id, const types::evse_manager::EnableDisableSource& source);
bool disable(int connector_id, const types::evse_manager::EnableDisableSource& source);
void set_faulted();
void set_hlc_error();
void set_rcd_error();
Expand Down Expand Up @@ -181,6 +181,8 @@ class Charger {

bool errors_prevent_charging();

types::evse_manager::EnableDisableSource get_last_enable_disable_source();

private:
void bcb_toggle_reset();
void bcb_toggle_detect_start_pulse();
Expand Down Expand Up @@ -332,6 +334,8 @@ class Charger {
constexpr static int legacy_wakeup_timeout{30000};

void clear_errors_on_unplug();

types::evse_manager::EnableDisableSource last_enable_disable_source{types::evse_manager::Source::Unspecified, true};
};

#define CHARGER_ABSOLUTE_MAX_CURRENT double(80.0F)
Expand Down
2 changes: 1 addition & 1 deletion modules/EvseManager/EvseManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -843,7 +843,7 @@ void EvseManager::ready() {

void EvseManager::ready_to_start_charging() {
charger->run();
charger->enable(0);
charger->enable(0, {types::evse_manager::Source::Unspecified, true});

this->p_evse->publish_ready(true);
EVLOG_info << fmt::format(fmt::emphasis::bold | fg(fmt::terminal_color::green),
Expand Down
19 changes: 13 additions & 6 deletions modules/EvseManager/evse/evse_managerImpl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,14 @@ void evse_managerImpl::init() {
[&charger = mod->charger, this](std::string data) { mod->updateLocalMaxWattLimit(std::stof(data)); });

mod->mqtt.subscribe(fmt::format("everest_external/nodered/{}/cmd/enable", mod->config.connector_id),
[&charger = mod->charger](const std::string& data) { charger->enable(0); });
[&charger = mod->charger](const std::string& data) {
charger->enable(0, {types::evse_manager::Source::LocalAPI, true});
});

mod->mqtt.subscribe(fmt::format("everest_external/nodered/{}/cmd/disable", mod->config.connector_id),
[&charger = mod->charger](const std::string& data) { charger->disable(0); });
[&charger = mod->charger](const std::string& data) {
charger->disable(0, {types::evse_manager::Source::LocalAPI, true});
});

mod->mqtt.subscribe(fmt::format("everest_external/nodered/{}/cmd/faulted", mod->config.connector_id),
[&charger = mod->charger](const std::string& data) { charger->set_faulted(); });
Expand Down Expand Up @@ -277,6 +281,9 @@ void evse_managerImpl::ready() {
if (connector_status_changed) {
se.connector_id = 1;
}

// Add source information (Who initiated this state change)
se.source = mod->charger->get_last_enable_disable_source();
}

se.uuid = session_uuid;
Expand Down Expand Up @@ -321,9 +328,9 @@ types::evse_manager::Evse evse_managerImpl::handle_get_evse() {
return evse;
}

bool evse_managerImpl::handle_enable(int& connector_id) {
bool evse_managerImpl::handle_enable(int& connector_id, types::evse_manager::EnableDisableSource& cmd_source) {
connector_status_changed = connector_id != 0;
return mod->charger->enable(connector_id);
return mod->charger->enable(connector_id, cmd_source);
};

void evse_managerImpl::handle_authorize_response(types::authorization::ProvidedIdToken& provided_token,
Expand Down Expand Up @@ -361,9 +368,9 @@ void evse_managerImpl::handle_cancel_reservation() {
mod->cancel_reservation(true);
};

bool evse_managerImpl::handle_disable(int& connector_id) {
bool evse_managerImpl::handle_disable(int& connector_id, types::evse_manager::EnableDisableSource& cmd_source) {
connector_status_changed = connector_id != 0;
return mod->charger->disable(connector_id);
return mod->charger->disable(connector_id, cmd_source);
};

void evse_managerImpl::handle_set_faulted() {
Expand Down
4 changes: 2 additions & 2 deletions modules/EvseManager/evse/evse_managerImpl.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ class evse_managerImpl : public evse_managerImplBase {
protected:
// command handler functions (virtual)
virtual types::evse_manager::Evse handle_get_evse() override;
virtual bool handle_enable(int& connector_id) override;
virtual bool handle_disable(int& connector_id) override;
virtual bool handle_enable(int& connector_id, types::evse_manager::EnableDisableSource& cmd_source) override;
virtual bool handle_disable(int& connector_id, types::evse_manager::EnableDisableSource& cmd_source) override;
virtual void handle_authorize_response(types::authorization::ProvidedIdToken& provided_token,
types::authorization::ValidationResult& validation_result) override;
virtual void handle_withdraw_authorization() override;
Expand Down
Loading

0 comments on commit 84a4278

Please sign in to comment.