Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

↔️ 💩 K12/K14 (if possible): hack together a hook up of the external charging limit from the UI to the CSMS #101

Open
shankari opened this issue Jan 13, 2025 · 62 comments

Comments

@shankari
Copy link
Collaborator

shankari commented Jan 13, 2025

There is a new testival coming up, which means a new demo that we will try to work towards.

The goal of this demo is:

  • Simulate a "local controller" that specifies the locally curtailed power available
  • This already lowers the power delivered by overriding the schedule from the CSMS
  • But the power is reset after ~ 20 seconds
1/12/2025, 8:14:28 PMnode: debug 7everest_external/nodered/1/powermeter/totalKw : msg : Object
{ topic: "everest_external/nodered/1/pow…", payload: "10.7", qos: 0, retain: false, _msgid: "7bd9486589f92169" }
1/12/2025, 8:14:28 PMnode: debug 7everest_external/nodered/1/powermeter/totalKw : msg : Object
{ topic: "everest_external/nodered/1/pow…", payload: "5.7", qos: 0, retain: false, _msgid: "cb85db44352f8fb8" }
1/12/2025, 8:14:28 PMnode: debug 6everest_external/nodered/1/cmd/set_max_current : msg : Object
{ topic: "everest_external/nodered/1/cmd…", payload: 9.7, qos: 1, retain: false, _msgid: "0a9be49633bc13b9" }
1/12/2025, 8:14:43 PMnode: debug 7everest_external/nodered/1/powermeter/totalKw : msg : Object
{ topic: "everest_external/nodered/1/pow…", payload: "5.9", qos: 0, retain: false, _msgid: "a208e71acb3e904b" }
1/12/2025, 8:14:44 PMnode: debug 7everest_external/nodered/1/powermeter/totalKw : msg : Object
{ topic: "everest_external/nodered/1/pow…", payload: "10.7", qos: 0, retain: false, _msgid: "c646ba494763ae9d" }

Example logs for overridden power
1/12/2025, 8:14:28 PMnode: debug 7everest_external/nodered/1/powermeter/totalKw : msg : Object
{ topic: "everest_external/nodered/1/pow…", payload: "0.0", qos: 0, retain: false, _msgid: "ce2a72114ccf25c1" }
1/12/2025, 8:14:28 PMnode: debug 7everest_external/nodered/1/powermeter/totalKw : msg : Object
{ topic: "everest_external/nodered/1/pow…", payload: "0.0", qos: 0, retain: false, _msgid: "96935e200a7bd806" }
1/12/2025, 8:14:28 PMnode: debug 7everest_external/nodered/1/powermeter/totalKw : msg : Object
{ topic: "everest_external/nodered/1/pow…", payload: "10.6", qos: 0, retain: false, _msgid: "b36f6ac16bb5a2c3" }
1/12/2025, 8:14:28 PMnode: debug 7everest_external/nodered/1/powermeter/totalKw : msg : Object
{ topic: "everest_external/nodered/1/pow…", payload: "10.4", qos: 0, retain: false, _msgid: "cf11f9a5daeaa551" }
1/12/2025, 8:14:28 PMnode: debug 7everest_external/nodered/1/powermeter/totalKw : msg : Object
{ topic: "everest_external/nodered/1/pow…", payload: "10.6", qos: 0, retain: false, _msgid: "069a2d833263aa92" }
1/12/2025, 8:14:28 PMnode: debug 7everest_external/nodered/1/powermeter/totalKw : msg : Object
{ topic: "everest_external/nodered/1/pow…", payload: "10.6", qos: 0, retain: false, _msgid: "c0b2e8e0cf7fd705" }
1/12/2025, 8:14:28 PMnode: debug 6everest_external/nodered/1/cmd/set_max_current : msg : Object
{ topic: "everest_external/nodered/1/cmd…", payload: 9.7, qos: 1, retain: false, _msgid: "0a9be49633bc13b9" }
1/12/2025, 8:14:28 PMnode: debug 6everest_external/nodered/1/cmd/set_max_current : msg : Object
{ topic: "everest_external/nodered/1/cmd…", payload: 9.4, qos: 1, retain: false, _msgid: "65ee105da9e1fb70" }
1/12/2025, 8:14:28 PMnode: debug 6everest_external/nodered/1/cmd/set_max_current : msg : Object
{ topic: "everest_external/nodered/1/cmd…", payload: 9.3, qos: 1, retain: false, _msgid: "9639cc12eac983a8" }
1/12/2025, 8:14:28 PMnode: debug 6everest_external/nodered/1/cmd/set_max_current : msg : Object
{ topic: "everest_external/nodered/1/cmd…", payload: 9.2, qos: 1, retain: false, _msgid: "a942bc6d1f34774a" }
1/12/2025, 8:14:28 PMnode: debug 6everest_external/nodered/1/cmd/set_max_current : msg : Object
{ topic: "everest_external/nodered/1/cmd…", payload: 9.1, qos: 1, retain: false, _msgid: "7e3f314be3d96837" }
1/12/2025, 8:14:28 PMnode: debug 6everest_external/nodered/1/cmd/set_max_current : msg : Object
{ topic: "everest_external/nodered/1/cmd…", payload: 9, qos: 1, retain: false, _msgid: "73be69204c749c1b" }
1/12/2025, 8:14:28 PMnode: debug 6everest_external/nodered/1/cmd/set_max_current : msg : Object
{ topic: "everest_external/nodered/1/cmd…", payload: 8.9, qos: 1, retain: false, _msgid: "e06ff973e97bab4d" }
1/12/2025, 8:14:28 PMnode: debug 6everest_external/nodered/1/cmd/set_max_current : msg : Object
{ topic: "everest_external/nodered/1/cmd…", payload: 8.8, qos: 1, retain: false, _msgid: "47ef5dbe237b111e" }
1/12/2025, 8:14:28 PMnode: debug 6everest_external/nodered/1/cmd/set_max_current : msg : Object
{ topic: "everest_external/nodered/1/cmd…", payload: 8.7, qos: 1, retain: false, _msgid: "91822759158f1a8f" }
1/12/2025, 8:14:28 PMnode: debug 7everest_external/nodered/1/powermeter/totalKw : msg : Object
{ topic: "everest_external/nodered/1/pow…", payload: "10.5", qos: 0, retain: false, _msgid: "ba93660531baf876" }
1/12/2025, 8:14:28 PMnode: debug 6everest_external/nodered/1/cmd/set_max_current : msg : Object
{ topic: "everest_external/nodered/1/cmd…", payload: 8.6, qos: 1, retain: false, _msgid: "f0985d0755d4d466" }
1/12/2025, 8:14:28 PMnode: debug 7everest_external/nodered/1/powermeter/totalKw : msg : Object
{ topic: "everest_external/nodered/1/pow…", payload: "10.7", qos: 0, retain: false, _msgid: "7bd9486589f92169" }
1/12/2025, 8:14:28 PMnode: debug 7everest_external/nodered/1/powermeter/totalKw : msg : Object
{ topic: "everest_external/nodered/1/pow…", payload: "5.7", qos: 0, retain: false, _msgid: "cb85db44352f8fb8" }
1/12/2025, 8:14:28 PMnode: debug 7everest_external/nodered/1/powermeter/totalKw : msg : Object
{ topic: "everest_external/nodered/1/pow…", payload: "5.7", qos: 0, retain: false, _msgid: "97774d07004cf663" }
1/12/2025, 8:14:28 PMnode: debug 7everest_external/nodered/1/powermeter/totalKw : msg : Object
{ topic: "everest_external/nodered/1/pow…", payload: "5.9", qos: 0, retain: false, _msgid: "41273f6d5338bbf5" }
1/12/2025, 8:14:29 PMnode: debug 7everest_external/nodered/1/powermeter/totalKw : msg : Object
{ topic: "everest_external/nodered/1/pow…", payload: "5.9", qos: 0, retain: false, _msgid: "dfe606745d17168f" }
1/12/2025, 8:14:30 PMnode: debug 7everest_external/nodered/1/powermeter/totalKw : msg : Object
{ topic: "everest_external/nodered/1/pow…", payload: "5.8", qos: 0, retain: false, _msgid: "f871e652a8b61fe0" }
1/12/2025, 8:14:31 PMnode: debug 7everest_external/nodered/1/powermeter/totalKw : msg : Object
{ topic: "everest_external/nodered/1/pow…", payload: "5.9", qos: 0, retain: false, _msgid: "fc4ce5eab681f1ad" }
1/12/2025, 8:14:32 PMnode: debug 7everest_external/nodered/1/powermeter/totalKw : msg : Object
{ topic: "everest_external/nodered/1/pow…", payload: "5.7", qos: 0, retain: false, _msgid: "8d6112906cdea02d" }
1/12/2025, 8:14:33 PMnode: debug 7everest_external/nodered/1/powermeter/totalKw : msg : Object
{ topic: "everest_external/nodered/1/pow…", payload: "5.9", qos: 0, retain: false, _msgid: "d855f31ea7df25f2" }
1/12/2025, 8:14:34 PMnode: debug 7everest_external/nodered/1/powermeter/totalKw : msg : Object
{ topic: "everest_external/nodered/1/pow…", payload: "5.8", qos: 0, retain: false, _msgid: "d8c031c554083103" }
1/12/2025, 8:14:35 PMnode: debug 7everest_external/nodered/1/powermeter/totalKw : msg : Object
{ topic: "everest_external/nodered/1/pow…", payload: "5.8", qos: 0, retain: false, _msgid: "7ed5ae1555622621" }
1/12/2025, 8:14:36 PMnode: debug 7everest_external/nodered/1/powermeter/totalKw : msg : Object
{ topic: "everest_external/nodered/1/pow…", payload: "5.8", qos: 0, retain: false, _msgid: "e383059418aff10f" }
1/12/2025, 8:14:37 PMnode: debug 7everest_external/nodered/1/powermeter/totalKw : msg : Object
{ topic: "everest_external/nodered/1/pow…", payload: "5.8", qos: 0, retain: false, _msgid: "31ba8e79dab83874" }
1/12/2025, 8:14:38 PMnode: debug 7everest_external/nodered/1/powermeter/totalKw : msg : Object
{ topic: "everest_external/nodered/1/pow…", payload: "5.8", qos: 0, retain: false, _msgid: "43d9cea3be120d69" }
1/12/2025, 8:14:39 PMnode: debug 7everest_external/nodered/1/powermeter/totalKw : msg : Object
{ topic: "everest_external/nodered/1/pow…", payload: "5.8", qos: 0, retain: false, _msgid: "27042e524137fed2" }
1/12/2025, 8:14:40 PMnode: debug 7everest_external/nodered/1/powermeter/totalKw : msg : Object
{ topic: "everest_external/nodered/1/pow…", payload: "5.8", qos: 0, retain: false, _msgid: "9174ec11626bdad1" }
1/12/2025, 8:14:41 PMnode: debug 7everest_external/nodered/1/powermeter/totalKw : msg : Object
{ topic: "everest_external/nodered/1/pow…", payload: "5.7", qos: 0, retain: false, _msgid: "ec4e9c0730df7ac1" }
1/12/2025, 8:14:42 PMnode: debug 7everest_external/nodered/1/powermeter/totalKw : msg : Object
{ topic: "everest_external/nodered/1/pow…", payload: "5.9", qos: 0, retain: false, _msgid: "26b39d19a3cf45a3" }
1/12/2025, 8:14:43 PMnode: debug 7everest_external/nodered/1/powermeter/totalKw : msg : Object
{ topic: "everest_external/nodered/1/pow…", payload: "5.9", qos: 0, retain: false, _msgid: "a208e71acb3e904b" }
1/12/2025, 8:14:44 PMnode: debug 7everest_external/nodered/1/powermeter/totalKw : msg : Object
{ topic: "everest_external/nodered/1/pow…", payload: "10.7", qos: 0, retain: false, _msgid: "c646ba494763ae9d" }
1/12/2025, 8:14:45 PMnode: debug 7everest_external/nodered/1/powermeter/totalKw : msg : Object
{ topic: "everest_external/nodered/1/pow…", payload: "10.5", qos: 0, retain: false, _msgid: "3a4a872f545cfd08" }
1/12/2025, 8:14:46 PMnode: debug 7everest_external/nodered/1/powermeter/totalKw : msg : Object
{ topic: "everest_external/nodered/1/pow…", payload: "10.8", qos: 0, retain: false, _msgid: "8c221a3255a1a822" }
1/12/2025, 8:14:47 PMnode: debug 7everest_external/nodered/1/powermeter/totalKw : msg : Object
{ topic: "everest_external/nodered/1/pow…", payload: "10.9", qos: 0, retain: false, _msgid: "91642f5eda2120f0" }
1/12/2025, 8:14:48 PMnode: debug 7everest_external/nodered/1/powermeter/totalKw : msg : Object
{ topic: "everest_external/nodered/1/pow…", payload: "10.5", qos: 0, retain: false, _msgid: "35fa85e6c11ec693" }
1/12/2025, 8:14:49 PMnode: debug 7everest_external/nodered/1/powermeter/totalKw : msg : Object
{ topic: "everest_external/nodered/1/pow…", payload: "10.5", qos: 0, retain: false, _msgid: "ad45e6e21ceac08e" }
1/12/2025, 8:14:50 PMnode: debug 7everest_external/nodered/1/powermeter/totalKw : msg : Object
{ topic: "everest_external/nodered/1/pow…", payload: "10.8", qos: 0, retain: false, _msgid: "1d5fa4d2019ff174" }
1/12/2025, 8:14:51 PMnode: debug 7everest_external/nodered/1/powermeter/totalKw : msg : Object
{ topic: "everest_external/nodered/1/pow…", payload: "10.5", qos: 0, retain: false, _msgid: "07eba930b0f3ba90" }
1/12/2025, 8:14:52 PMnode: debug 7everest_external/nodered/1/powermeter/totalKw : msg : Object
{ topic: "everest_external/nodered/1/pow…", payload: "10.8", qos: 0, retain: false, _msgid: "7590f67d8cd67f09" }
1/12/2025, 8:14:53 PMnode: debug 7everest_external/nodered/1/powermeter/totalKw : msg : Object
{ topic: "everest_external/nodered/1/pow…", payload: "10.7", qos: 0, retain: false, _msgid: "6404b9fb6fa8a041" }
1/12/2025, 8:14:54 PMnode: debug 7everest_external/nodered/1/powermeter/totalKw : msg : Object
{ topic: "everest_external/nodered/1/pow…", payload: "10.6", qos: 0, retain: false, _msgid: "ddd04b3f38b32fd3" }
1/12/2025, 8:14:55 PMnode: debug 7everest_external/nodered/1/powermeter/totalKw : msg : Object
{ topic: "everest_external/nodered/1/pow…", payload: "10.7", qos: 0, retain: false, _msgid: "9eee62c0b176bf97" }
1/12/2025, 8:14:56 PMnode: debug 7everest_external/nodered/1/powermeter/totalKw : msg : Object
{ topic: "everest_external/nodered/1/pow…", payload: "10.7", qos: 0, retain: false, _msgid: "df86c8fb459ad8e5" }
1/12/2025, 8:14:57 PMnode: debug 7everest_external/nodered/1/powermeter/totalKw : msg : Object
{ topic: "everest_external/nodered/1/pow…", payload: "10.9", qos: 0, retain: false, _msgid: "dd6008b3c2691551" }
1/12/2025, 8:14:58 PMnode: debug 7everest_external/nodered/1/powermeter/totalKw : msg : Object
{ topic: "everest_external/nodered/1/pow…", payload: "10.6", qos: 0, retain: false, _msgid: "db49ad0276591811" }
1/12/2025, 8:15:00 PMnode: debug 7everest_external/nodered/1/powermeter/totalKw : msg : Object
{ topic: "everest_external/nodered/1/pow…", payload: "10.8", qos: 0, retain: false, _msgid: "53f6ecb02f0f5218" }
1/12/2025, 8:15:01 PMnode: debug 7everest_external/nodered/1/powermeter/totalKw : msg : Object
{ topic: "everest_external/nodered/1/pow…", payload: "10.6", qos: 0, retain: false, _msgid: "8c96e6b86094d444" }
1/12/2025, 8:15:02 PMnode: debug 7everest_external/nodered/1/powermeter/totalKw : msg : Object
{ topic: "everest_external/nodered/1/pow…", payload: "10.6", qos: 0, retain: false, _msgid: "8e166ab547c3d350" }
1/12/2025, 8:15:03 PMnode: debug 7everest_external/nodered/1/powermeter/totalKw : msg : Object
{ topic: "everest_external/nodered/1/pow…", payload: "10.8", qos: 0, retain: false, _msgid: "1db8e2d9ab56ef62" }
1/12/2025, 8:15:03 PMnode: debug 7everest_external/nodered/1/powermeter/totalKw : msg : Object
{ topic: "everest_external/nodered/1/pow…", payload: "10.7", qos: 0, retain: false, _msgid: "dad5df71e1dab112" }
1/12/2025, 8:15:05 PMnode: debug 7everest_external/nodered/1/powermeter/totalKw : msg : Object
{ topic: "everest_external/nodered/1/pow…", payload: "10.9", qos: 0, retain: false, _msgid: "f6e2f4cc8721309d" }
1/12/2025, 8:15:06 PMnode: debug 7everest_external/nodered/1/powermeter/totalKw : msg : Object
{ topic: "everest_external/nodered/1/pow…", payload: "10.5", qos: 0, retain: false, _msgid: "ccb1c49fb81be877" }
1/12/2025, 8:15:07 PMnode: debug 7everest_external/nodered/1/powermeter/totalKw : msg : Object
{ topic: "everest_external/nodered/1/pow…", payload: "10.6", qos: 0, retain: false, _msgid: "b18ae5d9b83b7d15" }
1/12/2025, 8:15:08 PMnode: debug 7everest_external/nodered/1/powermeter/totalKw : msg : Object
{ topic: "everest_external/nodered/1/pow…", payload: "10.6", qos: 0, retain: false, _msgid: "a8a163e9dc5ac336" }
1/12/2025, 8:15:09 PMnode: debug 7everest_external/nodered/1/powermeter/totalKw : msg : Object
{ topic: "everest_external/nodered/1/pow…", payload: "10.8", qos: 0, retain: false, _msgid: "c4ad09284ca48f04" }
1/12/2025, 8:15:10 PMnode: debug 7everest_external/nodered/1/powermeter/totalKw : msg : Object
{ topic: "everest_external/nodered/1/pow…", payload: "10.8", qos: 0, retain: false, _msgid: "1a06d85ecefb25ed" }
1/12/2025, 8:15:11 PMnode: debug 7everest_external/nodered/1/powermeter/totalKw : msg : Object
{ topic: "everest_external/nodered/1/pow…", payload: "10.6", qos: 0, retain: false, _msgid: "4eb44e877f8f2689" }
1/12/2025, 8:15:12 PMnode: debug 7everest_external/nodered/1/powermeter/totalKw : msg : Object
{ topic: "everest_external/nodered/1/pow…", payload: "10.6", qos: 0, retain: false, _msgid: "2cedf03efa618752" }

