Skip to content

Commit

Permalink
Add new enable_disable commands to EvseManager
Browse files Browse the repository at this point in the history
It replaces the old enable/disable commands and adds a source and
priority. See README of the API module for more details.

Compile time breaking change on the evse_manager interface.
All enable/disable calls need to be switched over to the new
enable_disable call, for everest-core this is done in this PR.

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.
Using priorities this is quite flexible and a lot of use-cases can
be implemented without a change in EVerest code.

The API module also has a new enable_disable command. It also has
compatibility implementations for the old enable/disable commands.

Signed-off-by: Cornelius Claussen <[email protected]>
  • Loading branch information
corneliusclaussen committed Mar 21, 2024
1 parent a8408d3 commit bfb8e30
Show file tree
Hide file tree
Showing 12 changed files with 397 additions and 81 deletions.
26 changes: 9 additions & 17 deletions interfaces/evse_manager.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,29 +8,21 @@ cmds:
description: Object that contains information of the EVSE including its connectors
type: object
$ref: /evse_manager#/Evse
enable:
description: Enables the evse. EVSE is available for charging after this operation
enable_disable:
description: Enables or disables the evse
arguments:
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
false if enable failed e.g. due to permanent fault.
type: boolean
disable:
description: >-
Disables the evse. EVSE is not available for charging after this
operation
arguments:
connector_id:
description: Specifies the ID of the connector. If 0, the whole EVSE should be disabled
type: integer
result:
description: >-
Returns true if evse was disabled (or was disabled before), returns
false if it could not be disabled (i.e. due to communication error with hardware)
Returns true if evse is enabled after the command, false if it is disabled.
This may not be the same value as the request, since there may be a higher priority request
from another source that is actually deciding whether it is enabled or disabled.
type: boolean
authorize_response:
description: >-
Expand Down
101 changes: 82 additions & 19 deletions modules/API/API.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,14 @@ void SessionInfo::set_uk_random_delay_remaining(const types::uk_random_delay::Co
this->uk_random_delay_remaining = cd;
}

void SessionInfo::set_enable_disable_source(const std::string& active_source, const std::string& active_state,
const int active_priority) {
std::lock_guard<std::mutex> lock(this->session_info_mutex);
this->active_enable_disable_source = active_source;
this->active_enable_disable_state = active_state;
this->active_enable_disable_priority = active_priority;
}

static void to_json(json& j, const SessionInfo::Error& e) {
j = json{{"type", e.type}, {"description", e.description}, {"severity", e.severity}};
}
Expand All @@ -240,14 +248,23 @@ 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)},

});

json active_disable_enable = json::object({{"source", this->active_enable_disable_source},
{"state", this->active_enable_disable_state},
{"priority", this->active_enable_disable_priority}});
session_info["active_enable_disable_source"] = active_disable_enable;

