Skip to content
JelinaJelina DocumentationTechnical documentation
Back to website

Example: Charging Cluster Load Balancing

This example builds a load-balancing strategy supporting variable device counts without script changes, using only generic Edge-EMS Builder capabilities: dynamic data scanning, expression calculations, trigger schemes, and rate limiting. Device brands and naming prefixes are illustrative; replace them with actual project names.

A distribution panel supplies several charge points, using OCPP or proprietary protocols, plus essential loads such as lighting and air conditioning. Its grid-side incoming capacity is fixed. Simultaneous full-power charging can exceed that rating, causing trips or excess-capacity penalties.

The objective is to give essential loads priority while sharing remaining capacity among all online charge points, dynamically adjusting each charge point’s current. Rebalancing should finish within seconds when any charge point connects or disconnects, and adding a charge point should require no strategy-script changes.

The system has two key measurement considerations:

  • Grid_total_power: real-time total grid input power, including all panel loads: essential loads, charge points, and others.
  • Essential loads are already included in this measurement, so controlling total charging consumption alone keeps overall panel input below the rated power and inherently prioritizes essential loads.
-- Available headroom (kW) = (rated power - measured total grid power) × safety factor
-- A 10% safety margin prevents overshoot; essential loads are already measured and need not be subtracted again
local avail_kw = (rated_kw - grid_kw) * 0.9
local per_A = (avail_kw / n) * 1000 / voltage_V -- Allocate equally as current (A)

3. Dynamic device discovery: add charge points without changing scripts

Section titled “3. Dynamic device discovery: add charge points without changing scripts”

The strategy does not assume a charge-point count. Each execution automatically discovers all online charge points by scanning in batches using prefixes and suffixes:

local online = {}
-- Scan each brand prefix; add or remove prefixes as required
for _, brandPrefix in ipairs({"VENDORA", "VENDORB"}) do
local statusMap = getLatestDataValuesByPrefixAndSuffix("Collection", brandPrefix, "_status")
if statusMap then
-- The result is a Table keyed by dataKey; iterate with pairs() and compare against the sentinel "__Null__"
for dataKey, dto in pairs(statusMap) do
if dto.DataValue ~= "__Null__" then
table.insert(online, dto)
end
end
end
end

Provided new acquisition points follow the naming convention below, the next trigger automatically includes them in equal allocation, without script changes or service restarts. This combines the data model and Lua engine, rather than relying on a dedicated feature.

4. Trigger model: data refresh plus in-script rate limiting

Section titled “4. Trigger model: data refresh plus in-script rate limiting”

Fixed-interval polling alone can cause redundant writes when data has not changed. Putting rate limits into trigger conditions makes configuration more complex. This example uses a simpler model:

Data-refresh triggering plus timestamp-based rate limiting inside the script: the trigger detects data refreshes; the script controls command frequency.

Data refresh → Condition satisfied → Trigger script → Read aggregation point last_ctrl_ms (last execution timestamp)
→ Elapsed < 5000ms: return without sending commands
→ Elapsed ≥ 5000ms: control → write commands → update timestamp

Deployment prerequisite: manually create an aggregation point to act as the script’s memory:

{
"dataKey": "last_ctrl_ms",
"dataName": "充电控制上次执行时间戳",
"expression": "return 0",
"collectionCycle": -1
}

After each trigger, the script reads grid parameters → checks rate limits → scans online charge points → calculates available current → decides for each charge point → updates the timestamp → returns the command list. Per-charge-point rules:

Condition Charge-point state Action
Overload OR allocated current < minimum current Charging Stop charging (stop_transaction)
Overload OR allocated current < minimum current Other Skip
No overload AND allocated current ≥ minimum current Charging Send a new current limit (set_charging_profile)
No overload AND allocated current ≥ minimum current Not charging Start charging (start_transaction; valid authorization Tag required)

Core script excerpt (trigger action returning an array of write descriptors):