Ideally, we will be able to specify the time for which the new current limit is active, have that be part of the composite schedule and have it be reported back to the CSMS using NotifyChargingLimitRequest as specified in K14.

Let's see how complicated this will be to hack together, and whether we can get it done in the next month or so.

@shankari
Copy link
Collaborator Author

The slider sends an MQTT message

topic: "everest_external/nodered/1/cmd/set_max_current"
payload: 10.2
qos: 1
retain: false
_msgid: "b2baa290f8c347e9"

which calls nodered_set_current_limit

    mod->mqtt.subscribe(
        fmt::format("everest_external/nodered/{}/cmd/set_max_current", mod->config.connector_id),
        [&charger = mod->charger, this](std::string data) { mod->nodered_set_current_limit(std::stof(data)); });

which calls update_max_current_limit

// Note: deprecated. Only kept for node red compat.
// This overwrites all other schedules set before.
void EvseManager::nodered_set_current_limit(float max_current) {
    std::scoped_lock lock(external_local_limits_mutex);
    update_max_current_limit(external_local_energy_limits, max_current);
}

Not sure how/where this is stored.
Per #92 (comment)
I thought that this would eventually get to

    // external input to charger: update max_current and new validUntil
    bool set_max_current(float ampere, std::chrono::time_point<date::utc_clock> validUntil);

and it may still do that, but it is not a direct call since the subscribe callback invokes nodered_set_current_limit not set_max_current

@shankari
Copy link
Collaborator Author

With some more logging, we see that, consistent with #92, set_max_current is called consistently even when the charge session is not active and is called every second or so.

2025-01-13 05:09:19.818808 [INFO] evse_manager_1:  :: set_max_current (16, 2025-01-13 05:09:29.760000000) called
2025-01-13 05:09:19.820779 [INFO] evse_manager_1:  :: set_max_current (0, 2025-01-13 05:11:19.820756052) called 
...
2025-01-13 05:09:20.979493 [INFO] evse_manager_1:  :: set_max_current (16, 2025-01-13 05:09:30.965000000) called
2025-01-13 05:09:22.169567 [INFO] evse_manager_1:  :: set_max_current (16, 2025-01-13 05:09:32.113000000) called
2025-01-13 05:09:23.322156 [INFO] evse_manager_1:  :: set_max_current (16, 2025-01-13 05:09:33.264000000) called
2025-01-13 05:09:24.392850 [INFO] evse_manager_1:  :: set_max_current (16, 2025-01-13 05:09:34.377000000) called
2025-01-13 05:09:25.501396 [INFO] evse_manager_1:  :: set_max_current (16, 2025-01-13 05:09:35.487000000) called

Moving the slider sets the current limit until it is reset.

2025-01-13 05:10:01.716524 [INFO] evse_manager_1:  :: set_max_current (16, 2025-01-13 05:10:11.706000000) called
2025-01-13 05:10:01.999019 [INFO] evse_manager_1:  :: nodered called set_current_limit 10
2025-01-13 05:10:02.326188 [INFO] evse_manager_1:  :: nodered called set_current_limit 8.2
2025-01-13 05:10:03.913589 [INFO] evse_manager_1:  :: set_max_current (8.2, 2025-01-13 05:10:13.904000000) called
2025-01-13 05:10:19.661691 [INFO] evse_manager_1:  :: set_max_current (8.2, 2025-01-13 05:10:29.652000000) called
2025-01-13 05:10:20.751873 [INFO] evse_manager_1:  :: set_max_current (16, 2025-01-13 05:10:30.741000000) called

