- Jelina overview
- EMS Builder
- Configuration examples
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.
1. Business background
Section titled “1. Business background”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.
2. Power model
Section titled “2. Power model”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 againlocal avail_kw = (rated_kw - grid_kw) * 0.9local 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 requiredfor _, 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 endendProvided 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 timestampDeployment prerequisite: manually create an aggregation point to act as the script’s memory:
{ "dataKey": "last_ctrl_ms", "dataName": "充电控制上次执行时间戳", "expression": "return 0", "collectionCycle": -1}5. Per-charge-point decision table
Section titled “5. Per-charge-point decision table”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.001local 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 endend
table.insert(resultList, buildCommonWriteMap("Aggregation", "last_ctrl_ms", OffsetDateTime.now():unixMilli(), nil, nil, false))return resultList6. 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.
7. Handling future changes
Section titled “7. Handling future changes”| 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.
8. Combining other capabilities
Section titled “8. Combining other capabilities”- 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.
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.