if (uk_random_delay_remaining.countdown_s > 0) {
json random_delay =
json::object({{"remaining_s", uk_random_delay_remaining.countdown_s},
Expand Down Expand Up @@ -346,6 +363,13 @@ 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::enable_source_to_string(session_event.source.value().enable_source),
types::evse_manager::enable_state_to_string(session_event.source.value().enable_state),
session_event.source.value().enable_priority);
}

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 @@ -388,34 +412,73 @@ void API::init() {
// API commands
std::string cmd_base = evse_base + "/cmd/";

std::string cmd_enable = cmd_base + "enable";
this->mqtt.subscribe(cmd_enable, [&evse](const std::string& data) {
std::string cmd_enable_disable = cmd_base + "enable_disable";
this->mqtt.subscribe(cmd_enable_disable, [&evse](const std::string& data) {
auto connector_id = 0;
types::evse_manager::EnableDisableSource enable_source{types::evse_manager::Enable_source::LocalAPI,
types::evse_manager::Enable_state::Enable, 100};

if (!data.empty()) {
try {
connector_id = std::stoi(data);
auto arg = json::parse(data);
if (arg.contains("connector_id")) {
connector_id = arg.at("connector_id");
}
if (arg.contains("source")) {
enable_source.enable_source = types::evse_manager::string_to_enable_source(arg.at("source"));
}
if (arg.contains("state")) {
enable_source.enable_state = types::evse_manager::string_to_enable_state(arg.at("state"));
}
if (arg.contains("priority")) {
enable_source.enable_priority = arg.at("priority");
}

} 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_disable(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 enable_source{types::evse_manager::Enable_source::LocalAPI,
types::evse_manager::Enable_state::Disable, 100};

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 << "disable: Cannot parse argument, ignoring command";
}
} else {
EVLOG_error << "disable: No argument specified, ignoring command";
}
evse->call_enable_disable(connector_id, enable_source);
});

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::Enable_source::LocalAPI,
types::evse_manager::Enable_state::Enable, 100};

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;
EVLOG_error << "disable: Cannot parse argument, ignoring command";
}
} else {
EVLOG_error << "disable: No argument specified, ignoring command";
}
evse->call_disable(connector_id);
evse->call_enable_disable(connector_id, enable_source);
});