local OffsetDateTime = require("OffsetDateTime")
local ratedKw = tonumber(${Grid_current} or 0) * tonumber(${Grid_voltage} or 0) * 0.001
local gridKw = tonumber(${Grid_total_power} or 0)
local overload = gridKw >= ratedKw
local voltage = tonumber(${Grid_voltage} or 1)
local minChargePower = tonumber(${min_charge_power} or 4200)
local resultList = {}
if #online > 0 then
local availKw = (ratedKw - gridKw) * 0.9
local perA = math.floor((availKw / #online) * 1000 / voltage)
local minA = math.floor(minChargePower / voltage)
for _, pile in ipairs(online) do
local prefix = pile.DataKey:gsub("_status$", "") -- Recover the charge-point prefix from dataKey, for example VENDORA01
if overload or perA < minA then
if pile.DataValue == "Charging" then
table.insert(resultList, buildCommonWriteMap("Collection", prefix .. "_type", "stop_transaction", nil, nil, false))
end
else
if pile.DataValue == "Charging" then
table.insert(resultList, buildCommonWriteMap("Collection", prefix .. "_periods", '[{"start":0,"limit":' .. perA .. '}]', nil, nil, false))
elseif ${id_tag} ~= "__Null__" then
table.insert(resultList, buildCommonWriteMap("Collection", prefix .. "_type", "start_transaction", nil, nil, false))
end
end
end
end
table.insert(resultList, buildCommonWriteMap("Aggregation", "last_ctrl_ms", OffsetDateTime.now():unixMilli(), nil, nil, false))
return resultList

6. Naming convention: the basis for batch detection

Section titled “6. Naming convention: the basis for batch detection”

The script scans acquisition points in the format {brand-prefix}{two-digit-number}_{property}. Follow it strictly:

Part Rule Example
Brand prefix Uppercase, without underscores VENDORA, VENDORB; replace with your actual device-brand identifiers
Number Two digits, zero-padded 01, 02…10
Property See below status, type

Required acquisition points for each charge point (selected examples):

Property Complete dataKey example Read/write Description
_status VENDORA01_status Read Charge-point status and discovery basis; the charge point is not discovered if missing
_transaction_id VENDORA01_transaction_id Read Current transaction ID
_type VENDORA01_type Write Command type: start/stop/set_charging_profile
_periods VENDORA01_periods Write Limit-period JSON, such as [{"start":0,"limit":16}]

To add the sixth charge point of that brand, connect its acquisition points using VENDORA06_status, VENDORA06_type, and so on. It is automatically included in equal allocation.

Scenario Script changes needed? Action
Add a charge point of the same brand No Add acquisition points following the naming convention
Adjust the rated-capacity limit No Change aggregation point Grid_current
Adjust minimum charging power No Change aggregation point min_charge_power
Charge point temporarily goes offline No _status becomes empty after disconnection, automatically excluding it from the online list
Add a new charge-point brand Yes Add a prefix to the scan list, for example {"VENDORA", "VENDORB", "VENDORC"}
Make the safety-margin factor configurable Yes, once Add an aggregation point for the factor and change the script to read it

This table illustrates the design philosophy: most business changes involve configuration; only a few structural changes require scripts, and those changes remain predictable and controlled.

  • If charge points span several subdistribution panels, use Device coordination to share each panel’s measured grid power with a master device for centralized allocation.
  • If brands and models continually expand across projects with similar cluster structures, use Unit Dynamic Expansion to turn brand prefixes, naming conventions, and related points into templates, shortening new-project setup.
  • To receive current-limit thresholds from the cloud and view overall balancing status, use Cloud-edge coordination to periodically report grid headroom and command records for each charge point.
Acquisition, calculation, allocation, and execution flow for charging load balancing

This diagram follows the configuration logic on this page and illustrates data and control flow. Refer to the configuration descriptions and scripts for specific fields.