콘텐츠로 이동
공식 웹사이트로 돌아가기

사례: 충전기 클러스터 부하 분산

이 사례는 Edge-EMS Builder의 범용 기능인 동적 데이터 검색, 표현식 계산, 트리거 구성 및 빈도 제한만으로 “장치 수가 변해도 스크립트를 수정할 필요가 없는” 부하 분산 전략을 만드는 방법을 보여 줍니다. 장치 브랜드와 명명 접두사는 예시이므로 프로젝트의 실제 이름으로 교체하세요.

분전반에 여러 충전기(OCPP 또는 제조사 전용 프로토콜 등)와 필수 부하(조명, 냉난방 등)가 함께 연결되어 있습니다. 분전반의 계통 측 인입 용량은 고정되어 있으므로 모든 충전기가 동시에 최대 출력으로 충전하면 총 전력 사용량이 인입 정격 용량을 초과하여 차단기가 트립하거나 용량 초과 요금이 발생할 수 있습니다.

목표는 필수 부하에 우선 전력을 공급하면서 모든 온라인 충전기가 남은 가용 용량을 공유하도록 하는 것입니다. 각 충전기의 충전 전류를 동적으로 조정하고, 어느 충전기가 접속하거나 이탈해도 수초 내에 다시 균형을 맞추며, 새 충전기를 추가할 때 전략 스크립트를 변경하지 않습니다.

시스템에는 두 가지 핵심 측정 포인트가 있습니다.

  • Grid_total_power: 계통 측 실시간 총 입력 전력(분전반의 모든 부하 포함: 필수 부하 + 충전기 + 기타)
  • 필수 부하는 이미 이 실측값에 반영되어 있으므로 충전기의 총 소비만 제어하면 분전반의 총 입력이 정격 전력을 넘지 않도록 할 수 있으며, 필수 부하는 자연스럽게 우선 공급됩니다.
-- 배분 가능한 여유 전력(kW) = (정격 전력 - 실측 계통 총전력) × 안전 계수
-- 10%의 안전 여유로 전력 오버슈트를 방지합니다. 실측값에는 필수 부하가 포함되어 있으므로 별도로 차감할 필요가 없습니다.
local avail_kw = (rated_kw - grid_kw) * 0.9
local per_A = (avail_kw / n) * 1000 / voltage_V -- 전류(A)를 균등하게 배분합니다.

3. 동적 장치 검색: 충전기 추가 시 스크립트 수정 불필요

섹션 제목: “3. 동적 장치 검색: 충전기 추가 시 스크립트 수정 불필요”

전략은 충전기 수를 미리 정하지 않습니다. 실행할 때마다 접두사+접미사 기준의 일괄 검색으로 모든 온라인 충전기를 자동으로 찾습니다.

local online = {}
-- 각 브랜드 접두사를 순회합니다. 필요에 따라 추가하거나 삭제할 수 있습니다.
for _, brandPrefix in ipairs({"VENDORA", "VENDORB"}) do
local statusMap = getLatestDataValuesByPrefixAndSuffix("Collection", brandPrefix, "_status")
if statusMap then
-- 반환값은 dataKey를 키로 사용하는 Table입니다. pairs()로 순회하며, 비어 있는지 확인할 때는 센티널 문자열 "__Null__"과 비교합니다.
for dataKey, dto in pairs(statusMap) do
if dto.DataValue ~= "__Null__" then
table.insert(online, dto)
end
end
end
end

새 충전기의 데이터 포인트를 정해진 명명 규칙(아래 “명명 규칙” 참조)에 따라 연결하면 다음 트리거에서 균등 배분 계산에 자동 포함됩니다. 스크립트를 수정하거나 서비스를 다시 시작할 필요가 없습니다. 이는 데이터 모델과 Lua 엔진을 조합한 기능이며 특정 전용 기능이 아닙니다.

4. 트리거 모델: 데이터 갱신 및 스크립트 내부 빈도 제한

섹션 제목: “4. 트리거 모델: 데이터 갱신 및 스크립트 내부 빈도 제한”

고정 주기 폴링만 사용하면 “데이터가 바뀌지 않아도 실행되는” 중복 쓰기가 발생하고, 빈도 제한 조건까지 트리거에 넣으면 조건 설정이 복잡해집니다. 여기서는 더 간단한 방식을 사용합니다.

데이터 갱신 트리거 + 스크립트 내 타임스탬프 기반 빈도 제한: 트리거는 데이터 갱신만 감지하고 실제 명령 전송 빈도는 스크립트가 직접 제어합니다.

데이터 갱신 → 조건 충족 → 스크립트 트리거 → 집계 데이터 포인트 last_ctrl_ms(마지막 실행 타임스탬프) 읽기
→ 경과 시간 < 5000ms: 명령을 전송하지 않고 즉시 반환
→ 경과 시간 ≥ 5000ms: 제어 실행 → 명령 쓰기 → 타임스탬프 갱신

