Aller au contenu
JelinaJelina DocumentationDocumentation technique
Retour au site officiel

Exemple : équilibrage de charge d’un groupe de bornes

Cet exemple construit une stratégie d’équilibrage acceptant un nombre variable d’équipements sans modifier les scripts, uniquement avec les capacités génériques d’Edge-EMS Builder : découverte dynamique de données, calcul d’expressions, déclenchement et limitation de fréquence. Marques et préfixes sont illustratifs ; remplacez-les par ceux du projet.

Un tableau électrique alimente plusieurs bornes de recharge, utilisant OCPP ou un protocole propriétaire, ainsi que des charges essentielles comme éclairage et climatisation. La capacité de l’arrivée réseau est fixe. Si toutes les bornes chargent à pleine puissance, la consommation totale peut dépasser la capacité nominale et entraîner un déclenchement ou des pénalités de dépassement.

Objectif : donner la priorité aux charges essentielles et partager la capacité restante entre toutes les bornes en ligne, en ajustant leur courant de charge. Une connexion ou déconnexion doit être rééquilibrée en quelques secondes, et l’ajout d’une borne ne doit pas nécessiter de modifier les scripts de stratégie.

Le système comporte deux points de mesure essentiels :

  • Grid_total_power : puissance totale instantanée entrant depuis le réseau, incluant toutes les charges du tableau : charges essentielles, bornes et autres.
  • Les charges essentielles sont déjà incluses dans cette mesure. Contrôler la consommation totale des bornes suffit donc à maintenir l’entrée totale sous la puissance nominale, en donnant naturellement priorité aux charges essentielles.
-- Marge attribuable (kW) = (puissance nominale - puissance réseau totale mesurée) × coefficient de sécurité
-- La marge de 10% évite les dépassements ; la mesure inclut déjà les charges essentielles, sans déduction supplémentaire
local avail_kw = (rated_kw - grid_kw) * 0.9
local per_A = (avail_kw / n) * 1000 / voltage_V -- Répartir également en courant (A)

3. Découverte dynamique : ajouter des bornes sans modifier les scripts

Section intitulée « 3. Découverte dynamique : ajouter des bornes sans modifier les scripts »

La stratégie ne fixe pas le nombre de bornes. À chaque exécution, une recherche par préfixe et suffixe découvre automatiquement toutes les bornes en ligne :

local online = {}
-- Parcourir les préfixes de marque, à compléter ou réduire selon le besoin
for _, brandPrefix in ipairs({"VENDORA", "VENDORB"}) do
local statusMap = getLatestDataValuesByPrefixAndSuffix("Collection", brandPrefix, "_status")
if statusMap then
-- Le retour est une table indexée par dataKey, à parcourir avec pairs() ; comparer à la sentinelle "__Null__" pour détecter l’absence
for dataKey, dto in pairs(statusMap) do
if dto.DataValue ~= "__Null__" then
table.insert(online, dto)
end
end
end
end

Si les points d’une nouvelle borne respectent les conventions ci-dessous, elle participe automatiquement au prochain calcul de répartition, sans modification de script ni redémarrage de service. Cela résulte de la combinaison du modèle de données et du moteur Lua, sans fonction dédiée.

4. Déclenchement : actualisation des données et limitation dans le script

Section intitulée « 4. Déclenchement : actualisation des données et limitation dans le script »

Une interrogation périodique fixe peut produire des écritures même sans changement de données. Ajouter la limitation de fréquence au déclencheur complexifie sa condition. Le modèle retenu est plus simple :

Déclenchement sur actualisation + limitation par horodatage dans le script : le déclencheur détecte l’actualisation ; le script maîtrise la fréquence réelle des commandes.

Actualisation des données → Condition satisfaite → Déclencher le script → Lire le point agrégé last_ctrl_ms(Horodatage de dernière exécution)
→ Temps écoulé < 5000ms:Retour immédiat, sans commande
→ Temps écoulé ≥ 5000ms:Exécuter le contrôle → Instructions d’écriture → Actualiser l’horodatage