std::string cmd_pause_charging = cmd_base + "pause_charging";
Expand Down
6 changes: 6 additions & 0 deletions modules/API/API.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ class SessionInfo {
void set_latest_energy_export_wh(int32_t latest_export_energy_wh);
void set_latest_total_w(double latest_total_w);
void set_uk_random_delay_remaining(const types::uk_random_delay::CountDown& c);
void set_enable_disable_source(const std::string& active_source, const std::string& active_state,
const int active_priority);

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

std::string state_to_string(State s);

std::string active_enable_disable_source{"Unspecified"};
std::string active_enable_disable_state{"Enabled"};
int active_enable_disable_priority{0};
};
} // namespace module
// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1
Expand Down
111 changes: 102 additions & 9 deletions modules/API/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,19 @@ This variable is published every second and contains a json object with informat
"state": "Unplugged",
"active_permanent_faults": [],
"active_errors": [],
"active_enable_disable_source": {
"source": "Unspecified",
"state": "Enable",
"priority": 5000
},
"uk_random_delay": {
"remaining_s": 34,
"current_limit_after_delay_A": 16.0,
"current_limit_during_delay_A": 0.0,
"start_time": "2024-02-28T14:11:11.129Z"
}
},
"last_enable_disable_source": "Unspecified",
"enable_disable_allow_others_to_modify": true
}
```

Expand All @@ -77,7 +84,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 @@ -135,10 +144,13 @@ Example with permanent faults being active:
- PermanentFault
- PowermeterTransactionStartFailed

- **active_errors** array of all active errors that do not block charging. This could be shown to the user but the current state should still be shown as it does not interfere with charging. The enum is the same as for active_permanent_faults.
- **active_errors** array of all active errors that do not block charging.
This could be shown to the user but the current state should still be shown
as it does not interfere with charging. The enum is the same as for active_permanent_faults.

### everest_api/evse_manager/var/limits
This variable is published every second and contains a json object with information relating to the current limits of this EVSE.
This variable is published every second and contains a json object with information

Check notice on line 152 in modules/API/README.md

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

modules/API/README.md#L152

Expected: 80; Actual: 84
relating to the current limits of this EVSE.
```json
{
"max_current": 16.0,
Expand All @@ -161,7 +173,8 @@ This variable is published every second and contains telemetry of the EVSE.
```

### everest_api/evse_manager/var/powermeter
This variable is published every second and contains powermeter information of the EVSE.
This variable is published every second and contains powermeter information
of the EVSE.
```json
{
"current_A": {
Expand Down Expand Up @@ -201,16 +214,96 @@ This variable is published every second and contains powermeter information of t
## Periodically published variables for OCPP

### everest_api/ocpp/var/connection_status
This variable is published every second and contains the connection status of the OCPP module.
If the OCPP module has not yet published its "is_connected" status or no OCPP module is configured "unknown" is published. Otherwise "connected" or "disconnected" are published.
This variable is published every second and contains the connection
status of the OCPP module.
If the OCPP module has not yet published its "is_connected" status or
no OCPP module is configured "unknown" is published. Otherwise "connected"
or "disconnected" are published.


## Commands and variables published in response
### everest_api/evse_manager/cmd/enable_disable
Command to enable or disable a connector on the EVSE. The payload should be
the following json:

Check notice on line 228 in modules/API/README.md

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

modules/API/README.md#L228

Expected: 80; Actual: 226
```json
{
"connector_id": 0,

Check notice on line 231 in modules/API/README.md

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

modules/API/README.md#L231

Expected: 80; Actual: 203
"source": "LocalAPI",
"state": "Enable",
"priority": 42

Check notice on line 234 in modules/API/README.md

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

modules/API/README.md#L234

Expected: 80; Actual: 101
}
```
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

Check notice on line 242 in modules/API/README.md

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

modules/API/README.md#L242

Expected: fenced; Actual: indented
- LocalAPI
- LocalKeyLock
- ServiceTechnician
- RemoteKeyLock
- MobileApp
- FirmwareUpdate
- CSMS

The state can be either "enable", "disable", or "unassigned".

"enable" and "disable" enforce the state to be enable/disable, while unassigned means

Check notice on line 253 in modules/API/README.md

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

modules/API/README.md#L253

Expected: 80; Actual: 86
that the source does not care about the state and other sources may decide.

Each call to this command will update an internal table that looks like this:

| Source | State | Priority |
| ------------ | ---------- | -------- |
| Unspecified | unassigned | 10000 |
| LocalAPI | disable | 42 |
| LocalKeyLock | enable | 0 |

Evaluation will be done based on priorities. 0 is the highest priority,
10000 the lowest, so in this example the connector will be enabled regardless
of what other sources say.
Imagine LocalKeyLock sends a "unassigned, prio 0", the table will then look like this:

Check notice on line 267 in modules/API/README.md

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

modules/API/README.md#L267

Expected: 80; Actual: 86

| Source | State | Priority |
| ------------ | ---------- | -------- |
| Unspecified | unassigned | 10000 |
| LocalAPI | disable | 42 |
| LocalKeyLock | unassigned | 0 |

So now the connector will be disabled, because the second highest priority (42) sets it to disabled.

Check notice on line 275 in modules/API/README.md

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

modules/API/README.md#L275

Expected: 80; Actual: 100

If all sources are unassigned, the connector is enabled.
If two sources have the same priority, "disabled" has priority over "enabled".

### 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.
Legacy command to enable a connector on the EVSE kept for compatibility reasons.
They payload should be a positive integer identifying the connector that should be enabled.

Check notice on line 282 in modules/API/README.md

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

modules/API/README.md#L282

Expected: 80; Actual: 91
If the payload is 0 the whole EVSE is enabled.
It will actually call the following command on everest_api/evse_manager/cmd/enable_enable:
```json
{
"connector_id": 1,
"source": "LocalAPI",
"state": "enable",
"priority": 0
}
```

### 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.
Legacy command to enable a connector on the EVSE kept for compatibility reasons.
Command to disable a connector on the EVSE. They payload should be a positive integer

Check notice on line 296 in modules/API/README.md

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

modules/API/README.md#L296

Expected: 80; Actual: 86
identifying the connector that should be disabled. If the payload is 0 the whole EVSE is disabled.

Check notice on line 297 in modules/API/README.md

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

modules/API/README.md#L297

Expected: 80; Actual: 98
It will actually call the following command on everest_api/evse_manager/cmd/enable_disable:
```json
{
"connector_id": 1,
"source": "LocalAPI",
"state": "disable",
"priority": 0
}
```

### 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
Loading

0 comments on commit bfb8e30

Please sign in to comment.