The reset must happen from modules/EvseManager/energy_grid/energyImpl.cpp, energyImpl::handle_enforce_limits
which is invoked periodically. Let's take a look at some additional logs to figure out what is going on here.

@shankari
Copy link
Collaborator Author

  1. Once the slider is changed, the external limits are set through nodered_set_current_limit -> update_max_current_limit
  2. handle_enforce_limits is called ~ every second
  3. then, the composite schedules are recalculated every ~ 30 seconds. Since the external limits are currently only stored in the power tree, they are not part of the composite schedule, so the limit is reset to 16
Logs from the reset
2025-01-13 06:09:54.625467 [INFO] evse_manager_1:  :: Incoming enforce limits{
    "limits_root_side": {
        "ac_max_current_A": 9.399999618530273,
        "ac_max_phase_count": 3
    },
    "schedule": [
        {
            "limits_to_root": {
                "ac_max_current_A": 9.399999618530273,
                "ac_max_phase_count": 3
            },
            "timestamp": "2025-01-13T06:00:00.000Z"
        },
        {
            "limits_to_root": {
                "ac_max_current_A": 9.399999618530273,
                "ac_max_phase_count": 3
            },
            "timestamp": "2025-01-13T06:09:43.885Z"
        }
    ],
    "uuid": "evse_manager_1",
    "valid_until": "2025-01-13T06:10:04.571Z"
}
2025-01-13 06:09:54.627149 [INFO] evse_manager_1:  :: Update limit at charger 9.4
2025-01-13 06:09:54.627654 [INFO] evse_manager_1:  :: set_max_current (9.4, 2025-01-13 06:10:04.571000000) called

2025-01-13 06:09:55.728746 [INFO] evse_manager_1:  :: Incoming enforce limits{
    "limits_root_side": {
        "ac_max_current_A": 9.399999618530273,
        "ac_max_phase_count": 3
    },
    "schedule": [
        {
            "limits_to_root": {
                "ac_max_current_A": 9.399999618530273,
                "ac_max_phase_count": 3
            },
            "timestamp": "2025-01-13T06:00:00.000Z"
        },
        {
            "limits_to_root": {
                "ac_max_current_A": 9.399999618530273,
                "ac_max_phase_count": 3
            },
            "timestamp": "2025-01-13T06:09:43.885Z"
        }
    ],
    "uuid": "evse_manager_1",
    "valid_until": "2025-01-13T06:10:05.720Z"
}
2025-01-13 06:09:55.729577 [INFO] evse_manager_1:  :: Update limit at charger 9.4
2025-01-13 06:09:55.729820 [INFO] evse_manager_1:  :: set_max_current (9.4, 2025-01-13 06:10:05.720000000) called

2025-01-13 06:09:55.889258 [INFO] ocpp:OCPP201     :: About to publish composite charging schedules: 
2025-01-13 06:09:55.889624 [INFO] ocpp:OCPP201     :: {
    "chargingRateUnit": "A",
    "chargingSchedulePeriod": [
        {
            "limit": 16.0,
            "numberPhases": 3,
            "startPeriod": 0
        }
    ],
    "duration": 600,
    "evseId": 0,
    "scheduleStart": "2025-01-13T06:09:55.000Z"
}
2025-01-13 06:09:55.890421 [INFO] ocpp:OCPP201     :: {
    "chargingRateUnit": "A",
    "chargingSchedulePeriod": [
        {
            "limit": 16.0,
            "numberPhases": 3,
            "startPeriod": 0
        }
    ],
    "duration": 600,
    "evseId": 1,
    "scheduleStart": "2025-01-13T06:09:55.000Z"
}
2025-01-13 06:09:56.875453 [INFO] evse_manager_1:  :: Incoming enforce limits{
    "limits_root_side": {
        "ac_max_current_A": 16.0,
        "ac_max_phase_count": 3
    },
    "schedule": [
        {
            "limits_to_root": {
                "ac_max_current_A": 16.0,
                "ac_max_phase_count": 3
            },
            "timestamp": "2025-01-13T06:00:00.000Z"
        },
        {
            "limits_to_root": {
                "ac_max_current_A": 16.0,
                "ac_max_phase_count": 3
            },
            "timestamp": "2025-01-13T06:09:55.891Z"
        }
    ],
    "uuid": "evse_manager_1",
    "valid_until": "2025-01-13T06:10:06.865Z"
}

@shankari
Copy link
Collaborator Author

shankari commented Jan 13, 2025

However, the calculateCompositeSchedules method does appear to support external limits as input
https://github.com/EVerest/libocpp/blob/04b5b75aa614161a17eb24aa5e8a59dfa7ad1c4e/lib/ocpp/v201/smart_charging.cpp#L604

    std::vector<period_entry_t> charging_station_external_constraints_periods{};
    std::vector<period_entry_t> charge_point_max_periods{};
    std::vector<period_entry_t> tx_default_periods{};
    std::vector<period_entry_t> tx_periods{};

So the next steps should be:

  • figure out where the external constraints are read from
  • store the slider result to that location
  • add a second slider for the curtailment duration
  • figure out how to initiate a message from the station to the CSMS to send the NotifyChargingLimitRequest

@Abby-Wheelis for visibility

@shankari
Copy link
Collaborator Author

figure out where the external constraints are read from

The computation for the composite schedule reads all valid profiles and iterates through them. So we should be able to call add_profile with the limit and the duration and ChargingLimitSourceEnum::EMS. However, it is not clear if the EMS should be able to set that, or whether the CSMS should set it in response to NotifyChargingLimitRequest.

The CSMS response to NotifyChargingLimitRequest has not been defined in the spec, so let's go with this for now, but discuss with the community at PlugFest.

@shankari
Copy link
Collaborator Author

The value from the CSMS is set using (effectively)

void ChargePoint::handle_set_charging_profile_req(Call<SetChargingProfileRequest> call) {
    response = this->smart_charging_handler->conform_validate_and_add_profile(msg.chargingProfile, msg.evseId);
    ocpp::CallResult<SetChargingProfileResponse> call_result(response, call.uniqueId);
    this->message_dispatcher->dispatch_call_result(call_result);
}

Presumably we will not want to call ChargePoint::handle_set_charging_profile_req or smart_charging_handler->conform_validate_and_add_profile directly from another module (EvseManager). We need to figure out how to send it via MQTT

@shankari
Copy link
Collaborator Author

So I believe that the internal MQTT communication is through "interfaces", defined in
https://github.com/EVerest/everest-core/tree/main/interfaces

The OCPP module defines 5 interfaces

  auth_validator:
    description: Validates the provided token using CSMS, AuthorizationList or AuthorizationCache
    interface: auth_token_validator
  auth_provider:
    description: Provides authorization requests by CSMS
    interface: auth_token_provider
  data_transfer:
    description: OCPP data transfer towards the CSMS
    interface: ocpp_data_transfer
  ocpp_generic:
    description: Generic OCPP interface.
    interface: ocpp
  session_cost:
    description: Send session cost
    interface: session_cost

However, checking the interfaces, I don't see any option to receive a charging profile in any of those interfaces.

https://github.com/EVerest/everest-core/blob/main/interfaces/ocpp.yaml

I think we will want to essentially plumb through a set_charging_profile, similar to change_availability and allow it to be set "internally (as can be done by the CSMS)".

@shankari
Copy link
Collaborator Author

For an example of calling methods in other modules, https://github.com/EVerest/everest-core/tree/3401718254fc6ac2c6e741c5b9db83b5bbc80dc6/modules/OCPPExtensionExample seems like a good, simple example that calls OCPP. We will need to add OCPP as a dependency into one of the other modules to call this method.

I think that, at least for a first pass, it makes sense that we should call this directly from nodered_set_current_limit, which would imply the EvseManager module.

While this is a first pass/hack, there might be better options and we should discuss this with the community for longer-term planning. This would involve adding the new dependency to EvseManager, which is sub-optimal. We would also need to decide the response time; should the call from node-red immediately modify the limit, or should we wait until the composite schedule is next computed?

@shankari
Copy link
Collaborator Author

I think that, at least for a first pass, it makes sense that we should call this directly from nodered_set_current_limit, which would imply the EvseManager module.

Actually, I take this back. I think it might be better to hook it up to the "API" module. In the real world, it is not clear how the EMS signals will come in to EVerest, but it makes more sense that it would be through an "API" module instead of the "EnergyManager" module.

The "API" module already depends on both EVSEManager and OCPP, so we would not have to complicate the dependency chain further.

And it is designed for "exposing some internal functionality on an external MQTT connection". So it should have access to MQTT. Let's see if we can listen to the same node-red message in API and republish it (that's sort of the point of pub-sub in the first place).

@shankari
Copy link
Collaborator Author