Préalable au déploiement : créez manuellement un point agrégé servant de mémoire au script :

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

À chaque déclenchement, le script lit les paramètres réseau, vérifie la fréquence, découvre les bornes en ligne, calcule le courant disponible, décide pour chaque borne, actualise l’horodatage et retourne la liste d’instructions. Règles :

Condition État de la borne Action
Surcharge ou courant réparti < courant minimal En charge Arrêter la charge (stop_transaction)
Surcharge ou courant réparti < courant minimal Autre Ignorer
Pas de surcharge et courant réparti ≥ courant minimal En charge Envoyer la nouvelle limite de courant (set_charging_profile)
Pas de surcharge et courant réparti ≥ courant minimal Hors charge Démarrer la charge (start_transaction, avec Tag d’autorisation valide)

Extrait principal du script d’action, renvoyant un tableau de descriptions d’écriture :

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$", "") -- Retrouver le préfixe de la borne depuis dataKey, par exemple 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. Conventions de nommage pour la découverte par lot

Section intitulée « 6. Conventions de nommage pour la découverte par lot »

Le script recherche les points selon le format {préfixe-marque}{numéro-sur-deux-chiffres}_{propriété}, à respecter strictement :

Élément Règle Exemple
Préfixe de marque Majuscules, sans trait de soulignement VENDORA, VENDORB, à remplacer par les identifiants de marques réellement utilisés
Numéro Deux chiffres, complétés par zéro 01, 02…10
Propriété Voir ci-dessous status, type

Exemples de points requis pour chaque borne :

Propriété Exemple de dataKey complet Lecture/écriture Description
_status VENDORA01_status Lecture État de la borne, utilisé pour la découverte ; sans ce point, la borne n’est pas détectée
_transaction_id VENDORA01_transaction_id Lecture ID de transaction courante
_type VENDORA01_type Écriture Type de commande : start/stop/set_charging_profile
_periods VENDORA01_periods Écriture JSON des périodes de limitation, par exemple [{"start":0,"limit":16}]

Pour ajouter une sixième borne de cette marque, raccordez ses points selon ces noms, VENDORA06_status, VENDORA06_type, etc. Elle sera automatiquement incluse dans la répartition.

Scénario Modifier le script ? Opération
Ajouter une borne de même marque Non Ajouter ses points selon les conventions
Ajuster la limite de capacité nominale Non Modifier la valeur du point agrégé Grid_current
Ajuster la puissance minimale de recharge Non Modifier le point agrégé min_charge_power
Borne temporairement hors ligne Non Après déconnexion, _status devient vide et la borne est exclue de la liste en ligne
Ajouter une marque de bornes Oui Ajouter un préfixe à la liste, par exemple {"VENDORA", "VENDORB", "VENDORC"}
Rendre le coefficient de marge configurable Oui, une seule fois Créer un point agrégé pour le coefficient et modifier le script pour le lire

Cela illustre les principes de conception : la plupart des évolutions métier modifient seulement la configuration ; quelques évolutions structurelles exigent un changement de script, dans un périmètre prévisible et maîtrisé.

  • Si les bornes sont réparties entre plusieurs sous-tableaux, la coordination des équipements partage leurs puissances mesurées avec un contrôleur principal pour une répartition globale.
  • Si marques et modèles augmentent d’un projet à l’autre avec une structure similaire, l’extension dynamique des unités transforme préfixes, conventions et points associés en modèles pour accélérer les projets suivants.
  • Pour envoyer les limites depuis le cloud ou consulter l’état global, utilisez la coordination cloud-edge afin de remonter périodiquement marge réseau et commandes de chaque borne.
Acquisition, calcul, répartition et exécution pour équilibrer la charge des bornes

Ce schéma représente le flux de données et de contrôle selon la configuration décrite ici. Référez-vous aux configurations et scripts pour les champs exacts.