배포 사전 단계: 스크립트의 “기억”으로 사용할 집계 데이터 포인트 하나를 먼저 수동 생성해야 합니다.

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

트리거될 때마다 스크립트는 계통 매개변수 읽기 → 빈도 제한 확인 → 온라인 충전기 검색 → 가용 전류 계산 → 충전기별 결정 → 타임스탬프 갱신 → 명령 목록 반환 순서로 실행합니다. 충전기별 결정 규칙은 다음과 같습니다.

조건 충전기 상태 실행 동작
과부하 또는 균등 배분 전류 < 최소 전류 충전 중 충전 중지(stop_transaction)
과부하 또는 균등 배분 전류 < 최소 전류 기타 건너뛰기
과부하 아님 및 균등 배분 전류 ≥ 최소 전류 충전 중 새 전류 제한값 전달(set_charging_profile)
과부하 아님 및 균등 배분 전류 ≥ 최소 전류 충전 중 아님 충전 시작(start_transaction, 유효한 승인 Tag 필요)

해당 핵심 스크립트(트리거 동작, 쓰기 설명 배열 반환):

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$", "") -- dataKey에서 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

스크립트는 {브랜드_접두사}{두_자리_번호}_{속성} 형식으로 충전기 데이터 포인트를 일괄 검색하므로 다음 규칙을 엄격히 따라야 합니다.

부분 규칙 예
브랜드 접두사 모두 대문자, 밑줄 없음 VENDORA, VENDORB(실제 장치 브랜드 식별자로 교체)
번호 앞을 0으로 채운 두 자리 01, 02…10
속성 아래 표 참조 status, type

각 충전기에 필요한 수집 데이터 포인트(일부 예):

속성 전체 dataKey 예 읽기/쓰기 설명
_status VENDORA01_status 읽기 충전기 상태이며 검색 기준으로 사용합니다. 누락되면 해당 충전기를 찾지 못합니다.
_transaction_id VENDORA01_transaction_id 읽기 현재 트랜잭션 ID
_type VENDORA01_type 쓰기 명령 유형: start/stop/set_charging_profile
_periods VENDORA01_periods 쓰기 제한 주기 JSON(예: [{"start":0,"limit":16}])

같은 브랜드의 여섯 번째 충전기를 추가할 때는 위 규칙에 맞춰 VENDORA06_status, VENDORA06_type 등의 데이터 포인트를 연결하면 균등 배분 로직에 자동으로 포함됩니다.

상황 스크립트 변경 필요 여부 작업
같은 브랜드 충전기 추가 아니요 명명 규칙에 따라 수집 데이터 포인트 추가
정격 용량 상한 조정 아니요 집계 데이터 포인트 Grid_current 값 변경
최소 충전 전력 조정 아니요 집계 데이터 포인트 min_charge_power 값 변경
충전기 일시 오프라인 아니요 연결 해제 후 _status가 비어 온라인 목록에서 자동 제외
새 충전기 브랜드 추가 예 검색 목록에 {"VENDORA", "VENDORB", "VENDORC"} 등의 접두사 추가
안전 여유 계수 설정 기능 추가 예(한 번) 계수를 담을 집계 포인트를 추가하고 스크립트가 해당 포인트를 읽도록 변경

이 표는 설계 철학의 핵심도 보여 줍니다. 대부분의 업무 변경에는 설정 수정만 필요하며 일부 구조적 변경에만 스크립트 수정이 필요합니다. 변경 범위도 예측하고 제어할 수 있습니다.

  • 충전기 클러스터가 여러 하위 분전반에 분산되어 있다면 장치 협업으로 각 분전반의 계통 실측 전력을 주 제어 장치에 공유하고 전체 균등 배분을 통합 수행할 수 있습니다.
  • 프로젝트마다 충전기 브랜드와 모델이 계속 늘어나고 충전기 군 구조가 유사하다면 장치 유닛 동적 확장으로 “브랜드 접두사 + 명명 규칙 + 관련 데이터 포인트”를 템플릿으로 만들어 새 프로젝트 구성 시간을 줄일 수 있습니다.
  • 클라우드에서 전류 제한 임계값을 전달하거나 전체 부하 분산 상태를 확인해야 한다면 클라우드와 엣지의 협업으로 계통 여유 용량과 각 충전기의 명령 전송 기록을 주기적으로 보고합니다.
충전기 부하 분산의 수집, 계산, 배분 및 실행 흐름

이 그림은 본 페이지의 설정 로직을 바탕으로 데이터 및 제어 흐름을 보여 줍니다. 구체적인 작업 필드는 본문의 설정과 스크립트를 참조하세요.