@operato/twin-kernel 0.7.14 → 0.7.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/contract.d.ts +20 -1
- package/dist/ems-kernel.js +42 -3
- package/dist/energy-attribution.d.ts +10 -0
- package/dist/energy-attribution.js +5 -0
- package/dist-cjs/index.cjs +23 -2
- package/package.json +1 -1
package/dist/contract.d.ts
CHANGED
|
@@ -1210,8 +1210,27 @@ export interface MeterPointState {
|
|
|
1210
1210
|
export interface DemandWindowState {
|
|
1211
1211
|
startMs: number;
|
|
1212
1212
|
endMs: number;
|
|
1213
|
-
/**
|
|
1213
|
+
/**
|
|
1214
|
+
* 그 구간의 최대 순간부하 — 표본이 없으면 `undefined`(0 이 아니다).
|
|
1215
|
+
*
|
|
1216
|
+
* **계통에서 끌어온 수요**다(요금이 이것에 매겨진다). 현장에 발전·축전이 있으면 설비가 끌어당긴
|
|
1217
|
+
* 양과 다르다 — 그 차이는 `grossMaxKW` 와의 간격으로 읽는다.
|
|
1218
|
+
*/
|
|
1214
1219
|
maxKW?: number;
|
|
1220
|
+
/**
|
|
1221
|
+
* 설비가 **끌어당긴** 최대(발전·방전 차감 전, 충전 포함).
|
|
1222
|
+
*
|
|
1223
|
+
* ── 왜 둘을 함께 두나 (2026-08-17) ────────────────────────────────────────
|
|
1224
|
+
* 요금은 계통 수요(`maxKW`)로 매겨지지만, 「무엇이 그 수요를 만들었나」는 설비 부하가 답한다.
|
|
1225
|
+
* 하나만 두면 둘 중 하나를 잃는다: 순수요만 두면 태양광이 잠깐 가려졌을 때 왜 피크가 올랐는지 설명할
|
|
1226
|
+
* 수 없고, 총부하만 두면 요금이 실제보다 커 보인다.
|
|
1227
|
+
*
|
|
1228
|
+
* 그리고 **이 둘의 간격이 곧 깎인 양**이다(피크 셰이빙). 같은 표본에서 나온 두 최대값이므로 비교가
|
|
1229
|
+
* 성립한다 — 서로 다른 창의 값을 견주는 것이 아니다.
|
|
1230
|
+
*
|
|
1231
|
+
* 발전·축전을 선언하지 않은 현장에서는 `maxKW` 와 같다(그때는 차감할 것이 없다).
|
|
1232
|
+
*/
|
|
1233
|
+
grossMaxKW?: number;
|
|
1215
1234
|
/** 표본 평균 부하 — 요금 산정의 평균 수요에 대응한다. */
|
|
1216
1235
|
meanKW?: number;
|
|
1217
1236
|
/** **kW 를 실은** 표본 수 — 부하 판정의 근거 수다(받은 표본 전체가 아니다). */
|
package/dist/ems-kernel.js
CHANGED
|
@@ -309,6 +309,9 @@ export class EmsKernel extends FlowEngine {
|
|
|
309
309
|
startMs: w.startMs,
|
|
310
310
|
endMs: w.endMs,
|
|
311
311
|
...(w.maxKW !== undefined ? { maxKW: w.maxKW } : {}),
|
|
312
|
+
/* 총부하도 사실로 남긴다 — 이것이 없으면 저널을 읽는 쪽은 「무엇이 깎였나」를 되짚을 수 없다
|
|
313
|
+
(요금은 순수요로 매겨지지만, 그 수요를 만든 것은 설비 부하다). */
|
|
314
|
+
...(w.grossMaxKW !== undefined ? { grossMaxKW: w.grossMaxKW } : {}),
|
|
312
315
|
...(w.meanKW !== undefined ? { meanKW: w.meanKW } : {}),
|
|
313
316
|
samples: w.samples,
|
|
314
317
|
...(w.contractKW !== undefined ? { contractKW: w.contractKW } : {}),
|
|
@@ -450,14 +453,50 @@ export class EmsKernel extends FlowEngine {
|
|
|
450
453
|
}
|
|
451
454
|
if (!counted)
|
|
452
455
|
return; // 셀 것이 하나도 없으면 구간에 값을 넣지 않는다(0 을 주장하지 않는다)
|
|
456
|
+
/*
|
|
457
|
+
* ── 계통에서 끌어오는 것은 **순수요**다 (2026-08-17) ──────────────────────
|
|
458
|
+
*
|
|
459
|
+
* 그동안 이 함수는 소비만 셌다. 그래서 태양광이 400kW 를 만들고 배터리가 300kW 를 내보내도 계통
|
|
460
|
+
* 수요가 한 톨도 줄지 않았다 — 「배터리로 피크를 깎는다」가 시뮬에서 성립하지 않는 상태였고,
|
|
461
|
+
* 그 위에 세운 요금·계약 판정도 그만큼 틀렸다.
|
|
462
|
+
*
|
|
463
|
+
* 요금이 매겨지는 것은 계량 지점을 지나는 전력이다:
|
|
464
|
+
*
|
|
465
|
+
* 순수요 = 설비 부하 + 충전 − 발전 − 방전
|
|
466
|
+
*
|
|
467
|
+
* 충전이 **더해지는** 이유: 배터리를 채우는 동안 그 전기도 계통에서 온다(피크를 만들 수 있다 —
|
|
468
|
+
* 잘못된 시각에 충전하면 깎으려던 피크를 오히려 키운다. 그 사실이 값에 보여야 한다).
|
|
469
|
+
*
|
|
470
|
+
* 음수는 **역송**이지 음의 수요가 아니다. 계통 수요는 0 에서 멈추고, 남는 발전은 그 설비의
|
|
471
|
+
* `exportKW` 가 이미 사실로 말한다(여기서 다시 만들지 않는다).
|
|
472
|
+
*/
|
|
473
|
+
let generated = 0;
|
|
474
|
+
let discharged = 0;
|
|
475
|
+
let charged = 0;
|
|
476
|
+
for (const m of this.equipment.values()) {
|
|
477
|
+
const g = Number(m.generatedKW);
|
|
478
|
+
if (Number.isFinite(g) && g > 0)
|
|
479
|
+
generated += g;
|
|
480
|
+
const d = Number(m.dischargeKW);
|
|
481
|
+
if (Number.isFinite(d) && d > 0)
|
|
482
|
+
discharged += d;
|
|
483
|
+
const c = Number(m.chargeKW);
|
|
484
|
+
if (Number.isFinite(c) && c > 0)
|
|
485
|
+
charged += c;
|
|
486
|
+
}
|
|
487
|
+
const gross = total + charged;
|
|
488
|
+
const net = Math.max(0, gross - generated - discharged);
|
|
453
489
|
this.closeDue(atMs);
|
|
454
490
|
this.openWindow(atMs);
|
|
455
491
|
const w = this.open;
|
|
456
492
|
w.derived = true;
|
|
457
|
-
|
|
458
|
-
|
|
493
|
+
/* 요금이 보는 수는 순수요다. 총부하는 「무엇이 그 수요를 만들었나」를 위해 함께 남긴다. */
|
|
494
|
+
if (w.maxKW === undefined || net > w.maxKW)
|
|
495
|
+
w.maxKW = net;
|
|
496
|
+
if (w.grossMaxKW === undefined || gross > w.grossMaxKW)
|
|
497
|
+
w.grossMaxKW = gross;
|
|
459
498
|
w.samples++;
|
|
460
|
-
w.meanKW = w.meanKW === undefined ?
|
|
499
|
+
w.meanKW = w.meanKW === undefined ? net : (w.meanKW * (w.samples - 1) + net) / w.samples;
|
|
461
500
|
this.revision++;
|
|
462
501
|
this.judgeOpenWindow(atMs);
|
|
463
502
|
}
|
|
@@ -185,6 +185,15 @@ export interface WindowedEnergy {
|
|
|
185
185
|
peakKW?: number;
|
|
186
186
|
/** 그 최대수요가 난 구간의 시작 — 「언제였나」를 물으면 답할 수 있어야 한다. */
|
|
187
187
|
peakStartMs?: number;
|
|
188
|
+
/**
|
|
189
|
+
* **그 피크 구간에서 설비가 끌어당긴 최대**(발전·방전 차감 전).
|
|
190
|
+
*
|
|
191
|
+
* `peakKW` 와의 간격이 그 순간에 깎인 양이다(태양광·배터리가 계통에서 덜어낸 몫). 다른 구간의 값과
|
|
192
|
+
* 견주지 않는다 — **같은 구간의 두 수**여야 그 차이가 뜻을 갖는다.
|
|
193
|
+
*
|
|
194
|
+
* 발전·축전을 선언하지 않은 현장에서는 `peakKW` 와 같다.
|
|
195
|
+
*/
|
|
196
|
+
peakGrossKW?: number;
|
|
188
197
|
/**
|
|
189
198
|
* 어떻게 얻었나 — **파생임을 숨기지 않는다.**
|
|
190
199
|
* · `mean-kw` — 구간 평균부하 × 구간 길이. 표본의 평균이므로 **추정**이다.
|
|
@@ -217,6 +226,7 @@ export declare function energyOfWindows(windows: readonly {
|
|
|
217
226
|
endMs: number;
|
|
218
227
|
meanKW?: number;
|
|
219
228
|
maxKW?: number;
|
|
229
|
+
grossMaxKW?: number;
|
|
220
230
|
}[], range?: {
|
|
221
231
|
startMs: number;
|
|
222
232
|
endMs: number;
|
|
@@ -205,6 +205,7 @@ export function energyOfWindows(windows, range) {
|
|
|
205
205
|
let last;
|
|
206
206
|
let peakKW;
|
|
207
207
|
let peakStartMs;
|
|
208
|
+
let peakGrossKW;
|
|
208
209
|
for (const w of windows ?? []) {
|
|
209
210
|
if (!inRange(w))
|
|
210
211
|
continue;
|
|
@@ -216,6 +217,9 @@ export function energyOfWindows(windows, range) {
|
|
|
216
217
|
if (Number.isFinite(max) && (peakKW === undefined || max > peakKW)) {
|
|
217
218
|
peakKW = max;
|
|
218
219
|
peakStartMs = Number(w.startMs);
|
|
220
|
+
/* **그 구간의** 총부하를 함께 집는다 — 다른 구간의 총부하와 견주면 차이가 뜻을 잃는다. */
|
|
221
|
+
const gross = Number(w.grossMaxKW);
|
|
222
|
+
peakGrossKW = Number.isFinite(gross) ? gross : undefined;
|
|
219
223
|
}
|
|
220
224
|
const mean = Number(w.meanKW);
|
|
221
225
|
const hours = (Number(w.endMs) - Number(w.startMs)) / 3_600_000;
|
|
@@ -233,6 +237,7 @@ export function energyOfWindows(windows, range) {
|
|
|
233
237
|
return {
|
|
234
238
|
...(counted > 0 ? { kWh } : {}),
|
|
235
239
|
...(peakKW !== undefined ? { peakKW, peakStartMs } : {}),
|
|
240
|
+
...(peakGrossKW !== undefined ? { peakGrossKW } : {}),
|
|
236
241
|
basis: 'mean-kw',
|
|
237
242
|
counted,
|
|
238
243
|
skipped,
|
package/dist-cjs/index.cjs
CHANGED
|
@@ -6019,6 +6019,9 @@ var EmsKernel = class extends FlowEngine {
|
|
|
6019
6019
|
startMs: w.startMs,
|
|
6020
6020
|
endMs: w.endMs,
|
|
6021
6021
|
...w.maxKW !== void 0 ? { maxKW: w.maxKW } : {},
|
|
6022
|
+
/* 총부하도 사실로 남긴다 — 이것이 없으면 저널을 읽는 쪽은 「무엇이 깎였나」를 되짚을 수 없다
|
|
6023
|
+
(요금은 순수요로 매겨지지만, 그 수요를 만든 것은 설비 부하다). */
|
|
6024
|
+
...w.grossMaxKW !== void 0 ? { grossMaxKW: w.grossMaxKW } : {},
|
|
6022
6025
|
...w.meanKW !== void 0 ? { meanKW: w.meanKW } : {},
|
|
6023
6026
|
samples: w.samples,
|
|
6024
6027
|
...w.contractKW !== void 0 ? { contractKW: w.contractKW } : {},
|
|
@@ -6144,13 +6147,27 @@ var EmsKernel = class extends FlowEngine {
|
|
|
6144
6147
|
}
|
|
6145
6148
|
}
|
|
6146
6149
|
if (!counted) return;
|
|
6150
|
+
let generated = 0;
|
|
6151
|
+
let discharged = 0;
|
|
6152
|
+
let charged = 0;
|
|
6153
|
+
for (const m of this.equipment.values()) {
|
|
6154
|
+
const g = Number(m.generatedKW);
|
|
6155
|
+
if (Number.isFinite(g) && g > 0) generated += g;
|
|
6156
|
+
const d = Number(m.dischargeKW);
|
|
6157
|
+
if (Number.isFinite(d) && d > 0) discharged += d;
|
|
6158
|
+
const c = Number(m.chargeKW);
|
|
6159
|
+
if (Number.isFinite(c) && c > 0) charged += c;
|
|
6160
|
+
}
|
|
6161
|
+
const gross = total + charged;
|
|
6162
|
+
const net = Math.max(0, gross - generated - discharged);
|
|
6147
6163
|
this.closeDue(atMs);
|
|
6148
6164
|
this.openWindow(atMs);
|
|
6149
6165
|
const w = this.open;
|
|
6150
6166
|
w.derived = true;
|
|
6151
|
-
if (w.maxKW === void 0 ||
|
|
6167
|
+
if (w.maxKW === void 0 || net > w.maxKW) w.maxKW = net;
|
|
6168
|
+
if (w.grossMaxKW === void 0 || gross > w.grossMaxKW) w.grossMaxKW = gross;
|
|
6152
6169
|
w.samples++;
|
|
6153
|
-
w.meanKW = w.meanKW === void 0 ?
|
|
6170
|
+
w.meanKW = w.meanKW === void 0 ? net : (w.meanKW * (w.samples - 1) + net) / w.samples;
|
|
6154
6171
|
this.revision++;
|
|
6155
6172
|
this.judgeOpenWindow(atMs);
|
|
6156
6173
|
}
|
|
@@ -6461,12 +6478,15 @@ function energyOfWindows(windows, range) {
|
|
|
6461
6478
|
let last;
|
|
6462
6479
|
let peakKW;
|
|
6463
6480
|
let peakStartMs;
|
|
6481
|
+
let peakGrossKW;
|
|
6464
6482
|
for (const w of windows ?? []) {
|
|
6465
6483
|
if (!inRange(w)) continue;
|
|
6466
6484
|
const max = Number(w.maxKW);
|
|
6467
6485
|
if (Number.isFinite(max) && (peakKW === void 0 || max > peakKW)) {
|
|
6468
6486
|
peakKW = max;
|
|
6469
6487
|
peakStartMs = Number(w.startMs);
|
|
6488
|
+
const gross = Number(w.grossMaxKW);
|
|
6489
|
+
peakGrossKW = Number.isFinite(gross) ? gross : void 0;
|
|
6470
6490
|
}
|
|
6471
6491
|
const mean = Number(w.meanKW);
|
|
6472
6492
|
const hours = (Number(w.endMs) - Number(w.startMs)) / 36e5;
|
|
@@ -6482,6 +6502,7 @@ function energyOfWindows(windows, range) {
|
|
|
6482
6502
|
return {
|
|
6483
6503
|
...counted > 0 ? { kWh } : {},
|
|
6484
6504
|
...peakKW !== void 0 ? { peakKW, peakStartMs } : {},
|
|
6505
|
+
...peakGrossKW !== void 0 ? { peakGrossKW } : {},
|
|
6485
6506
|
basis: "mean-kw",
|
|
6486
6507
|
counted,
|
|
6487
6508
|
skipped,
|
package/package.json
CHANGED