Ah, it looks like this will be good - EVSE manager listens to these commands

  // Interface to Node-RED debug UI

    mod->mqtt.subscribe(
        fmt::format("everest_external/nodered/{}/cmd/set_max_current", mod->config.connector_id),
        [&charger = mod->charger, this](std::string data) { mod->nodered_set_current_limit(std::stof(data)); });

    mod->mqtt.subscribe(
        fmt::format("everest_external/nodered/{}/cmd/set_max_watt", mod->config.connector_id),
        [&charger = mod->charger, this](std::string data) { mod->nodered_set_watt_limit(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_disable(0, {types::evse_manager::Enable_source::LocalAPI,
                                                        types::evse_manager::Enable_state::Enable, 100});
                        });

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

And "API" listens to them too, including to set_limit_amps.
https://github.com/EVerest/everest-core/blob/3401718254fc6ac2c6e741c5b9db83b5bbc80dc6/modules/API/API.cpp#L502

        std::string cmd_set_limit = cmd_base + "set_limit_amps";

@shankari
Copy link
Collaborator Author

The note for set_limit_amps says

Command to set an amps limit for this EVSE that will be considered within the EnergyManager. This does not automatically imply that this limit will be set by the EVSE because the energymanagement might consider limitations from other sources, too. The payload can be a positive or negative number.

📌 **Note:** You have to configure one evse_energy_sink connection per EVSE within the configuration file in order to use this topic!

This was refactored two months ago, so is not in the latest stable release from Sept (2024.9.0) that we are using
EVerest/everest-core@01dee7b

The copy that we have checked out also has a set_limit_amps which essentially calls set_external_limits.
Let's try just changing the node_red command to publish to the new topic. If we want to be backwards compat, we could always publish two topics, but given that this area has already changed significantly, I'm just going to go ahead and rip off the bandaid, at least in the demo.

We need to see if the community agrees!

@shankari
Copy link
Collaborator Author

Naive change is not doing anything.
We see

topic: "everest_external/nodered/1/cmd/set_limit_amps"
payload: 14.3
qos: 1
retain: false
_msgid: "dee17febfda6e258"

But

2025-01-13 21:07:37.053234 [INFO] evse_manager_1:  :: Incoming enforce limits{
    "limits_root_side": {
        "ac_max_current_A": 10.0,
        "ac_max_phase_count": 3
    },
    "schedule": [
        {
            "limits_to_root": {
                "ac_max_current_A": 10.0,
                "ac_max_phase_count": 3
            },
            "timestamp": "2025-01-13T21:00:00.000Z"
        },
        {
            "limits_to_root": {
                "ac_max_current_A": 10.0,
                "ac_max_phase_count": 3
            },
            "timestamp": "2025-01-13T21:07:23.112Z"
        }
    ],
    "uuid": "evse_manager_1",
    "valid_until": "2025-01-13T21:07:47.039Z"
}

Let's add some logs to understand further

@shankari
Copy link
Collaborator Author

Having issues recompiling with the changes. Notably, when I tried to call

    response = this->mod->charge_point->smart_charging_handler->conform_validate_and_add_profile(msg.charging_profile, msg.evse);

This failed because smart_charging_handler was protected in the chargepoint.
I had to move it to public to make progress, but making the pointer be public is not great. We should consider exposing a function in the public interface instead.

@shankari
Copy link
Collaborator Author

shankari commented Jan 14, 2025

Running into mismatch in function signatures, but they look identical to me!

/usr/include/c++/12/bits/unique_ptr.h:1065:30: error: invalid new-expression of abstract class type 'module::ocpp_generic::ocppImpl'
 1065 |     { return unique_ptr<_Tp>(new _Tp(std::forward<_Args>(__args)...)); }
      |                              ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In file included from /ext/build/generated/modules/OCPP/ld-ev.cpp:15:
/ext/source/modules/OCPP/ocpp_generic/ocppImpl.hpp:24:7: note:   because the following virtual functions are pure within 'module::ocpp_generic::ocppImpl':
   24 | class ocppImpl : public ocppImplBase {
      |       ^~~~~~~~
In file included from /ext/source/modules/OCPP/OCPP.hpp:16,
                 from /ext/build/generated/modules/OCPP/ld-ev.cpp:10:
/ext/build/generated/include/generated/interfaces/ocpp/Implementation.hpp:117:53: note:     'virtual types::ocpp::SetChargingProfileResponse ocppImplBase::handle_set_charging_profile(types::ocpp::SetChargingProfileRequest&)'
  117 |     virtual types::ocpp::SetChargingProfileResponse handle_set_charging_profile(types::ocpp::SetChargingProfileRequest& request) = 0;
      |                                                     ^~~~~~~~~~~~~~~~~~~~~~~~~~~

is

types::ocpp::SetChargingProfileResponse
ocppImpl::handle_set_charging_profile(types::ocpp::SetChargingProfileRequest& request) {
...
}

@shankari
Copy link
Collaborator Author

shankari commented Jan 14, 2025

I temporarily changed the definition of set_charging_profile to take no arguments and return a boolean,
so now the error is

/ext/source/modules/OCPP201/ocpp_generic/ocppImpl.hpp:47:5: error: 'virtual types::ocpp::SetChargingProfileResponse module::ocpp_generic::ocppImpl::handle_set_charging_profile(types::ocpp::SetChargingProfileRequest&)' marked 'override', but does not override
   47 |     handle_set_charging_profile(types::ocpp::SetChargingProfileRequest& request) override;
      |     ^~~~~~~~~~~~~~~~~~~~~~~~~~~

After changing the hpp file, I get

/usr/include/c++/12/bits/unique_ptr.h:1065:30: error: invalid new-expression of abstract class type 'module::ocpp_generic::ocppImpl'
 1065 |     { return unique_ptr<_Tp>(new _Tp(std::forward<_Args>(__args)...)); }
      |                              ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In file included from /ext/build/generated/modules/OCPP/ld-ev.cpp:15:
/ext/source/modules/OCPP/ocpp_generic/ocppImpl.hpp:24:7: note:   because the following virtual functions are pure within 'module::ocpp_generic::ocppImpl':
   24 | class ocppImpl : public ocppImplBase {
      |       ^~~~~~~~
In file included from /ext/source/modules/OCPP/OCPP.hpp:16,
                 from /ext/build/generated/modules/OCPP/ld-ev.cpp:10:
/ext/build/generated/include/generated/interfaces/ocpp/Implementation.hpp:117:18: note:     'virtual bool ocppImplBase::handle_set_charging_profile()'
  117 |     virtual bool handle_set_charging_profile() = 0;
      |                  ^~~~~~~~~~~~~~~~~~~~~~~~~~~

so the original mismatch must have been between hpp and cpp.

Making the same changes to the cpp, I still get the error. This is a weird issue in the cpp and I just have to poke at it until it works.

@shankari
Copy link
Collaborator Author

shankari commented Jan 14, 2025

Ah it is because we have an implementation in the OCPP201 module, but not the OCPP module. Adding it to both, with the 1.6 implementation as a NOP.

@shankari
Copy link
Collaborator Author

wrt #101 (comment)

the reason is because the format of the message is different.

2025-01-14 03:29:24.286927 [INFO] api:API          :: SHANKARI: called API init: 
2025-01-14 03:29:24.326996 [INFO] api:API          :: SHANKARI: cmd_base is: everest_api/evse_manager_1/cmd/
2025-01-14 03:29:24.361164 [INFO] api:API          :: SHANKARI: cmd_set_limit: everest_api/evse_manager_1/cmd/set_limit_amps

Switching to the new message format, it werks!

2025-01-14 03:32:08.564233 [INFO] api:API          :: SHANKARI: Received set_limit_amps callback.
2025-01-14 03:32:09.351186 [INFO] evse_manager_1:  :: Update limit at charger 9.3
2025-01-14 03:32:09.351537 [INFO] evse_manager_1:  :: set_max_current (9.3, 2025-01-14 03:32:19.340000000) called

...

2025-01-14 03:32:28.104959 [INFO] ocpp:OCPP201     :: About to publish composite charging schedules: 
2025-01-14 03:32:28.106167 [INFO] ocpp:OCPP201     :: {
    "chargingRateUnit": "A",
    "chargingSchedulePeriod": [
        {
            "limit": 10.0,
            "numberPhases": 3,
            "startPeriod": 0
        }
    ],
    "duration": 600,
    "evseId": 0,
    "scheduleStart": "2025-01-14T03:32:28.000Z"
}
2025-01-14 03:32:28.106783 [INFO] ocpp:OCPP201     :: {
    "chargingRateUnit": "A",
    "chargingSchedulePeriod": [
        {
            "limit": 10.0,
            "numberPhases": 3,
            "startPeriod": 0
        }
    ],
    "duration": 600,
    "evseId": 1,
    "scheduleStart": "2025-01-14T03:32:28.000Z"
}
...
2025-01-14 03:32:29.837879 [INFO] evse_manager_1:  :: Update limit at charger 10
2025-01-14 03:32:29.838204 [INFO] evse_manager_1:  :: set_max_current (10, 2025-01-14 03:32:39.786000000) called
2025-01-14 03:32:31.005929 [INFO] evse_manager_1:  :: Update limit at charger 10
2025-01-14 03:32:31.006314 [INFO] evse_manager_1:  :: set_max_current (10, 2025-01-14 03:32:40.954000000) called

Although the resetting is still happening. Let's just quickly add the call from the API to the set_charging_profile method on the OCPP module.

@shankari
Copy link
Collaborator Author

shankari commented Jan 14, 2025

Although the API could use the OCPP module, the config we were using did not have it as a dependency.
After adding it, I think I have all the plumbing largely hooked up now. When we change the slider in the UI, we get the callback in the API

2025-01-14 04:06:45.891495 [INFO] api:API          :: SHANKARI: Received set_limit_amps callback with 7.7

The API then calls the EVSE manager to set the limit to 7.6

2025-01-14 04:06:45.945972 [INFO] api:API          :: SHANKARI: Finished calling EVSE manager with limit: {
    "schedule_import": [
        {
            "limits_to_leaves": {
                "ac_max_current_A": 7.699999809265137
            },
            "limits_to_root": {},
            "timestamp": "2025-01-14T04:06:45.892Z"
        }
    ]
}

And then we receive a SetChargingProfileRequest in the OCPP callback!

2025-01-14 04:06:45.954821 [INFO] ocpp:OCPP201     :: Received internal SetChargingProfileRequest: for -218467168 with purpose ChargingStationExternalConstraints

I haven't populated the SetChargingProfileRequest fully because it is annoying to have such a deeply nested object, so the value still gets reset to 10. But if we do populate it properly, I believe this will work.

@Abby-Wheelis this is all yours now!

@shankari
Copy link
Collaborator Author

shankari commented Jan 14, 2025

I have attached patches of the changes that I have made so far.

The changes were to:

  1. config/config-sil-ocpp201-pnc.yaml, add the following to the API module dependencies
       evse_manager:
         - module_id: evse_manager_1
           implementation_id: evse
+      ocpp:
+        - module_id: ocpp
+          implementation_id: ocpp_generic
       error_history:
         - module_id: error_history
           implementation_id: error_history
  1. interfaces/ocpp.yaml: add the new method
  2. modules/API/API.cpp: add some logging, and the call to the OCPP module
  3. modules/OCPP/ocpp_generic/ocppImpl.*: add a NOP implementation of the new method
  4. modules/OCPP201/ocpp_generic/ocppImpl.*: add an implementation that calls the smart charging handler
  5. types/ocpp.yaml: Add the SetChargingProfileRequest and SetChargingProfileResponse data types so we can use them as types::ocpp::SetChargingProfileRequest. Note that the names are subtly different - using _ instead of CamelCase. I wonder if we should hew so closely to the OCPP Request/Response definition here, or simplify this internal interface to just specify the limit and the duration
  6. /ext/cache/cpm/libocpp/56452082640eeee05feec42f2f502d6beb8e684c/libocpp/include/ocpp/v201/charge_point.hpp: make the smart_charging_handler public so we can call it from the OCPP 201 module. This is sub-optimal and needs to be cleaned up, potentially by adding a new function to the charge_point class.
  7. nodered/config/config-sil-iso15118-ac-flow.json: change the MaxCurrent slider in the "Connector 1" tab to publish to the topic everest_api/evse_manager_1/cmd/set_limit_amps

I have attached:

  1. patch file for all the changes in /ext/source (changes 2-6 above)
    call_ocpp_from_the_api.patch
  2. one patch for the changes in /ext/cache/ (change 7 above)
    make_smart_charging_handler_public.patch
  3. changes 1 and 8 can be made manually
  4. I have also added a patch for additional logging in case you find it helpful
    add_extra_logging_charge_schedules.patch

I described them instead of just checking them in and creating a PR because:

  • the code is not fully tested end-to-end
  • I think that reproducing this might make it easier to really understand the changes instead of applying a bunch of pre-prepared patches through a script

@Abby-Wheelis LMK if you need any additional support

@shankari
Copy link
Collaborator Author

Note that, instead of creating the SetChargingProfileRequest through code, you should be able to copy/paste JSON and use something like
https://github.com/EVerest/everest-core/blob/3401718254fc6ac2c6e741c5b9db83b5bbc80dc6/modules/API/API.cpp#L223

along with the from_json method in
..//build/generated/include/generated/types/ocpp.hpp

struct SetChargingProfileRequest {
        int32_t evse; ///< The OCPP 2.0.1 EVSE ID (not used in OCPP 1.6).
        types::ocpp::ChargingProfile charging_profile; ///< TODO: description

    /// \brief Conversion from a given SetChargingProfileRequest \p k to a given json object \p j
    friend void to_json(json& j, const SetChargingProfileRequest& k) {
        // the required parts of the type
                j = json{{"evse",        k.evse},{"charging_profile",        k.charging_profile},        };
                // the optional parts of the type
    }

    /// \brief Conversion from a given json object \p j to a given SetChargingProfileRequest \p k
    friend void from_json(const json& j, SetChargingProfileRequest& k) {
        // the required parts of the type
                    k.evse =                            j.at("evse");
                    k.charging_profile =                            j.at("charging_profile");

        // the optional parts of the type
    }

    /// \brief Writes the string representation of the given SetChargingProfileRequest \p k to the given output stream \p os
    /// \returns an output stream with the SetChargingProfileRequest written to
    friend std::ostream& operator<<(std::ostream& os, const SetChargingProfileRequest& k) {
        os << json(k).dump(4);
        return os;
    }

};

@Abby-Wheelis
Copy link
Contributor

After applying the patches, I tried to build, but there were error messages during the build. It still seemed to complete the build, just slowly. Are these errors normal?

FAILED: _deps/libocpp-build/lib/CMakeFiles/ocpp.dir/ocpp/v201/charge_point.cpp.o 
/usr/bin/ccache /usr/bin/c++ -DBOOST_ALL_NO_LIB -DBOOST_ATOMIC_DYN_LINK -DBOOST_CHRONO_DYN_LINK -DBOOST_FILESYSTEM_DYN_LINK -DBOOST_LOG_DYN_LINK -DBOOST_REGEX_DYN_LINK -DBOOST_SYSTEM_DYN_LINK -DBOOST_THREAD_DYN_LINK -DMIGRATION_DEVICE_MODEL_FILE_VERSION_V201=1 -DMIGRATION_FILE_VERSION_V16=3 -DMIGRATION_FILE_VERSION_V201=6 -DONLY_C_LOCALE=0 -DUSE_OS_TZDB=1 -I/ext/cache/cpm/libocpp/56452082640eeee05feec42f2f502d6beb8e684c/libocpp/include -I/ext/cache/cpm/libocpp/56452082640eeee05feec42f2f502d6beb8e684c/libocpp/3rd_party -I/ext/cache/cpm/liblog/5d578ffd5bc0e07e1c9bd75d269f3692d33a96ca/liblog/include -I/ext/cache/cpm/libtimer/5f95893b9dd3ee5a130229e9a42efaedcd74640b/libtimer/include -I/ext/cache/cpm/date/91ef8592e97dd3a2a15aed9de5dbf7e3ff2636eb/date/include -I/ext/cache/cpm/nlohmann_json_schema_validator/d0cc0ac5dcfe136af525cbd99ce63d6e750a949f/nlohmann_json_schema_validator/src -I/ext/cache/cpm/nlohmann_json/b3708972f6694fe462e4112e47aa04f10d2390b4/nlohmann_json/include -I/ext/cache/cpm/libevse-security/39e30fb0cb91bd822b5c9056e9fe69d4ce9de43f/libevse-security/include -I/ext/cache/cpm/libwebsockets/3866e4bc96015c77bf61889acd9dd6f4642df6fe/libwebsockets/lib/../include -I/ext/build/_deps/libwebsockets-build/lib/../include -g --coverage -fprofile-abs-path -g -Wimplicit-fallthrough -std=gnu++17 -MD -MT _deps/libocpp-build/lib/CMakeFiles/ocpp.dir/ocpp/v201/charge_point.cpp.o -MF _deps/libocpp-build/lib/CMakeFiles/ocpp.dir/ocpp/v201/charge_point.cpp.o.d -o _deps/libocpp-build/lib/CMakeFiles/ocpp.dir/ocpp/v201/charge_point.cpp.o -c /ext/cache/cpm/libocpp/56452082640eeee05feec42f2f502d6beb8e684c/libocpp/lib/ocpp/v201/charge_point.cpp
c++: fatal error: Killed signal terminated program cc1plus
compilation terminated.
[4/21] Building CXX object modules/CMakeFiles/OCPP201.dir/OCPP201/OCPP201.cpp.o
FAILED: modules/CMakeFiles/OCPP201.dir/OCPP201/OCPP201.cpp.o 
/usr/bin/ccache /usr/bin/c++ -DBOOST_ALL_NO_LIB -DBOOST_ATOMIC_DYN_LINK -DBOOST_CHRONO_DYN_LINK -DBOOST_FILESYSTEM_DYN_LINK -DBOOST_LOG_DYN_LINK -DBOOST_REGEX_DYN_LINK -DBOOST_SYSTEM_DYN_LINK -DBOOST_THREAD_DYN_LINK -DFMT_SHARED -DONLY_C_LOCALE=0 -DUSE_OS_TZDB=1 -I/ext/source/modules/OCPP201 -I/ext/build/generated/include -I/ext/build/generated/modules/OCPP201 -I/ext/build/_deps/everest-framework-build/generated -I/ext/cache/cpm/everest-framework/d01be681d203f70289900147e87a8f4c43ff47f0/everest-framework/include -I/ext/cache/cpm/date/91ef8592e97dd3a2a15aed9de5dbf7e3ff2636eb/date/include -I/ext/cache/cpm/nlohmann_json/b3708972f6694fe462e4112e47aa04f10d2390b4/nlohmann_json/include -I/ext/cache/cpm/nlohmann_json_schema_validator/d0cc0ac5dcfe136af525cbd99ce63d6e750a949f/nlohmann_json_schema_validator/src -I/ext/cache/cpm/libfmt/db7252a8aa3d829180246c17fb97ef0f367a2322/libfmt/include -I/ext/cache/cpm/liblog/5d578ffd5bc0e07e1c9bd75d269f3692d33a96ca/liblog/include -I/ext/cache/cpm/libocpp/56452082640eeee05feec42f2f502d6beb8e684c/libocpp/include -I/ext/cache/cpm/libocpp/56452082640eeee05feec42f2f502d6beb8e684c/libocpp/3rd_party -I/ext/cache/cpm/libtimer/5f95893b9dd3ee5a130229e9a42efaedcd74640b/libtimer/include -I/ext/cache/cpm/libevse-security/39e30fb0cb91bd822b5c9056e9fe69d4ce9de43f/libevse-security/include -I/ext/cache/cpm/libwebsockets/3866e4bc96015c77bf61889acd9dd6f4642df6fe/libwebsockets/lib/../include -I/ext/build/_deps/libwebsockets-build/lib/../include -I/ext/source/lib/staging/ocpp -g --coverage -fprofile-abs-path -g -Wall -Wno-unused-function -Wimplicit-fallthrough -Werror=switch-enum -std=gnu++17 -MD -MT modules/CMakeFiles/OCPP201.dir/OCPP201/OCPP201.cpp.o -MF modules/CMakeFiles/OCPP201.dir/OCPP201/OCPP201.cpp.o.d -o modules/CMakeFiles/OCPP201.dir/OCPP201/OCPP201.cpp.o -c /ext/source/modules/OCPP201/OCPP201.cpp
c++: fatal error: Killed signal terminated program cc1plus
compilation terminated.

@shankari
Copy link
Collaborator Author

@Abby-Wheelis given that the process was terminated, it looks like you just might not have enough memory. When I tried to build, the process terminated at 16GB but completed at 24GB RAM. I also have 250 GB disk allocated to docker, although I didn't try changing that slowly, so you probably don't need that much.

@Abby-Wheelis
Copy link
Contributor

My personal laptop that I'm testing on right now only has 8GB of memory, so that could be the issue!

@Abby-Wheelis
Copy link
Contributor

I bumped it from 6 (what it was set to) to 8 and still see the errors, I can try on my work laptop next

@Abby-Wheelis
Copy link
Contributor

I am able to get a charging session to start, and when I move the slider there is a change in the charging behavior, but it is still only temporary, which is to be expected:

I haven't populated the SetChargingProfileRequest fully because it is annoying to have such a deeply nested object, so the value still gets reset to 10. But if we do populate it properly, I believe this will work.

I'm also a little worried that I'm not seeing the right logging messages, which makes me think that a) I applied the patches wrong or b) since the build is failing it is just defaulting to some older version.

The first spot I see room for error on the patches with is the nodered changes, which I'm not sure at all if I got right, and if the signal is never sent, then none of the resulting changes will happen, regardless of if the patches are implemented.

I'm also seeing lots of errors from OCPP about timeout and failed connection, so I need to figure out what has gone wrong there.

@Abby-Wheelis
Copy link
Contributor

These are the repeated OCPP errors that I see:

2025-01-15 21:04:32.239969 [ERRO] ocpp:OCPP201    int ocpp::WebsocketTlsTPM::process_callback(void*, int, void*, void*, size_t) :: CLIENT_CONNECTION_ERROR: HS: ws upgrade response not 101
2025-01-15 21:04:32.240369 [ERRO] ocpp:OCPP201    void ocpp::WebsocketTlsTPM::on_conn_fail() :: OCPP client connection to server failed
...
2025-01-15 21:05:31.602220 [INFO] ocpp:OCPP201     :: Closing websocket: reconnect attempts exhausted
2025-01-15 21:05:31.607806 [WARN] ocpp:OCPP201    ocpp::v201::ConnectivityManager::init_websocket()::<lambda(ocpp::WebsocketCloseReason)> :: Closed websocket of NetworkConfigurationPriority: 1 which is configurationSlot 1

These could very likely be related to the fact that OCPP is one of the sources of build/compile errors, so it could just BE broken on this installation.

In my Docker Desktop, 3/4 Maeve containers are down, and will not start up from the UI an don't start when I start up the manager, either, so that's very likely the problem - if one piece isn't running they surely can't contact each other.

I can try wiping and rebuilding, but it seems the real problem is my tech limits...

@Abby-Wheelis
Copy link
Contributor

Trying on my work laptop again, and the build process fails in the same way it does on my personal computer with the process being terminated, and my resources are as high as they go.

@Abby-Wheelis
Copy link
Contributor

Now I have a loaner laptop with some more resources and can build, slowly. I changed the number of jobs to run in parallel to 1 ninja -j1 -C "$EXT_MOUNT/build" on the last line of /ext/scripts/compile.sh to make sure it did not terminate due to lack of resources, which is working for now. I will up the number slowly to find the right balance of speed and not running out of memory!

I am seeing the new log messages when I move the slider now! So it seems that the API module is recieving the message from MQTT when I move the slider.

2025-01-23 17:39:24.775351 [INFO] api:API          :: SHANKARI: Received set_limit_amps callback with 13.6
2025-01-23 17:39:24.818434 [INFO] api:API          :: SHANKARI: Finished calling EVSE manager with limit: {
    "schedule_import": [
        {
            "limits_to_leaves": {
                "ac_max_current_A": 13.600000381469727
            },
            "limits_to_root": {},
            "timestamp": "2025-01-23T17:39:24.775Z"
        }
    ]
}
2025-01-23 17:39:24.818667 [INFO] api:API          :: SHANKARI: ocpp size: 0

The next task is making sure that the limit sticks around. @shankari do you have any more patches that you've made or should I proceed with what you posted here?

@shankari
Copy link
Collaborator Author

@Abby-Wheelis no additional patches, sorry. have been busy with other tasks. You should proceed with what I have posted.

Just to clarify: the logs above just show that the change makes it to the EVSE manager. We need to make sure that it makes it through from the EVSE manager to the OCPP module so that we can save it as part of the charging profiles, and send it up to the CSMS.

That is the log line

2025-01-14 04:06:45.954821 [INFO] ocpp:OCPP201     :: Received internal SetChargingProfileRequest: for -218467168 with purpose ChargingStationExternalConstraints

I think you are not seeing that because ocpp is not configured as a "connection" to the API module, so the value from the slider will still be

2025-01-23 17:39:24.818667 [INFO] api:API          :: SHANKARI: ocpp size: 0

You need to edit the config.yaml per #101 (comment)
so that the callback is received in the OCPP module, and then you can hook it up to the smart_charging_handler and validate that it shows up in the composite schedule!

@shankari
Copy link
Collaborator Author

We don't have a local controller in the demo - only a station talking directly to the CSMS. So what we really need is K12 and if possible, K11.

@Abby-Wheelis
Copy link
Contributor

Abby-Wheelis commented Jan 24, 2025

A couple of UI updates to enable resetting the limit. This includes the change to send the limit (8 in #101 (comment)).

config-sil-iso15118-ac-flow.json

In the slider element in the nodered flow, there is a checkbox for "pass input msg into output" and leaving that unchecked should keep resetting the slider from sending out the limit as 16, but that behavior is something to watch for, it was a little buggy when I was testing, and hard to test since the limit still resets regularly.

@shankari
Copy link
Collaborator Author

shankari commented Jan 25, 2025

Quick update: I think I have everything hooked up now!

  • Send set_limit/clear_limit from UI to API
  • Wrap limit in a ChargingSchedule and send it from API to OCPP module
  • Wrap ChargingSchedule in a ChargingProfile and send it to the smart charging handler for addition
  • Add it in the SmartChargingHandler
  • Send the notify message to the CSMS

However it is still not working as intended because of the following issues

  • Minor: every set_limit from the UI also comes with a clear_limit (not sure if this is desired)
  • Major: the new charging schedule is saved in the database, but is not incorporated into the composite schedule, so the reset behavior still happens
  • Major: we get a timeout from the CSMS while sending them the notify message
Logs from a single adjustment of the slider below the fold

We first receive a clear_limit_amps although I did not use the "clear limits" button, and get a timeout from the MessageQueue.

2025-01-25 17:06:15.534737 [INFO] api:API          :: SHANKARI: Received clear_limit_amps callback with 14
2025-01-25 17:06:15.534931 [INFO] api:API          :: SHANKARI: ocpp size: 1
2025-01-25 17:06:15.537047 [INFO] ocpp:OCPP201     :: Received internal cleared_charging_limit: for 1
2025-01-25 17:06:15.540774 [INFO] ocpp:OCPP201     :: Returning from on_charging_limit_cleared with 1
2025-01-25 17:06:15.543522 [ERRO] ocpp:OCPP201    ocpp::EnhancedMessage<M> ocpp::MessageQueue<M>::receive(std::string_view) [with M = ocpp::v201::MessageType; std::string_view = std::basic_string_view<char>] :: Received a CALLERROR for message with UID: 4e890cf8-fc09-4d79-b165-d2e7066938a8
2025-01-25 17:06:15.543687 [WARN] ocpp:OCPP201    void ocpp::MessageQueue<M>::handle_timeout_or_callerror(const std::optional<ocpp::EnhancedMessage<M> >&) [with M = ocpp::v201::MessageType] :: CALLERROR for: ClearedChargingLimit (4e890cf8-fc09-4d79-b165-d2e7066938a8)
2025-01-25 17:06:15.543735 [WARN] ocpp:OCPP201    void ocpp::MessageQueue<M>::handle_timeout_or_callerror(const std::optional<ocpp::EnhancedMessage<M> >&) [with M = ocpp::v201::MessageType] :: Message is not transaction related, dropping it

Then we receive a set_limit_amps which makes its way into the database. I don't see a timeout here!!

2025-01-25 17:06:15.582416 [INFO] api:API          :: SHANKARI: Received set_limit_amps callback with 14
2025-01-25 17:06:15.583041 [INFO] api:API          :: SHANKARI: After conversion, limit is 14
2025-01-25 17:06:15.632255 [INFO] api:API          :: SHANKARI: Finished calling EVSE manager with limit: {
    "schedule_import": [
        {
            "limits_to_leaves": {
                "ac_max_current_A": 14.0
            },
            "limits_to_root": {},
            "timestamp": "2025-01-25T17:06:15.583Z"
        }
    ]
}
2025-01-25 17:06:15.632613 [INFO] api:API          :: SHANKARI: ocpp size: 1
2025-01-25 17:06:15.635421 [INFO] ocpp:OCPP201     :: Received internal set_charging_limit: for 1 with purpose 14
2025-01-25 17:06:15.635753 [INFO] ocpp:OCPP201     :: Invoking validate_and_add_profile with 1 and source EMS
2025-01-25 17:06:15.637129 [INFO] ocpp:OCPP201     :: SHANKARI: Profile is valid, adding it
2025-01-25 17:06:15.637180 [INFO] ocpp:OCPP201     :: SHANKARI: Modifying database

However, it doesn't show up in the next composite schedule computed

2025-01-25 17:06:28.794397 [INFO] ocpp:OCPP201     :: About to publish composite charging schedules: 
2025-01-25 17:06:28.794669 [INFO] ocpp:OCPP201     :: {
    "chargingRateUnit": "A",
    "chargingSchedulePeriod": [
        {
            "limit": 16.0,
            "numberPhases": 3,
            "startPeriod": 0
        }
    ],
    "duration": 600,
    "evseId": 0,
    "scheduleStart": "2025-01-25T17:06:28.000Z"
}
2025-01-25 17:06:28.795144 [INFO] ocpp:OCPP201     :: {
    "chargingRateUnit": "A",
    "chargingSchedulePeriod": [
        {
            "limit": 16.0,
            "numberPhases": 3,
            "startPeriod": 0
        }
    ],
    "duration": 600,
    "evseId": 1,
    "scheduleStart": "2025-01-25T17:06:28.000Z"
}

@shankari
Copy link
Collaborator Author

Major: the new charging schedule is saved in the database, but is not incorporated into the composite schedule, so the reset behavior still happens

This is because the composite schedule is apparently invalid

2025-01-25 19:09:02.101052 [INFO] ocpp:OCPP201     :: SHANKARI: in compute composite schedule, iterating over 0 valid profiles
2025-01-25 19:09:02.104501 [INFO] ocpp:OCPP201     :: SHANKARI: in compute composite schedule, iterating over 0 valid profiles

The periodic call is from the OCPP module -> charge point's get_all_composite_schedules which calls get_composite_schedule_internal which calls the smart charging handler's get_valid_profiles(evse_id) which calls get_valid_profiles_for_evse. We need to add some logs there.

@shankari
Copy link
Collaborator Author

So this is a bit weird.

We do validate the profile before we save it, and it is valid then
2025-01-25 20:47:13.591883 [INFO] ocpp:OCPP201     :: Received internal set_charging_limit: for 1 with purpose 14
2025-01-25 20:47:13.593840 [INFO] ocpp:OCPP201     :: SHANKARI: Invoking validate_and_add_profile with 1 and source EMS
2025-01-25 20:47:13.594249 [INFO] ocpp:OCPP201     :: SHANKARI: invoked validate_profile for evse_id: 1 and profile {
    "chargingProfileKind": "Absolute",
    "chargingProfilePurpose": "ChargingStationExternalConstraints",
    "chargingSchedule": [
        {
            "chargingRateUnit": "A",
            "chargingSchedulePeriod": [
                {
                    "limit": 14.0,
                    "startPeriod": 0
                }
            ],
            "duration": 86400,
            "id": 0,
            "startSchedule": "2025-01-25T20:47:13.592Z"
        }
    ],
    "id": 398,
    "stackLevel": 10
}
2025-01-25 20:47:13.596031 [INFO] ocpp:OCPP201     :: SHANKARI: after validating file request source 
2025-01-25 20:47:13.596688 [INFO] ocpp:OCPP201     :: SHANKARI: after validating conflicting external constraints
2025-01-25 20:47:13.597815 [INFO] ocpp:OCPP201     :: SHANKARI: before validating by purpose
2025-01-25 20:47:13.597852 [INFO] ocpp:OCPP201     :: SHANKARI: should return valid here!
2025-01-25 20:47:13.597876 [INFO] ocpp:OCPP201     :: SHANKARI: result for 1 is Valid
2025-01-25 20:47:13.597974 [INFO] ocpp:OCPP201     :: SHANKARI: Modifying database
2025-01-25 20:47:13.601494 [INFO] ocpp:OCPP201     :: Returning from on_charging_limit_set with 1 and status Accepted
But it is apparently not valid when we use it to compute the composite schedule
2025-01-25 20:47:25.231064 [INFO] ocpp:OCPP201     :: SHANKARI: in get_valid_profiles_for_evse, iterating over 0 profiles
2025-01-25 20:47:25.231396 [INFO] ocpp:OCPP201     :: SHANKARI: in compute composite schedule, iterating over 0 valid profiles
2025-01-25 20:47:25.237402 [INFO] ocpp:OCPP201     :: SHANKARI: in get_valid_profiles_for_evse, iterating over 1 profiles
2025-01-25 20:47:25.237948 [INFO] ocpp:OCPP201     :: SHANKARI: considering profile with purpose ChargingStationExternalConstraints id 398 and stack level 10
2025-01-25 20:47:25.238700 [INFO] ocpp:OCPP201     :: SHANKARI: invoked validate_profile for evse_id: 1 and profile {
    "chargingProfileKind": "Absolute",
    "chargingProfilePurpose": "ChargingStationExternalConstraints",
    "chargingSchedule": [
        {
            "chargingRateUnit": "A",
            "chargingSchedulePeriod": [
                {
                    "limit": 14.0,
                    "numberPhases": 3,
                    "startPeriod": 0
                }
            ],
            "duration": 86400,
            "id": 0,
            "startSchedule": "2025-01-25T20:47:13.592Z"
        }
    ],
    "id": 398,
    "stackLevel": 10,
    "validFrom": "2025-01-25T20:47:13.596Z",
    "validTo": "2262-04-11T23:46:49.854Z"
}
2025-01-25 20:47:25.239287 [INFO] ocpp:OCPP201     :: SHANKARI: after validating file request source 
2025-01-25 20:47:25.239779 [INFO] ocpp:OCPP201     :: SHANKARI: in get_valid_profiles_for_evse, iterating over 0 profiles
2025-01-25 20:47:25.239867 [INFO] ocpp:OCPP201     :: SHANKARI: in compute composite schedule, iterating over 0 valid profiles
2025-01-25 20:47:25.240809 [INFO] ocpp:OCPP201     :: About to publish composite charging schedules: 
2025-01-25 20:47:25.240857 [INFO] ocpp:OCPP201     :: {
    "chargingRateUnit": "A",
    "chargingSchedulePeriod": [
        {
            "limit": 16.0,
            "numberPhases": 3,
            "startPeriod": 0
        }
    ],
    "duration": 600,
    "evseId": 0,
    "scheduleStart": "2025-01-25T20:47:25.000Z"
}
2025-01-25 20:47:25.241110 [INFO] ocpp:OCPP201     :: {
    "chargingRateUnit": "A",
    "chargingSchedulePeriod": [
        {
            "limit": 16.0,
            "numberPhases": 3,
            "startPeriod": 0
        }
    ],
    "duration": 600,
    "evseId": 1,
    "scheduleStart": "2025-01-25T20:47:25.000Z"
}

In the second case, we error out after EVLOG_info << "SHANKARI: after validating file request source "; but before EVLOG_info << "SHANKARI: after validating conflicting external constraints";

So it is probably that the verify_no_conflicting_external_constraints_id fails

@shankari
Copy link
Collaborator Author

shankari commented Jan 26, 2025

Aha! The code for `verify_no_conflicting_external_constraints_id` fails if there is an external constraints profile with the same ID as the one being validated.
ProfileValidationResultEnum
SmartChargingHandler::verify_no_conflicting_external_constraints_id(const ChargingProfile& profile) const {
    auto result = ProfileValidationResultEnum::Valid;
    auto conflicts_stmt =
        this->database_handler.new_statement("SELECT PROFILE FROM CHARGING_PROFILES WHERE ID = @profile_id AND "
                                             "CHARGING_PROFILE_PURPOSE = 'ChargingStationExternalConstraints'");

    conflicts_stmt->bind_int("@profile_id", profile.id);
    if (conflicts_stmt->step() == SQLITE_ROW) {
        result = ProfileValidationResultEnum::ExistingChargingStationExternalConstraints;
    }

    return result;
So it is guaranteed to fail while validating an external constraints profile. So, as written, `validate_profile` will fail for all external constraint profiles, which contradicts this comment at the end
    result = verify_no_conflicting_external_constraints_id(profile);
    if (result != ProfileValidationResultEnum::Valid) {
        return result;
    }
...
    switch (profile.chargingProfilePurpose) {
    ...
    case ChargingProfilePurposeEnum::ChargingStationExternalConstraints:
        // TODO: How do we check this? We shouldn't set it in
        // `SetChargingProfileRequest`, but that doesn't mean they're always
        // invalid. K01.FR.05 is the only thing that seems relevant.
        result = ProfileValidationResultEnum::Valid;
        break;
    }

I am not sure why this was added, working around it by only invoking verify_no_conflicting_external_constraints for non external constraint profiles.

UPDATE: That worked!
2025-01-26 03:55:09.090805 [INFO] ocpp:OCPP201     :: SHANKARI: in get_valid_profiles_for_evse, iterating over 1 profiles
2025-01-26 03:55:09.090918 [INFO] ocpp:OCPP201     :: SHANKARI: considering profile with purpose ChargingStationExternalConstraints id 398 and stack level 10
2025-01-26 03:55:09.090950 [INFO] ocpp:OCPP201     :: SHANKARI: invoked validate_profile for evse_id: 1 and profile {
    "chargingProfileKind": "Absolute",
    "chargingProfilePurpose": "ChargingStationExternalConstraints",
    "chargingSchedule": [
        {
            "chargingRateUnit": "A",
            "chargingSchedulePeriod": [
                {
                    "limit": 15.0,
                    "numberPhases": 3,
                    "startPeriod": 0
                }
            ],
            "duration": 86400,
            "id": 0,
            "startSchedule": "2025-01-26T03:53:43.789Z"
        }
    ],
    "id": 398,
    "stackLevel": 10,
    "validFrom": "2025-01-26T03:53:43.790Z",
    "validTo": "2262-04-11T23:46:49.854Z"
}
2025-01-26 03:55:09.091571 [INFO] ocpp:OCPP201     :: SHANKARI: after validating file request source 
2025-01-26 03:55:09.091744 [INFO] ocpp:OCPP201     :: SHANKARI: after validating conflicting external constraints
2025-01-26 03:55:09.093027 [INFO] ocpp:OCPP201     :: SHANKARI: before validating by purpose
2025-01-26 03:55:09.093118 [INFO] ocpp:OCPP201     :: SHANKARI: should return valid here!
2025-01-26 03:55:09.093150 [INFO] ocpp:OCPP201     :: SHANKARI: profile with purpose ChargingStationExternalConstraints and id valid, adding to list 
2025-01-26 03:55:09.093258 [INFO] ocpp:OCPP201     :: SHANKARI: in get_valid_profiles_for_evse, iterating over 0 profiles
2025-01-26 03:55:09.093299 [INFO] ocpp:OCPP201     :: SHANKARI: in compute composite schedule, iterating over 1 valid profiles
2025-01-26 03:55:09.093325 [INFO] ocpp:OCPP201     :: SHANKARI: considering valid profile with purpose ChargingStationExternalConstraints id 398 and stack level 10
2025-01-26 03:55:09.094579 [INFO] ocpp:OCPP201     :: About to publish composite charging schedules: 
2025-01-26 03:55:09.094673 [INFO] ocpp:OCPP201     :: {
    "chargingRateUnit": "A",
    "chargingSchedulePeriod": [
        {
            "limit": 16.0,
            "numberPhases": 3,
            "startPeriod": 0
        }
    ],
    "duration": 600,
    "evseId": 0,
    "scheduleStart": "2025-01-26T03:55:09.000Z"
}
2025-01-26 03:55:09.094998 [INFO] ocpp:OCPP201     :: {
    "chargingRateUnit": "A",
    "chargingSchedulePeriod": [
        {
            "limit": 15.0,
            "numberPhases": 3,
            "startPeriod": 0
        }
    ],
    "duration": 600,
    "evseId": 1,
    "scheduleStart": "2025-01-26T03:55:09.000Z"
}

@shankari
Copy link
Collaborator Author

shankari commented Jan 26, 2025

Next, we want to verify that this is actually sent to the CSMS. I don't see a timeout for the notify_limit message (unlike the clear_limit call), but I also am not sure if it makes it to the CSMS properly.

Let's verify this by:

  • checking the maeve MQTT
  • adding additional logs to the MaEVe gateway not doing this until we actually implement at least a pass-through

@shankari
Copy link
Collaborator Author

MaEVe MQTT shows that it receives this message

{"type":2,"action":"ClearedChargingLimit","id":"1af4a081-ab1d-4ac5-bba9-17fd94e73731","request":{"chargingLimitSource":"Other","evseId":1}}

but then it fails with a NotImplemented

{"type":4,"action":"ClearedChargingLimit","id":"1af4a081-ab1d-4ac5-bba9-17fd94e73731","error_code":"NotImplemented","error_description":"ClearedChargingLimit not implemented"}

Let's try to add a simple implementation so we can make sure that we are at least fully implemented end-to-end on our side including receiving the response properly.

@shankari
Copy link
Collaborator Author

wrt:

Then we receive a set_limit_amps which makes its way into the database. I don't see a timeout here!!

from the details in #101 (comment)
it is because we had commented out the actual notify call to focus on the composite schedule.

After reinstating, we get the same `NotImplemented` for `set_current_limit` as well
2025-01-26 04:21:50.974500 [INFO] ocpp:OCPP201     :: SHANKARI: after validating file request source 
2025-01-26 04:21:50.974527 [INFO] ocpp:OCPP201     :: SHANKARI: after validating conflicting external constraints
2025-01-26 04:21:50.975503 [INFO] ocpp:OCPP201     :: SHANKARI: before validating by purpose
2025-01-26 04:21:50.975536 [INFO] ocpp:OCPP201     :: SHANKARI: should return valid here!
2025-01-26 04:21:50.975549 [INFO] ocpp:OCPP201     :: SHANKARI: result for 1 is Valid
2025-01-26 04:21:50.975567 [INFO] ocpp:OCPP201     :: SHANKARI: Modifying database
2025-01-26 04:21:50.979373 [INFO] ocpp:OCPP201     :: Returning from on_charging_limit_set with 1 and status Accepted
2025-01-26 04:21:50.982298 [ERRO] ocpp:OCPP201    ocpp::EnhancedMessage<M> ocpp::MessageQueue<M>::receive(std::string_view) [with M = ocpp::v201::MessageType; std::string_view = std::basic_string_view<char>] :: Received a CALLERROR for message with UID: ec6e5200-ca71-4684-a28c-138ffba24205
2025-01-26 04:21:50.982477 [WARN] ocpp:OCPP201    void ocpp::MessageQueue<M>::handle_timeout_or_callerror(const std::optional<ocpp::EnhancedMessage<M> >&) [with M = ocpp::v201::MessageType] :: CALLERROR for: NotifyChargingLimit (ec6e5200-ca71-4684-a28c-138ffba24205)
2025-01-26 04:21:50.982523 [WARN] ocpp:OCPP201    void ocpp::MessageQueue<M>::handle_timeout_or_callerror(const std::optional<ocpp::EnhancedMessage<M> >&) [with M = ocpp::v201::MessageType] :: Message is not transaction related, dropping it
{"type":2,"action":"NotifyChargingLimit","id":"ec6e5200-ca71-4684-a28c-138ffba24205","request":{"chargingLimit":{"chargingLimitSource":"EMS"},"chargingSchedule":[{"chargingRateUnit":"A","chargingSchedulePeriod":[{"limit":15.0,"startPeriod":0}],"duration":86400,"id":0,"startSchedule":"2025-01-26T04:21:50.973Z"}],"evseId":1}}
{"type":4,"action":"NotifyChargingLimit","id":"ec6e5200-ca71-4684-a28c-138ffba24205","error_code":"NotImplemented","error_description":"NotifyChargingLimit not implemented"}

@shankari
Copy link
Collaborator Author

I have now plumbed through the code needed to implement notify_charging_limit in MaEVe. It was a bit annoying because I had to create the appropriate classes manually (why aren't they autogenerated!) but it was just copy-pasting - it wasn't hard!

I do want to document the process for testing out changes to MaEVe, since within NREL, we have issues with SSL certs while trying to download go modules.

The steps are:

  • Create a "build container" by using the manager.Dockerfile.dev file
  • Add the root certs properly
  • Compile until everything passes
  • Copy out the /app binary
  • Copy the /app binary into the currently running MaEVe manager container as /app
  • Ensure that manager/config/config.toml is in the directory that is mounted by the MaEVe manager container as /config
  • Restart the manager

I'm going to check this code in now, though, so we will build new images for the demo/testing.

shankari pushed a commit to US-JOET/maeve-csms that referenced this issue Jan 27, 2025
So that we can finish testing the EVerest changes in
EVerest/everest-demo#101
end-to-end

I have not yet plumbed through the changes to `clear_charging_limit`
but they should be fairly simple, and I can leave them as an exercise for
@Abby-Wheelis

Outline of changes:
- add `notify_charging_limit` to the `CallMap`, and have it redirect to the
  `notify_charging_limit_handler`
- add a handler for the `notify_charging_limit`
- add classes for the request and response that we can use in the handler

Testing done:
Without this change:
EVerest/everest-demo#101 (comment)

With this change:

```
2025-01-27 02:36:16.234166 [INFO] ocpp:OCPP201     :: Received internal set_charging_limit: for 1 with purpose 15
2025-01-27 02:36:16.234471 [INFO] ocpp:OCPP201     :: SHANKARI: Invoking validate_and_add_profile with 1 and source EMS
2025-01-27 02:36:16.234598 [INFO] evse_manager_1:  :: set_max_current (14, 2025-01-27 02:36:26.094000000) called
2025-01-27 02:36:16.234517 [INFO] ocpp:OCPP201     :: SHANKARI: invoked validate_profile for evse_id: 1 and profile {
    "chargingProfileKind": "Absolute",
    "chargingProfilePurpose": "ChargingStationExternalConstraints",
    "chargingSchedule": [
        {
            "chargingRateUnit": "A",
            "chargingSchedulePeriod": [
                {
                    "limit": 15.0,
                    "startPeriod": 0
                }
            ],
            "duration": 86400,
            "id": 0,
            "startSchedule": "2025-01-27T02:36:16.234Z"
        }
    ],
    "id": 398,
    "stackLevel": 10
}
2025-01-27 02:36:16.234902 [INFO] ocpp:OCPP201     :: SHANKARI: after validating file request source
2025-01-27 02:36:16.234926 [INFO] ocpp:OCPP201     :: SHANKARI: after validating conflicting external constraints
2025-01-27 02:36:16.235939 [INFO] ocpp:OCPP201     :: SHANKARI: before validating by purpose
2025-01-27 02:36:16.235974 [INFO] ocpp:OCPP201     :: SHANKARI: should return valid here!
2025-01-27 02:36:16.235986 [INFO] ocpp:OCPP201     :: SHANKARI: result for 1 is Valid
2025-01-27 02:36:16.236010 [INFO] ocpp:OCPP201     :: SHANKARI: Modifying database
2025-01-27 02:36:16.243501 [INFO] ocpp:OCPP201     :: Returning from on_charging_limit_set with 1 and status Accepted
```

```
{"type":2,"action":"NotifyChargingLimit","id":"feda7835-c7d5-4717-8df6-79cdc5b17e9c","request":{"chargingLimit":{"chargingLimitSource":"EMS"},"chargingSchedule":[{"chargingRateUnit":"A","chargingSchedulePeriod":[{"limit":15.0,"startPeriod":0}],"duration":86400,"id":0,"startSchedule":"2025-01-27T02:36:16.234Z"}],"evseId":1}}

{"type":3,"action":"NotifyChargingLimit","id":"feda7835-c7d5-4717-8df6-79cdc5b17e9c","response":{}}
```
@shankari
Copy link
Collaborator Author

shankari commented Jan 27, 2025

New tasks:

  • EVerest: make sure that clear limit actually clears the limit
  • MaEVe: plumb through clear_charging_limit, similar to notify_charging_limit
  • Both: publish images and generate patches so we can submit PRs down the road
  • the pending one on why every drag of the slider calls clear before set

@shankari
Copy link
Collaborator Author

shankari commented Jan 27, 2025

Hm, further, the "Clear Limit" button does not actually call clear_limit_amps.
I clicked it, and there is no instance of clear_limit_amps in the logs

And although the slider value is changed, the max limit is not actually modified
Image

EDIT: UI seems fine, and checking with the MQTT explorer, I see that when we change the slider, set_limit_amps is the only message that is sent. Argh! I had subscribed to the cmd_set_limit for the clear callback too!!
Fixing now...

@shankari
Copy link
Collaborator Author

Works! Although because I implement it by clearing all profiles, the max current only changes when the composite schedules are next computed. Would be good to change that, but it seems non-obvious from an architectural perspective, and probably needs coordination with the community.

Concretely, we want to reset to the default hw capability, but:

  • the API does not store the hw capability, that comes from the EVSE manager
  • the API does subscribe to changes to the hw capability but then just re-publishes them
  • the hw capability is not available in the clear callback

I think I am going to work around this by having the clear command send the max limit for now
Note that the max limit should also really come from the hw capabilities instead of being hardcoded

@shankari
Copy link
Collaborator Author

shankari commented Jan 27, 2025

For "publish images and generate patches so we can submit PRs down the road", I have committed all the relevant containers, and will switch docker-compose to use them for now.

I am also uploading the patches that I am aware of before I delete the containers and test after re-creating them.

These are the same patches as #101 (comment)

shankari pushed a commit to US-JOET/everest-demo that referenced this issue Jan 27, 2025
…hanges

Changes:
- EVerest manager uses the changes to receive a limit through the API, pass it
  through to the OCPP module, store it in the database and use it to influence
  the power limits
- Node-red sends the correct MQTT messages
- MaEVe manager has an NOP implementation of notify limit

The actual changes/patches are listed here and will need to be merged in one
step at a time
EVerest#101 (comment)

Testing done:
```
$ bash demo-iso15118-2-ocpp-201.sh -1 -r $(pwd) -m
$ docker exec -it everest-ac-demo-manager-1 /bin/bas
(container) $ sh /ext/build/run-scripts/run-sil-ocpp201-pnc.sh
```

Then
- moved the slider to various points, the max current changed
- plugged in car, handshake was successful
- moved the slider while the car was plugged in, max current changed and power drawn changed
- cleared limit, max current went to 16 and max power went to 11kW

Signed-off-by: Shankari <[email protected]>
shankari pushed a commit to US-JOET/everest-demo that referenced this issue Jan 27, 2025
…hanges

Changes:
- EVerest manager uses the changes to receive a limit through the API, pass it
  through to the OCPP module, store it in the database and use it to influence
  the power limits
- Node-red sends the correct MQTT messages
- MaEVe manager has an NOP implementation of notify limit

The actual changes/patches are listed here and will need to be merged in one
step at a time
EVerest#101 (comment)

Testing done:
```
$ bash demo-iso15118-2-ocpp-201.sh -1 -r $(pwd) -m
$ docker exec -it everest-ac-demo-manager-1 /bin/bas
(container) $ sh /ext/build/run-scripts/run-sil-ocpp201-pnc.sh
```

Then
- moved the slider to various points, the max current changed
- plugged in car, handshake was successful
- moved the slider while the car was plugged in, max current changed and power drawn changed
- cleared limit, max current went to 16 and max power went to 11kW

Signed-off-by: Shankari <[email protected]>
@shankari
Copy link
Collaborator Author

shankari commented Jan 27, 2025

@Abby-Wheelis I think this is at a reasonable point for you to take over.

IMHO, next step is:

If time permits:

At that point, I think we can close this issue.

We need to figure out what is going on with the rebuilt images, submit patches as a PR etc but those can be tracked in separate issues.

@Abby-Wheelis
Copy link
Contributor

Abby-Wheelis commented Jan 27, 2025

Testing done:
curl https://raw.githubusercontent.com/everest/everest-demo/main/demo-iso15118-2-ocpp-201.sh | bash -s -- -1 -m

error: unable to prepare context: path "/var/folders/40/kvb685dn3wd4zz7twz3rbjxw0000gr/T/tmp.3O2Cpf4qt3/everest-demo/maeve/manager" not found Failed to start maeve

Tried again, did not fail, but did take FOREVER and eventually stall out

Kept trying, got on a faster network have the manager running!

  • moved the slider to various points, seeing a change in the limit, but it eventually gets rounded to the nearest whole number? EDIT: It gets cut off 9.2 -> 9 and 9.9 -> 9
  • tried to plug in car - failing to recognize or go into "waiting for auth"

Could be because I stopped / restarted the pull so many times? Maybe I need to start over with a clean slate (on the faster network)

@shankari
Copy link
Collaborator Author

shankari commented Jan 27, 2025

@Abby-Wheelis actual next steps:

  • commit the change to the config to enable single phase (to manager/config-sil-ocpp201-pnc.yaml) so that everything works out of the box
  • figure out/document how to change the CSMS URL easily so we can easily test against other CSMSes using EVerest
  • one more thing if we have time is to try to understand the cert setup so that we can try SP2 or SP3 but that is a nice to have and I don't think we will have time for it
  • one more things if we have time is to try to run the python automated tests in the container as we did in docker-compose.automated-tests.yml - this should be the equivalent of "clicking the button" in the UI; again to tease to the CSMS vendors that you don't even have to click the button but can automate testing.

Right now, in the demo-iso15118-2-ocpp-201.sh script, we set the CSMS URL depending on whether we are integrating with MAEVE or CITRINE

  source ../${DEMO_CSMS}/apply-runtime-patches.sh

where apply-runtime-patches.sh sets the URL

#!/usr/bin/env bash

export CSMS_SP1_URL="ws://host.docker.internal/ws/cp001"
export CSMS_SP2_URL="wss://host.docker.internal/ws/cp001"
export CSMS_SP3_URL="wss://host.docker.internal/ws/cp001"

and then we replace the localhost with the CSMS URL

echo "Configured to SecurityProfile: 1, disabling TLS and configuring server to ${CSMS_SP1_URL}"
docker exec everest-ac-demo-manager-1 /bin/bash -c "sed -i 's#ws://localhost:9000#${CSMS_SP1_URL}#' /ext/dist/share/everest/modules/OCPP201/component_config/standardized/InternalCtrlr.json"

So I think what we need is a demo_...._ext_csms.sh
This will contain everything after pushd everest-demo || exit 1 (will omit all the code to configure and startup the containers for the CSMS)
It will also assume that CSMS_SP1_URL etc defined in the shell where the demo is run; so check for existence and fail if it doesn't exist etc.

It's really just a simpler script and some instructions.

We may want to start MaEVe or Citrine or MobilityHouse separately and then run the new script to confirm that everything works (https://github.com/EVerest/libocpp/?tab=readme-ov-file#csms-compatibility-ocpp-201)

@Abby-Wheelis
Copy link
Contributor

PR #107 is open to handle the first task listed above.

@shankari
Copy link
Collaborator Author

@Abby-Wheelis even after switching to single phase, the max power is 10.5 kW, which is what I remember seeing with the three phase as well. Is there anything else we need to change/fix to make the math work?

@Abby-Wheelis
Copy link
Contributor

even after switching to single phase, the max power is 10.5 kW, which is what I remember seeing with the three phase as well. Is there anything else we need to change/fix to make the math work?

I'll look into it! That is the value I remember as well

@Abby-Wheelis
Copy link
Contributor

Abby-Wheelis commented Jan 28, 2025

Updates:

  • I have a basic script that seems to be working with the maeve csms (I just manually added the url to the environment), and throws an error without
  • I am now getting complaints when I run the manager that three_phase is not a config option

Heading home, but will pick back up when I get there to:

  • resolve the config issue
  • investigate the math
  • submit script + documentation as a PR
  • test my script with another CSMS

@Abby-Wheelis
Copy link
Contributor

resolve the config issue

I made a mistake in the PR I submitted earlier, #108 is a revision which fixes my error.

@Abby-Wheelis
Copy link
Contributor

Abby-Wheelis commented Jan 28, 2025

investigate the math

2025-01-28 01:59:55.552921 [DEBG] iso15118_car    pybind11_init_everestpy(pybind11::module_&)::<lambda(const std::string&)> :: Decoded message (ns=Namespace.ISO_V2_MSG_DEF): {"V2G_Message":{"Header":{"SessionID":"1A17FD77FD3FFFF9"},"Body":{"ChargeParameterDiscoveryRes":{"ResponseCode":"OK","EVSEProcessing":"Finished","SAScheduleList":{"SAScheduleTuple":[{"SAScheduleTupleID":1,"PMaxSchedule":{"PMaxScheduleEntry":[{"RelativeTimeInterval":{"start":0,"duration":86400},"PMax":{"Multiplier":0,"Unit":"W","Value":3680}}]}}]},"AC_EVSEChargeParameter":{"AC_EVSEStatus":{"NotificationMaxDelay":0,"EVSENotification":"None","RCD":false},"EVSENominalVoltage":{"Multiplier":-1,"Unit":"V","Value":2300},"EVSEMaxCurrent":{"Multiplier":-1,"Unit":"A","Value":160}}}}}}

This looks right - 230V x 16A = 3.68kW, that's just not what we see on the meter ...

Update: so the value in the meter comes from everest_external/nodered/+/powermeter/totalKw, which is published to here in the JsYetiSimulator, I'm not really sure of the math behind that yet.

Also published to here in the EvseManager, with a value I also don't fully understand.

It looks like both come from the sum of (L1, L2, L3) power / 1000 ... I'm just not sure why

Putting a pin in this to get the external csms script out and tested, will continue in #105 when I have the time.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants