@operato/twin-kernel 0.7.74 → 0.7.76
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 +55 -0
- package/dist/contract.js +12 -0
- package/dist/ems-kernel.d.ts +42 -1
- package/dist/ems-kernel.js +142 -1
- package/dist/energy-ingest.d.ts +16 -0
- package/dist/energy-ingest.js +57 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist-cjs/index.cjs +174 -1
- package/package.json +1 -1
package/dist/contract.d.ts
CHANGED
|
@@ -2035,6 +2035,24 @@ export interface EnergyState {
|
|
|
2035
2035
|
* 틀렸다는 뜻이고, 사람이 찾아내지 않아도 드러나야 한다.
|
|
2036
2036
|
*/
|
|
2037
2037
|
lastBill?: EnergyBillData;
|
|
2038
|
+
/**
|
|
2039
|
+
* 마지막으로 받은 **요금 기준** — 이 주기에 적용되는 요금적용전력과 단가.
|
|
2040
|
+
*
|
|
2041
|
+
* 청구서보다 이쪽이 먼저다: 청구서는 끝난 기간의 정산이고 이것은 지금 적용되는 값이다.
|
|
2042
|
+
*/
|
|
2043
|
+
tariffBasis?: EnergyTariffBasisData;
|
|
2044
|
+
/**
|
|
2045
|
+
* 이 청구 주기의 **최고 수요** — 기본요금이 이 값으로 다시 정해진다.
|
|
2046
|
+
*
|
|
2047
|
+
* `periodStart` 는 마지막 청구서의 끝이다(그 뒤가 아직 청구되지 않은 구간). 그것이 바뀌면 처음부터
|
|
2048
|
+
* 다시 센다 — 지난 주기의 최고가 이번 주기를 정하지 않는다.
|
|
2049
|
+
*/
|
|
2050
|
+
demandHigh?: {
|
|
2051
|
+
kW: number;
|
|
2052
|
+
atTo: ISOTime;
|
|
2053
|
+
periodStart?: ISOTime;
|
|
2054
|
+
since: ISOTime;
|
|
2055
|
+
};
|
|
2038
2056
|
/**
|
|
2039
2057
|
* 설비마다 **지금 기간의 발전 관측** — 기간 발전량을 확정할 때 쓰는 기준점.
|
|
2040
2058
|
*
|
|
@@ -2462,6 +2480,18 @@ export declare const ENERGY_EVENT: {
|
|
|
2462
2480
|
* 쪽인지 말한다.
|
|
2463
2481
|
*/
|
|
2464
2482
|
readonly usagePeriod: "energy.usage.period";
|
|
2483
|
+
/**
|
|
2484
|
+
* **이 주기에 적용되는 요금 기준이 정해졌다** — 요금적용전력과 기본요금 단가.
|
|
2485
|
+
*
|
|
2486
|
+
* 청구서와 다른 사건이다. 청구서는 끝난 기간의 정산이고 이것은 **지금 적용되는 기준**이다.
|
|
2487
|
+
* 그래서 사건 시각도 다르다 — 청구서는 기간의 끝, 이것은 기간의 시작이다(그때부터 유효하다).
|
|
2488
|
+
*
|
|
2489
|
+
* 이 값은 매 주기 다시 정해진다. 모델의 선언에 적어 두면 사람이 매달 갱신해야 하고, 안 하면 낡은
|
|
2490
|
+
* 값으로 기본요금이 조용히 틀린다. 공급자가 알려 주는 값을 그대로 받는 것이 맞다.
|
|
2491
|
+
*
|
|
2492
|
+
* **금액을 싣지 않는다.** 요금적용전력 × 단가는 커널이 계산한다. 곱을 받으면 그것이 정산인 척한다.
|
|
2493
|
+
*/
|
|
2494
|
+
readonly tariffBasis: "energy.tariff.basis";
|
|
2465
2495
|
/**
|
|
2466
2496
|
* **청구서가 도착했다** — 공급자가 확정한 금액.
|
|
2467
2497
|
*
|
|
@@ -2717,6 +2747,31 @@ export interface EnergyUsagePeriodData {
|
|
|
2717
2747
|
* **커널의 요금 어휘로 받는다**(사용량 요금·기본요금·합계). 공급자마다 청구서의 칸 이름이 다르므로
|
|
2718
2748
|
* 그 이름을 그대로 들이면 커널이 한 공급자의 모양에 묶인다. 옮기는 것은 커넥터의 일이다.
|
|
2719
2749
|
*/
|
|
2750
|
+
/**
|
|
2751
|
+
* 이 주기에 적용되는 요금 기준 — `ENERGY_EVENT.tariffBasis` 의 데이터.
|
|
2752
|
+
*
|
|
2753
|
+
* 사건 시각은 `from` 이다. 「이 시각부터 이 기준이 적용된다」이므로 주기가 끝나기 전에 성립하고,
|
|
2754
|
+
* `to` 로 시각을 잡으면 아직 오지 않은 시각이 저널에 적힌다.
|
|
2755
|
+
*/
|
|
2756
|
+
export interface EnergyTariffBasisData {
|
|
2757
|
+
from: ISOTime;
|
|
2758
|
+
to: ISOTime;
|
|
2759
|
+
/** 기본요금이 걸리는 기준(kW) — 공급자가 정한다(§`TariffDeclaration.billingDemandKW`). */
|
|
2760
|
+
billingDemandKW?: number;
|
|
2761
|
+
/** 그 기준 1kW 당 기본요금. */
|
|
2762
|
+
demandChargePerKW?: number;
|
|
2763
|
+
currency?: string;
|
|
2764
|
+
}
|
|
2765
|
+
/**
|
|
2766
|
+
* 청구서 — 공급자가 **정산한** 금액.
|
|
2767
|
+
*
|
|
2768
|
+
* ── 지금 이 문으로 오는 것이 없다 (2026-08-29 확인) ────────────────────────
|
|
2769
|
+
* 붙어 있는 원본에 정산 금액을 주는 조회가 없다. 그 원본이 주는 것은 이 주기의 **요금 기준**이고
|
|
2770
|
+
* 그것은 다른 문으로 받는다(§`EnergyTariffBasisData`).
|
|
2771
|
+
*
|
|
2772
|
+
* 그 둘을 한 문으로 받으면 **우리가 곱한 값을 우리가 다시 빼는** 비교가 된다 — 실제로 한 번 그렇게
|
|
2773
|
+
* 했고, 차이가 0 인 것을 계산이 맞았다는 확인으로 읽었다. 같은 두 값을 두 번 곱한 것이었다.
|
|
2774
|
+
*/
|
|
2720
2775
|
export interface EnergyBillData {
|
|
2721
2776
|
from: ISOTime;
|
|
2722
2777
|
to: ISOTime;
|
package/dist/contract.js
CHANGED
|
@@ -1046,6 +1046,18 @@ export const ENERGY_EVENT = {
|
|
|
1046
1046
|
* 쪽인지 말한다.
|
|
1047
1047
|
*/
|
|
1048
1048
|
usagePeriod: 'energy.usage.period',
|
|
1049
|
+
/**
|
|
1050
|
+
* **이 주기에 적용되는 요금 기준이 정해졌다** — 요금적용전력과 기본요금 단가.
|
|
1051
|
+
*
|
|
1052
|
+
* 청구서와 다른 사건이다. 청구서는 끝난 기간의 정산이고 이것은 **지금 적용되는 기준**이다.
|
|
1053
|
+
* 그래서 사건 시각도 다르다 — 청구서는 기간의 끝, 이것은 기간의 시작이다(그때부터 유효하다).
|
|
1054
|
+
*
|
|
1055
|
+
* 이 값은 매 주기 다시 정해진다. 모델의 선언에 적어 두면 사람이 매달 갱신해야 하고, 안 하면 낡은
|
|
1056
|
+
* 값으로 기본요금이 조용히 틀린다. 공급자가 알려 주는 값을 그대로 받는 것이 맞다.
|
|
1057
|
+
*
|
|
1058
|
+
* **금액을 싣지 않는다.** 요금적용전력 × 단가는 커널이 계산한다. 곱을 받으면 그것이 정산인 척한다.
|
|
1059
|
+
*/
|
|
1060
|
+
tariffBasis: 'energy.tariff.basis',
|
|
1049
1061
|
/**
|
|
1050
1062
|
* **청구서가 도착했다** — 공급자가 확정한 금액.
|
|
1051
1063
|
*
|
package/dist/ems-kernel.d.ts
CHANGED
|
@@ -122,6 +122,13 @@ export declare class EmsKernel extends FlowEngine {
|
|
|
122
122
|
* 모르는 지점이면 세어 둔다 — 조용히 버리면 「값이 왜 안 보이지」로만 남는다(계량과 같은 규율).
|
|
123
123
|
*/
|
|
124
124
|
private applyUsagePeriod;
|
|
125
|
+
/**
|
|
126
|
+
* 이 주기에 적용되는 요금 기준을 상태에 둔다.
|
|
127
|
+
*
|
|
128
|
+
* 늦게 온 지난 주기의 기준이 최신을 덮지 않게 시작 시각으로 가린다. 같은 주기의 값이 다시 오면
|
|
129
|
+
* 새것을 쓴다 — 사업자가 그 주기의 기준을 고칠 수 있다.
|
|
130
|
+
*/
|
|
131
|
+
private applyTariffBasis;
|
|
125
132
|
/**
|
|
126
133
|
* 청구서를 상태에 둔다 — **우리 계산을 덮지 않는다.**
|
|
127
134
|
*
|
|
@@ -197,8 +204,17 @@ export declare class EmsKernel extends FlowEngine {
|
|
|
197
204
|
private generationPeriodObs;
|
|
198
205
|
/** 기준점이 없어 마감하지 못한 기간 누적의 수 — 조용히 사라지지 않게 센다. */
|
|
199
206
|
private unclosedGenerationPeriods;
|
|
200
|
-
/** 마지막으로 받은 청구서 — 공급자가
|
|
207
|
+
/** 마지막으로 받은 청구서 — 공급자가 **정산한** 금액. 지금 이 문으로 오는 것은 없다. */
|
|
201
208
|
private lastBill?;
|
|
209
|
+
/** 마지막으로 받은 요금 기준 — 이 주기에 적용되는 요금적용전력과 단가. */
|
|
210
|
+
private tariffBasis?;
|
|
211
|
+
/**
|
|
212
|
+
* 이 청구 주기의 **최고 수요** — 기본요금이 이 값으로 다시 정해진다.
|
|
213
|
+
*
|
|
214
|
+
* `since` 는 이 주기에서 처음 잰 구간의 끝이다. `periodStart` 는 마지막 청구서의 끝이고,
|
|
215
|
+
* 그것이 바뀌면 처음부터 다시 센다.
|
|
216
|
+
*/
|
|
217
|
+
private demandHigh?;
|
|
202
218
|
private applyEquipmentEnergy;
|
|
203
219
|
/** 우리 모델이 모르는 설비가 상태를 보내 온 횟수 — 조용히 버리지 않는다. */
|
|
204
220
|
private unknownEquipmentReports;
|
|
@@ -242,6 +258,31 @@ export declare class EmsKernel extends FlowEngine {
|
|
|
242
258
|
* 어려우므로 높게 부르지 않는다(경보가 소음이 되면 사람이 경보 자체를 읽지 않는다).
|
|
243
259
|
*/
|
|
244
260
|
protected collectAttentions(): Attention[];
|
|
261
|
+
/**
|
|
262
|
+
* **이 주기의 최고가 요금적용전력을 넘었다** — 다음 주기의 기본요금이 오른다.
|
|
263
|
+
*
|
|
264
|
+
* ── 왜 계약 초과와 다른 신호인가 (2026-08-29) ────────────────────────────
|
|
265
|
+
* 계약 초과는 약정 위반이고 지금 조치할 일이다(줄이면 된다). 이쪽은 **이미 일어난 일**이다 —
|
|
266
|
+
* 넘은 순간 다음 주기의 기준이 정해지고, 지금 줄여도 되돌아가지 않는다. 사람이 할 일이 다르므로
|
|
267
|
+
* 신호도 다르다.
|
|
268
|
+
*
|
|
269
|
+
* ── 한 번만 울린다 ──────────────────────────────────────────────────────────
|
|
270
|
+
* 넘으면 그 주기 내내 참이다. 구간마다 다시 울리면 소음이 되고, 소음이 되면 아무도 안 본다.
|
|
271
|
+
* 그래서 신호의 id 를 주기로 묶고 「처음 넘은 시각」이 아니라 **그 주기의 최고**를 싣는다.
|
|
272
|
+
*
|
|
273
|
+
* ── 늦게 안다 ───────────────────────────────────────────────────────────────
|
|
274
|
+
* 최고 수요는 지난 구간이 마감되어야 온다(지금 원본은 시간마다). 그래서 넘은 것을 최대 한 시간
|
|
275
|
+
* 뒤에 안다. 그 사실을 `observedTo` 로 함께 내어 화면이 「언제까지의 값인가」를 말할 수 있게 한다.
|
|
276
|
+
*/
|
|
277
|
+
protected billingDemandAttention(): Attention | undefined;
|
|
278
|
+
/**
|
|
279
|
+
* 선언된 **요금적용전력** — 기본요금이 걸리는 기준(§`EMS_PROPERTY.billingDemandKW`).
|
|
280
|
+
*
|
|
281
|
+
* 청구서가 그 값을 실어 오면 그쪽이 먼저다(§`billingDemandKW()`). 이것은 청구가 오지 않는 현장의 몫이다.
|
|
282
|
+
*/
|
|
283
|
+
private declaredBillingDemandKW;
|
|
284
|
+
/** 지금 쓰는 요금적용전력 — **청구서가 먼저다.** 매달 바뀌는 값이라 선언은 곧 낡는다. */
|
|
285
|
+
private billingDemandKW;
|
|
245
286
|
/** 계약을 선언한 수전 자리 — 주의가 가리킬 대상. 없으면 대상을 지어내지 않는다. */
|
|
246
287
|
private incomingLocationId;
|
|
247
288
|
/** 감축 가능으로 **선언된** 설비 — 커널이 능력을 짐작하지 않는다(타입이 선언한다). */
|
package/dist/ems-kernel.js
CHANGED
|
@@ -277,6 +277,10 @@ export class EmsKernel extends FlowEngine {
|
|
|
277
277
|
this.applyBill(envelope);
|
|
278
278
|
return;
|
|
279
279
|
}
|
|
280
|
+
if (envelope.eventType === ENERGY_EVENT.tariffBasis) {
|
|
281
|
+
this.applyTariffBasis(envelope);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
280
284
|
super.apply(envelope);
|
|
281
285
|
}
|
|
282
286
|
/**
|
|
@@ -315,6 +319,45 @@ export class EmsKernel extends FlowEngine {
|
|
|
315
319
|
point.lastPeriodTo = d.to;
|
|
316
320
|
if (d?.basis)
|
|
317
321
|
point.lastPeriodBasis = d.basis;
|
|
322
|
+
/*
|
|
323
|
+
* ── 이 청구 주기의 최고 수요 (2026-08-29) ─────────────────────────────────
|
|
324
|
+
*
|
|
325
|
+
* 기본요금은 이 값으로 다시 정해진다. 그래서 **넘은 순간과 얼마나 넘었는지**를 기억한다.
|
|
326
|
+
*
|
|
327
|
+
* 주기의 시작은 마지막 청구서의 끝이다(그 뒤가 아직 청구되지 않은 구간이다). 청구서가 없으면
|
|
328
|
+
* 관측을 시작한 때부터다 — 그 사실은 `since` 가 말한다.
|
|
329
|
+
*
|
|
330
|
+
* 구간마다 새로 세지 않는다: 한 번 넘으면 그 주기 내내 참이고, 매 구간 다시 알리면 소음이 된다.
|
|
331
|
+
*/
|
|
332
|
+
const maxKW = Number(d?.maxKW);
|
|
333
|
+
if (Number.isFinite(maxKW) && maxKW >= 0 && d?.to) {
|
|
334
|
+
const periodStart = this.lastBill?.to;
|
|
335
|
+
/* 청구 주기가 바뀌었으면 처음부터 다시 센다 — 지난 주기의 최고가 이번 주기를 정하지 않는다. */
|
|
336
|
+
if (this.demandHigh && periodStart && this.demandHigh.periodStart !== periodStart)
|
|
337
|
+
this.demandHigh = undefined;
|
|
338
|
+
if (!this.demandHigh || maxKW > this.demandHigh.kW) {
|
|
339
|
+
this.demandHigh = { kW: maxKW, atTo: String(d.to), periodStart, since: this.demandHigh?.since ?? String(d.to) };
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
this.revision++;
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* 이 주기에 적용되는 요금 기준을 상태에 둔다.
|
|
346
|
+
*
|
|
347
|
+
* 늦게 온 지난 주기의 기준이 최신을 덮지 않게 시작 시각으로 가린다. 같은 주기의 값이 다시 오면
|
|
348
|
+
* 새것을 쓴다 — 사업자가 그 주기의 기준을 고칠 수 있다.
|
|
349
|
+
*/
|
|
350
|
+
applyTariffBasis(envelope) {
|
|
351
|
+
const d = envelope.data;
|
|
352
|
+
if (!d?.from || !d?.to)
|
|
353
|
+
return;
|
|
354
|
+
const fromMs = Date.parse(String(d.from));
|
|
355
|
+
if (!Number.isFinite(fromMs))
|
|
356
|
+
return;
|
|
357
|
+
const priorFromMs = Date.parse(String(this.tariffBasis?.from ?? ''));
|
|
358
|
+
if (Number.isFinite(priorFromMs) && fromMs < priorFromMs)
|
|
359
|
+
return;
|
|
360
|
+
this.tariffBasis = { ...d };
|
|
318
361
|
this.revision++;
|
|
319
362
|
}
|
|
320
363
|
/**
|
|
@@ -580,8 +623,17 @@ export class EmsKernel extends FlowEngine {
|
|
|
580
623
|
generationPeriodObs = new Map();
|
|
581
624
|
/** 기준점이 없어 마감하지 못한 기간 누적의 수 — 조용히 사라지지 않게 센다. */
|
|
582
625
|
unclosedGenerationPeriods = 0;
|
|
583
|
-
/** 마지막으로 받은 청구서 — 공급자가
|
|
626
|
+
/** 마지막으로 받은 청구서 — 공급자가 **정산한** 금액. 지금 이 문으로 오는 것은 없다. */
|
|
584
627
|
lastBill;
|
|
628
|
+
/** 마지막으로 받은 요금 기준 — 이 주기에 적용되는 요금적용전력과 단가. */
|
|
629
|
+
tariffBasis;
|
|
630
|
+
/**
|
|
631
|
+
* 이 청구 주기의 **최고 수요** — 기본요금이 이 값으로 다시 정해진다.
|
|
632
|
+
*
|
|
633
|
+
* `since` 는 이 주기에서 처음 잰 구간의 끝이다. `periodStart` 는 마지막 청구서의 끝이고,
|
|
634
|
+
* 그것이 바뀌면 처음부터 다시 센다.
|
|
635
|
+
*/
|
|
636
|
+
demandHigh;
|
|
585
637
|
applyEquipmentEnergy(envelope) {
|
|
586
638
|
const d = envelope.data;
|
|
587
639
|
const id = String(d?.equipmentId ?? '').trim();
|
|
@@ -945,6 +997,9 @@ export class EmsKernel extends FlowEngine {
|
|
|
945
997
|
*/
|
|
946
998
|
collectAttentions() {
|
|
947
999
|
const out = super.collectAttentions();
|
|
1000
|
+
const billingOver = this.billingDemandAttention();
|
|
1001
|
+
if (billingOver)
|
|
1002
|
+
out.push(billingOver);
|
|
948
1003
|
const w = this.open;
|
|
949
1004
|
if (!w || w.contractKW === undefined || w.meanKW === undefined)
|
|
950
1005
|
return out;
|
|
@@ -988,6 +1043,85 @@ export class EmsKernel extends FlowEngine {
|
|
|
988
1043
|
});
|
|
989
1044
|
return out;
|
|
990
1045
|
}
|
|
1046
|
+
/**
|
|
1047
|
+
* **이 주기의 최고가 요금적용전력을 넘었다** — 다음 주기의 기본요금이 오른다.
|
|
1048
|
+
*
|
|
1049
|
+
* ── 왜 계약 초과와 다른 신호인가 (2026-08-29) ────────────────────────────
|
|
1050
|
+
* 계약 초과는 약정 위반이고 지금 조치할 일이다(줄이면 된다). 이쪽은 **이미 일어난 일**이다 —
|
|
1051
|
+
* 넘은 순간 다음 주기의 기준이 정해지고, 지금 줄여도 되돌아가지 않는다. 사람이 할 일이 다르므로
|
|
1052
|
+
* 신호도 다르다.
|
|
1053
|
+
*
|
|
1054
|
+
* ── 한 번만 울린다 ──────────────────────────────────────────────────────────
|
|
1055
|
+
* 넘으면 그 주기 내내 참이다. 구간마다 다시 울리면 소음이 되고, 소음이 되면 아무도 안 본다.
|
|
1056
|
+
* 그래서 신호의 id 를 주기로 묶고 「처음 넘은 시각」이 아니라 **그 주기의 최고**를 싣는다.
|
|
1057
|
+
*
|
|
1058
|
+
* ── 늦게 안다 ───────────────────────────────────────────────────────────────
|
|
1059
|
+
* 최고 수요는 지난 구간이 마감되어야 온다(지금 원본은 시간마다). 그래서 넘은 것을 최대 한 시간
|
|
1060
|
+
* 뒤에 안다. 그 사실을 `observedTo` 로 함께 내어 화면이 「언제까지의 값인가」를 말할 수 있게 한다.
|
|
1061
|
+
*/
|
|
1062
|
+
billingDemandAttention() {
|
|
1063
|
+
const high = this.demandHigh;
|
|
1064
|
+
const basis = this.billingDemandKW();
|
|
1065
|
+
if (!high || basis === undefined || high.kW <= basis)
|
|
1066
|
+
return undefined;
|
|
1067
|
+
const over = high.kW - basis;
|
|
1068
|
+
const ratio = over / basis;
|
|
1069
|
+
return {
|
|
1070
|
+
id: `billing-demand-exceeded:${high.periodStart ?? high.since}`,
|
|
1071
|
+
kind: 'billing-demand-exceeded',
|
|
1072
|
+
severity: ratio > 0.15 ? 'high' : 'medium',
|
|
1073
|
+
anchor: { locationId: this.incomingLocationId() },
|
|
1074
|
+
params: {
|
|
1075
|
+
peakKW: Math.round(high.kW),
|
|
1076
|
+
billingDemandKW: Math.round(basis),
|
|
1077
|
+
overKW: Math.round(over),
|
|
1078
|
+
/* 값의 출처 — 청구서가 말한 기준인지 현장의 선언인지. 선언은 곧 낡는다. */
|
|
1079
|
+
basisFrom: Number.isFinite(Number(this.tariffBasis?.billingDemandKW))
|
|
1080
|
+
? 'tariff-basis'
|
|
1081
|
+
: Number.isFinite(Number(this.lastBill?.billingDemandKW))
|
|
1082
|
+
? 'bill'
|
|
1083
|
+
: 'declaration',
|
|
1084
|
+
observedTo: high.atTo,
|
|
1085
|
+
periodSince: high.since
|
|
1086
|
+
},
|
|
1087
|
+
/*
|
|
1088
|
+
* 조치를 걸지 않는다. **이미 일어난 일이라 되돌릴 수 없다** — 지금 줄여도 이 주기의 최고는
|
|
1089
|
+
* 그대로다. 조치를 붙이면 화면이 「누르면 해결된다」고 거짓말한다.
|
|
1090
|
+
*/
|
|
1091
|
+
recommendedActions: []
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
/**
|
|
1095
|
+
* 선언된 **요금적용전력** — 기본요금이 걸리는 기준(§`EMS_PROPERTY.billingDemandKW`).
|
|
1096
|
+
*
|
|
1097
|
+
* 청구서가 그 값을 실어 오면 그쪽이 먼저다(§`billingDemandKW()`). 이것은 청구가 오지 않는 현장의 몫이다.
|
|
1098
|
+
*/
|
|
1099
|
+
declaredBillingDemandKW() {
|
|
1100
|
+
let max;
|
|
1101
|
+
for (const loc of this.boardDef?.locations ?? []) {
|
|
1102
|
+
for (const p of loc.properties ?? []) {
|
|
1103
|
+
if (p.id !== EMS_PROPERTY.billingDemandKW)
|
|
1104
|
+
continue;
|
|
1105
|
+
const v = Number(p.value);
|
|
1106
|
+
if (!Number.isFinite(v) || v <= 0)
|
|
1107
|
+
continue;
|
|
1108
|
+
if (max === undefined || v > max)
|
|
1109
|
+
max = v;
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
return max;
|
|
1113
|
+
}
|
|
1114
|
+
/** 지금 쓰는 요금적용전력 — **청구서가 먼저다.** 매달 바뀌는 값이라 선언은 곧 낡는다. */
|
|
1115
|
+
billingDemandKW() {
|
|
1116
|
+
/* 지금 적용되는 기준이 먼저다 — 청구서는 끝난 기간의 것이라 이번 주기를 말하지 않는다. */
|
|
1117
|
+
const basis = Number(this.tariffBasis?.billingDemandKW);
|
|
1118
|
+
if (Number.isFinite(basis) && basis > 0)
|
|
1119
|
+
return basis;
|
|
1120
|
+
const billed = Number(this.lastBill?.billingDemandKW);
|
|
1121
|
+
if (Number.isFinite(billed) && billed > 0)
|
|
1122
|
+
return billed;
|
|
1123
|
+
return this.declaredBillingDemandKW();
|
|
1124
|
+
}
|
|
991
1125
|
/** 계약을 선언한 수전 자리 — 주의가 가리킬 대상. 없으면 대상을 지어내지 않는다. */
|
|
992
1126
|
incomingLocationId() {
|
|
993
1127
|
for (const loc of this.boardDef?.locations ?? []) {
|
|
@@ -1381,6 +1515,11 @@ export class EmsKernel extends FlowEngine {
|
|
|
1381
1515
|
/* 청구서도 이어받는다 — 재기동마다 사라지면 우리 계산과 견줄 기준이 없어진다. */
|
|
1382
1516
|
if (e.lastBill?.from && e.lastBill?.to)
|
|
1383
1517
|
this.lastBill = { ...e.lastBill };
|
|
1518
|
+
if (e.tariffBasis?.from && e.tariffBasis?.to)
|
|
1519
|
+
this.tariffBasis = { ...e.tariffBasis };
|
|
1520
|
+
/* 이 주기의 최고도 이어받는다 — 재기동마다 0 으로 돌아가면 이미 넘은 것을 못 넘었다고 말한다. */
|
|
1521
|
+
if (Number.isFinite(Number(e.demandHigh?.kW)) && e.demandHigh?.since)
|
|
1522
|
+
this.demandHigh = { ...e.demandHigh };
|
|
1384
1523
|
}
|
|
1385
1524
|
getSnapshot() {
|
|
1386
1525
|
const snap = super.getSnapshot();
|
|
@@ -1406,6 +1545,8 @@ export class EmsKernel extends FlowEngine {
|
|
|
1406
1545
|
: {}),
|
|
1407
1546
|
...(this.unclosedGenerationPeriods ? { unclosedGenerationPeriods: this.unclosedGenerationPeriods } : {}),
|
|
1408
1547
|
...(this.lastBill ? { lastBill: { ...this.lastBill } } : {}),
|
|
1548
|
+
...(this.tariffBasis ? { tariffBasis: { ...this.tariffBasis } } : {}),
|
|
1549
|
+
...(this.demandHigh ? { demandHigh: { ...this.demandHigh } } : {}),
|
|
1409
1550
|
...(contractKW !== undefined ? { contractKW } : {}),
|
|
1410
1551
|
/* 물류 흐름 요청을 받은 적이 있나 — 있으면 이 트윈에 엉뚱한 명령이 오고 있다는 사실이다. */
|
|
1411
1552
|
...(this.flowRequests.size
|
package/dist/energy-ingest.d.ts
CHANGED
|
@@ -115,6 +115,22 @@ export interface EnergyUsagePeriodRecord {
|
|
|
115
115
|
basis?: string;
|
|
116
116
|
}
|
|
117
117
|
export declare function isEnergyUsagePeriodRecord(record: unknown): boolean;
|
|
118
|
+
/** 이 주기에 적용되는 요금 기준 — 공급자가 정하고 매 주기 다시 정해진다. */
|
|
119
|
+
export interface EnergyTariffBasisRecord {
|
|
120
|
+
from: string;
|
|
121
|
+
to: string;
|
|
122
|
+
billingDemandKW?: number;
|
|
123
|
+
demandChargePerKW?: number;
|
|
124
|
+
currency?: string;
|
|
125
|
+
}
|
|
126
|
+
export declare function isEnergyTariffBasisRecord(record: unknown): boolean;
|
|
127
|
+
/**
|
|
128
|
+
* 요금 기준을 봉투로 — **사건 시각은 `from`** 이다.
|
|
129
|
+
*
|
|
130
|
+
* 「이 시각부터 이 기준이 적용된다」이므로 주기가 끝나기 전에 성립한다. `to` 로 잡으면 아직 오지 않은
|
|
131
|
+
* 시각이 저널에 적힌다(실제로 그렇게 해서 9월 1일 사건이 8월 29일에 적힌 적이 있다).
|
|
132
|
+
*/
|
|
133
|
+
export declare function ingestEnergyTariffBasisRecords(records: EnergyTariffBasisRecord | EnergyTariffBasisRecord[] | undefined | null, opts: EnergyIngestOptions): EnergyIngestResult;
|
|
118
134
|
/** 청구서 — 커넥터가 커널의 요금 어휘로 옮겨 보낸다. */
|
|
119
135
|
export interface EnergyBillRecord {
|
|
120
136
|
from: string;
|
package/dist/energy-ingest.js
CHANGED
|
@@ -378,6 +378,63 @@ export function isEnergyUsagePeriodRecord(record) {
|
|
|
378
378
|
const r = record;
|
|
379
379
|
return !!r && typeof r === 'object' && r.meterId !== undefined && r.from !== undefined && r.to !== undefined && r.kWh !== undefined;
|
|
380
380
|
}
|
|
381
|
+
export function isEnergyTariffBasisRecord(record) {
|
|
382
|
+
const r = record;
|
|
383
|
+
if (!r || typeof r !== 'object')
|
|
384
|
+
return false;
|
|
385
|
+
if (r.from === undefined || r.to === undefined)
|
|
386
|
+
return false;
|
|
387
|
+
return r.billingDemandKW !== undefined || r.demandChargePerKW !== undefined;
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* 요금 기준을 봉투로 — **사건 시각은 `from`** 이다.
|
|
391
|
+
*
|
|
392
|
+
* 「이 시각부터 이 기준이 적용된다」이므로 주기가 끝나기 전에 성립한다. `to` 로 잡으면 아직 오지 않은
|
|
393
|
+
* 시각이 저널에 적힌다(실제로 그렇게 해서 9월 1일 사건이 8월 29일에 적힌 적이 있다).
|
|
394
|
+
*/
|
|
395
|
+
export function ingestEnergyTariffBasisRecords(records, opts) {
|
|
396
|
+
const list = records === undefined || records === null ? [] : Array.isArray(records) ? records : [records];
|
|
397
|
+
const accepted = [];
|
|
398
|
+
const rejected = [];
|
|
399
|
+
let seq = 0;
|
|
400
|
+
for (const r of list) {
|
|
401
|
+
const errors = [];
|
|
402
|
+
const span = readSpan(r, errors);
|
|
403
|
+
const billingDemandKW = readAmount(r?.billingDemandKW, 'billingDemandKW', errors);
|
|
404
|
+
const demandChargePerKW = readAmount(r?.demandChargePerKW, 'demandChargePerKW', errors);
|
|
405
|
+
if (billingDemandKW === undefined && demandChargePerKW === undefined) {
|
|
406
|
+
errors.push('요금 기준이 하나도 없다 — 적을 사실이 없는 기준은 받지 않는다');
|
|
407
|
+
}
|
|
408
|
+
/* 단가를 실었으면 통화가 있어야 한다 — 단위 없는 금액은 다른 금액과 더할 수 없다. */
|
|
409
|
+
const currency = String(r?.currency ?? '').trim() || undefined;
|
|
410
|
+
if (demandChargePerKW !== undefined && !currency)
|
|
411
|
+
errors.push('demandChargePerKW 를 실었는데 currency 가 없다');
|
|
412
|
+
/* 금액은 받지 않는다 — 두 값의 곱은 커널이 한다. 곱을 받으면 그것이 정산인 척한다. */
|
|
413
|
+
if (r?.demandCharge !== undefined || r?.total !== undefined) {
|
|
414
|
+
errors.push('금액(demandCharge·total)은 이 문이 받지 않는다 — 정산 금액은 청구서 문으로 보낸다');
|
|
415
|
+
}
|
|
416
|
+
if (errors.length || !span) {
|
|
417
|
+
rejected.push({ record: r, errors });
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
const data = {
|
|
421
|
+
from: span.from,
|
|
422
|
+
to: span.to,
|
|
423
|
+
...(billingDemandKW !== undefined ? { billingDemandKW } : {}),
|
|
424
|
+
...(demandChargePerKW !== undefined ? { demandChargePerKW } : {}),
|
|
425
|
+
...(currency ? { currency } : {})
|
|
426
|
+
};
|
|
427
|
+
accepted.push({
|
|
428
|
+
eventId: `${opts.tenantId}-tariff-basis-${++seq}`,
|
|
429
|
+
eventType: ENERGY_EVENT.tariffBasis,
|
|
430
|
+
/* **시작 시각**이다 — 그때부터 유효하다(§ 위 주석). */
|
|
431
|
+
eventTime: span.from,
|
|
432
|
+
tenantId: opts.tenantId,
|
|
433
|
+
data
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
return { accepted, rejected };
|
|
437
|
+
}
|
|
381
438
|
export function isEnergyBillRecord(record) {
|
|
382
439
|
const r = record;
|
|
383
440
|
if (!r || typeof r !== 'object')
|
package/dist/index.d.ts
CHANGED
|
@@ -30,10 +30,10 @@ export { WmsKernel } from './kernel.ts';
|
|
|
30
30
|
export { YmsKernel } from './yms-kernel.ts';
|
|
31
31
|
export { MesKernel } from './mes-kernel.ts';
|
|
32
32
|
export { EmsKernel, DEMAND_WINDOW_MS, demandWindowStart } from './ems-kernel.ts';
|
|
33
|
-
export { ingestEnergyRecords, isEnergyRecord, ingestEnergyEquipmentRecords, isEnergyEquipmentRecord, ingestEnergyGenerationRecords, isEnergyGenerationRecord, ingestEnergyUsagePeriodRecords, isEnergyUsagePeriodRecord, ingestEnergyBillRecords, isEnergyBillRecord } from './energy-ingest.ts';
|
|
33
|
+
export { ingestEnergyRecords, isEnergyRecord, ingestEnergyEquipmentRecords, isEnergyEquipmentRecord, ingestEnergyGenerationRecords, isEnergyGenerationRecord, ingestEnergyUsagePeriodRecords, isEnergyUsagePeriodRecord, ingestEnergyBillRecords, isEnergyBillRecord, ingestEnergyTariffBasisRecords, isEnergyTariffBasisRecord } from './energy-ingest.ts';
|
|
34
34
|
export { ingestOperationalRecords, isOperationalRecord, operationalKindOf } from './operational-ingest.ts';
|
|
35
35
|
export type { OperationalKind, OperationalRecord, OperationalIngestOptions } from './operational-ingest.ts';
|
|
36
36
|
export { attributeEnergy, electricityCost, energyIntensity, energyOfWindows } from './energy-attribution.ts';
|
|
37
37
|
export type { AttributionBasis, AttributionResult, ElectricityCost, EnergyConsumer, EnergyPool, EnergyShare, IntensityInput, IntensityResult, IntensityDenominator, TariffDeclaration, WeightKind, WindowedEnergy } from './energy-attribution.ts';
|
|
38
|
-
export type { EnergyRecord, EnergyEquipmentRecord, EnergyGenerationRecord, EnergyUsagePeriodRecord, EnergyBillRecord, EnergyIngestOptions, EnergyIngestResult } from './energy-ingest.ts';
|
|
38
|
+
export type { EnergyRecord, EnergyEquipmentRecord, EnergyGenerationRecord, EnergyUsagePeriodRecord, EnergyBillRecord, EnergyTariffBasisRecord, EnergyIngestOptions, EnergyIngestResult } from './energy-ingest.ts';
|
|
39
39
|
export * from './vocabulary.ts';
|
package/dist/index.js
CHANGED
|
@@ -30,7 +30,7 @@ export { WmsKernel } from "./kernel.js";
|
|
|
30
30
|
export { YmsKernel } from "./yms-kernel.js";
|
|
31
31
|
export { MesKernel } from "./mes-kernel.js";
|
|
32
32
|
export { EmsKernel, DEMAND_WINDOW_MS, demandWindowStart } from "./ems-kernel.js";
|
|
33
|
-
export { ingestEnergyRecords, isEnergyRecord, ingestEnergyEquipmentRecords, isEnergyEquipmentRecord, ingestEnergyGenerationRecords, isEnergyGenerationRecord, ingestEnergyUsagePeriodRecords, isEnergyUsagePeriodRecord, ingestEnergyBillRecords, isEnergyBillRecord } from "./energy-ingest.js";
|
|
33
|
+
export { ingestEnergyRecords, isEnergyRecord, ingestEnergyEquipmentRecords, isEnergyEquipmentRecord, ingestEnergyGenerationRecords, isEnergyGenerationRecord, ingestEnergyUsagePeriodRecords, isEnergyUsagePeriodRecord, ingestEnergyBillRecords, isEnergyBillRecord, ingestEnergyTariffBasisRecords, isEnergyTariffBasisRecord } from "./energy-ingest.js";
|
|
34
34
|
/* 운영 사실의 문 — 리듀서가 다루는 여섯이 들어오는 자리(미러가 시뮬보다 가난하지 않게). */
|
|
35
35
|
export { ingestOperationalRecords, isOperationalRecord, operationalKindOf } from "./operational-ingest.js";
|
|
36
36
|
export { attributeEnergy, electricityCost, energyIntensity, energyOfWindows } from "./energy-attribution.js";
|
package/dist-cjs/index.cjs
CHANGED
|
@@ -129,6 +129,7 @@ __export(index_exports, {
|
|
|
129
129
|
ingestEnergyEquipmentRecords: () => ingestEnergyEquipmentRecords,
|
|
130
130
|
ingestEnergyGenerationRecords: () => ingestEnergyGenerationRecords,
|
|
131
131
|
ingestEnergyRecords: () => ingestEnergyRecords,
|
|
132
|
+
ingestEnergyTariffBasisRecords: () => ingestEnergyTariffBasisRecords,
|
|
132
133
|
ingestEnergyUsagePeriodRecords: () => ingestEnergyUsagePeriodRecords,
|
|
133
134
|
ingestMasterData: () => ingestMasterData,
|
|
134
135
|
ingestOperationalRecords: () => ingestOperationalRecords,
|
|
@@ -138,6 +139,7 @@ __export(index_exports, {
|
|
|
138
139
|
isEnergyEquipmentRecord: () => isEnergyEquipmentRecord,
|
|
139
140
|
isEnergyGenerationRecord: () => isEnergyGenerationRecord,
|
|
140
141
|
isEnergyRecord: () => isEnergyRecord,
|
|
142
|
+
isEnergyTariffBasisRecord: () => isEnergyTariffBasisRecord,
|
|
141
143
|
isEnergyUsagePeriodRecord: () => isEnergyUsagePeriodRecord,
|
|
142
144
|
isEpcisEventType: () => isEpcisEventType,
|
|
143
145
|
isEquipmentLevel: () => isEquipmentLevel,
|
|
@@ -709,6 +711,18 @@ var ENERGY_EVENT = {
|
|
|
709
711
|
* 쪽인지 말한다.
|
|
710
712
|
*/
|
|
711
713
|
usagePeriod: "energy.usage.period",
|
|
714
|
+
/**
|
|
715
|
+
* **이 주기에 적용되는 요금 기준이 정해졌다** — 요금적용전력과 기본요금 단가.
|
|
716
|
+
*
|
|
717
|
+
* 청구서와 다른 사건이다. 청구서는 끝난 기간의 정산이고 이것은 **지금 적용되는 기준**이다.
|
|
718
|
+
* 그래서 사건 시각도 다르다 — 청구서는 기간의 끝, 이것은 기간의 시작이다(그때부터 유효하다).
|
|
719
|
+
*
|
|
720
|
+
* 이 값은 매 주기 다시 정해진다. 모델의 선언에 적어 두면 사람이 매달 갱신해야 하고, 안 하면 낡은
|
|
721
|
+
* 값으로 기본요금이 조용히 틀린다. 공급자가 알려 주는 값을 그대로 받는 것이 맞다.
|
|
722
|
+
*
|
|
723
|
+
* **금액을 싣지 않는다.** 요금적용전력 × 단가는 커널이 계산한다. 곱을 받으면 그것이 정산인 척한다.
|
|
724
|
+
*/
|
|
725
|
+
tariffBasis: "energy.tariff.basis",
|
|
712
726
|
/**
|
|
713
727
|
* **청구서가 도착했다** — 공급자가 확정한 금액.
|
|
714
728
|
*
|
|
@@ -7950,6 +7964,10 @@ var EmsKernel = class extends FlowEngine {
|
|
|
7950
7964
|
this.applyBill(envelope);
|
|
7951
7965
|
return;
|
|
7952
7966
|
}
|
|
7967
|
+
if (envelope.eventType === ENERGY_EVENT.tariffBasis) {
|
|
7968
|
+
this.applyTariffBasis(envelope);
|
|
7969
|
+
return;
|
|
7970
|
+
}
|
|
7953
7971
|
super.apply(envelope);
|
|
7954
7972
|
}
|
|
7955
7973
|
/**
|
|
@@ -7980,6 +7998,30 @@ var EmsKernel = class extends FlowEngine {
|
|
|
7980
7998
|
if (d?.from) point.lastPeriodFrom = d.from;
|
|
7981
7999
|
if (d?.to) point.lastPeriodTo = d.to;
|
|
7982
8000
|
if (d?.basis) point.lastPeriodBasis = d.basis;
|
|
8001
|
+
const maxKW = Number(d?.maxKW);
|
|
8002
|
+
if (Number.isFinite(maxKW) && maxKW >= 0 && d?.to) {
|
|
8003
|
+
const periodStart = this.lastBill?.to;
|
|
8004
|
+
if (this.demandHigh && periodStart && this.demandHigh.periodStart !== periodStart) this.demandHigh = void 0;
|
|
8005
|
+
if (!this.demandHigh || maxKW > this.demandHigh.kW) {
|
|
8006
|
+
this.demandHigh = { kW: maxKW, atTo: String(d.to), periodStart, since: this.demandHigh?.since ?? String(d.to) };
|
|
8007
|
+
}
|
|
8008
|
+
}
|
|
8009
|
+
this.revision++;
|
|
8010
|
+
}
|
|
8011
|
+
/**
|
|
8012
|
+
* 이 주기에 적용되는 요금 기준을 상태에 둔다.
|
|
8013
|
+
*
|
|
8014
|
+
* 늦게 온 지난 주기의 기준이 최신을 덮지 않게 시작 시각으로 가린다. 같은 주기의 값이 다시 오면
|
|
8015
|
+
* 새것을 쓴다 — 사업자가 그 주기의 기준을 고칠 수 있다.
|
|
8016
|
+
*/
|
|
8017
|
+
applyTariffBasis(envelope) {
|
|
8018
|
+
const d = envelope.data;
|
|
8019
|
+
if (!d?.from || !d?.to) return;
|
|
8020
|
+
const fromMs = Date.parse(String(d.from));
|
|
8021
|
+
if (!Number.isFinite(fromMs)) return;
|
|
8022
|
+
const priorFromMs = Date.parse(String(this.tariffBasis?.from ?? ""));
|
|
8023
|
+
if (Number.isFinite(priorFromMs) && fromMs < priorFromMs) return;
|
|
8024
|
+
this.tariffBasis = { ...d };
|
|
7983
8025
|
this.revision++;
|
|
7984
8026
|
}
|
|
7985
8027
|
/**
|
|
@@ -8156,8 +8198,17 @@ var EmsKernel = class extends FlowEngine {
|
|
|
8156
8198
|
generationPeriodObs = /* @__PURE__ */ new Map();
|
|
8157
8199
|
/** 기준점이 없어 마감하지 못한 기간 누적의 수 — 조용히 사라지지 않게 센다. */
|
|
8158
8200
|
unclosedGenerationPeriods = 0;
|
|
8159
|
-
/** 마지막으로 받은 청구서 — 공급자가
|
|
8201
|
+
/** 마지막으로 받은 청구서 — 공급자가 **정산한** 금액. 지금 이 문으로 오는 것은 없다. */
|
|
8160
8202
|
lastBill;
|
|
8203
|
+
/** 마지막으로 받은 요금 기준 — 이 주기에 적용되는 요금적용전력과 단가. */
|
|
8204
|
+
tariffBasis;
|
|
8205
|
+
/**
|
|
8206
|
+
* 이 청구 주기의 **최고 수요** — 기본요금이 이 값으로 다시 정해진다.
|
|
8207
|
+
*
|
|
8208
|
+
* `since` 는 이 주기에서 처음 잰 구간의 끝이다. `periodStart` 는 마지막 청구서의 끝이고,
|
|
8209
|
+
* 그것이 바뀌면 처음부터 다시 센다.
|
|
8210
|
+
*/
|
|
8211
|
+
demandHigh;
|
|
8161
8212
|
applyEquipmentEnergy(envelope) {
|
|
8162
8213
|
const d = envelope.data;
|
|
8163
8214
|
const id = String(d?.equipmentId ?? "").trim();
|
|
@@ -8397,6 +8448,8 @@ var EmsKernel = class extends FlowEngine {
|
|
|
8397
8448
|
*/
|
|
8398
8449
|
collectAttentions() {
|
|
8399
8450
|
const out = super.collectAttentions();
|
|
8451
|
+
const billingOver = this.billingDemandAttention();
|
|
8452
|
+
if (billingOver) out.push(billingOver);
|
|
8400
8453
|
const w = this.open;
|
|
8401
8454
|
if (!w || w.contractKW === void 0 || w.meanKW === void 0) return out;
|
|
8402
8455
|
const projected = w.meanKW;
|
|
@@ -8435,6 +8488,74 @@ var EmsKernel = class extends FlowEngine {
|
|
|
8435
8488
|
});
|
|
8436
8489
|
return out;
|
|
8437
8490
|
}
|
|
8491
|
+
/**
|
|
8492
|
+
* **이 주기의 최고가 요금적용전력을 넘었다** — 다음 주기의 기본요금이 오른다.
|
|
8493
|
+
*
|
|
8494
|
+
* ── 왜 계약 초과와 다른 신호인가 (2026-08-29) ────────────────────────────
|
|
8495
|
+
* 계약 초과는 약정 위반이고 지금 조치할 일이다(줄이면 된다). 이쪽은 **이미 일어난 일**이다 —
|
|
8496
|
+
* 넘은 순간 다음 주기의 기준이 정해지고, 지금 줄여도 되돌아가지 않는다. 사람이 할 일이 다르므로
|
|
8497
|
+
* 신호도 다르다.
|
|
8498
|
+
*
|
|
8499
|
+
* ── 한 번만 울린다 ──────────────────────────────────────────────────────────
|
|
8500
|
+
* 넘으면 그 주기 내내 참이다. 구간마다 다시 울리면 소음이 되고, 소음이 되면 아무도 안 본다.
|
|
8501
|
+
* 그래서 신호의 id 를 주기로 묶고 「처음 넘은 시각」이 아니라 **그 주기의 최고**를 싣는다.
|
|
8502
|
+
*
|
|
8503
|
+
* ── 늦게 안다 ───────────────────────────────────────────────────────────────
|
|
8504
|
+
* 최고 수요는 지난 구간이 마감되어야 온다(지금 원본은 시간마다). 그래서 넘은 것을 최대 한 시간
|
|
8505
|
+
* 뒤에 안다. 그 사실을 `observedTo` 로 함께 내어 화면이 「언제까지의 값인가」를 말할 수 있게 한다.
|
|
8506
|
+
*/
|
|
8507
|
+
billingDemandAttention() {
|
|
8508
|
+
const high = this.demandHigh;
|
|
8509
|
+
const basis = this.billingDemandKW();
|
|
8510
|
+
if (!high || basis === void 0 || high.kW <= basis) return void 0;
|
|
8511
|
+
const over = high.kW - basis;
|
|
8512
|
+
const ratio = over / basis;
|
|
8513
|
+
return {
|
|
8514
|
+
id: `billing-demand-exceeded:${high.periodStart ?? high.since}`,
|
|
8515
|
+
kind: "billing-demand-exceeded",
|
|
8516
|
+
severity: ratio > 0.15 ? "high" : "medium",
|
|
8517
|
+
anchor: { locationId: this.incomingLocationId() },
|
|
8518
|
+
params: {
|
|
8519
|
+
peakKW: Math.round(high.kW),
|
|
8520
|
+
billingDemandKW: Math.round(basis),
|
|
8521
|
+
overKW: Math.round(over),
|
|
8522
|
+
/* 값의 출처 — 청구서가 말한 기준인지 현장의 선언인지. 선언은 곧 낡는다. */
|
|
8523
|
+
basisFrom: Number.isFinite(Number(this.tariffBasis?.billingDemandKW)) ? "tariff-basis" : Number.isFinite(Number(this.lastBill?.billingDemandKW)) ? "bill" : "declaration",
|
|
8524
|
+
observedTo: high.atTo,
|
|
8525
|
+
periodSince: high.since
|
|
8526
|
+
},
|
|
8527
|
+
/*
|
|
8528
|
+
* 조치를 걸지 않는다. **이미 일어난 일이라 되돌릴 수 없다** — 지금 줄여도 이 주기의 최고는
|
|
8529
|
+
* 그대로다. 조치를 붙이면 화면이 「누르면 해결된다」고 거짓말한다.
|
|
8530
|
+
*/
|
|
8531
|
+
recommendedActions: []
|
|
8532
|
+
};
|
|
8533
|
+
}
|
|
8534
|
+
/**
|
|
8535
|
+
* 선언된 **요금적용전력** — 기본요금이 걸리는 기준(§`EMS_PROPERTY.billingDemandKW`).
|
|
8536
|
+
*
|
|
8537
|
+
* 청구서가 그 값을 실어 오면 그쪽이 먼저다(§`billingDemandKW()`). 이것은 청구가 오지 않는 현장의 몫이다.
|
|
8538
|
+
*/
|
|
8539
|
+
declaredBillingDemandKW() {
|
|
8540
|
+
let max;
|
|
8541
|
+
for (const loc of this.boardDef?.locations ?? []) {
|
|
8542
|
+
for (const p of loc.properties ?? []) {
|
|
8543
|
+
if (p.id !== EMS_PROPERTY.billingDemandKW) continue;
|
|
8544
|
+
const v = Number(p.value);
|
|
8545
|
+
if (!Number.isFinite(v) || v <= 0) continue;
|
|
8546
|
+
if (max === void 0 || v > max) max = v;
|
|
8547
|
+
}
|
|
8548
|
+
}
|
|
8549
|
+
return max;
|
|
8550
|
+
}
|
|
8551
|
+
/** 지금 쓰는 요금적용전력 — **청구서가 먼저다.** 매달 바뀌는 값이라 선언은 곧 낡는다. */
|
|
8552
|
+
billingDemandKW() {
|
|
8553
|
+
const basis = Number(this.tariffBasis?.billingDemandKW);
|
|
8554
|
+
if (Number.isFinite(basis) && basis > 0) return basis;
|
|
8555
|
+
const billed = Number(this.lastBill?.billingDemandKW);
|
|
8556
|
+
if (Number.isFinite(billed) && billed > 0) return billed;
|
|
8557
|
+
return this.declaredBillingDemandKW();
|
|
8558
|
+
}
|
|
8438
8559
|
/** 계약을 선언한 수전 자리 — 주의가 가리킬 대상. 없으면 대상을 지어내지 않는다. */
|
|
8439
8560
|
incomingLocationId() {
|
|
8440
8561
|
for (const loc of this.boardDef?.locations ?? []) {
|
|
@@ -8715,6 +8836,8 @@ var EmsKernel = class extends FlowEngine {
|
|
|
8715
8836
|
}
|
|
8716
8837
|
if (Number.isFinite(e.unclosedGenerationPeriods)) this.unclosedGenerationPeriods = Number(e.unclosedGenerationPeriods);
|
|
8717
8838
|
if (e.lastBill?.from && e.lastBill?.to) this.lastBill = { ...e.lastBill };
|
|
8839
|
+
if (e.tariffBasis?.from && e.tariffBasis?.to) this.tariffBasis = { ...e.tariffBasis };
|
|
8840
|
+
if (Number.isFinite(Number(e.demandHigh?.kW)) && e.demandHigh?.since) this.demandHigh = { ...e.demandHigh };
|
|
8718
8841
|
}
|
|
8719
8842
|
getSnapshot() {
|
|
8720
8843
|
const snap = super.getSnapshot();
|
|
@@ -8736,6 +8859,8 @@ var EmsKernel = class extends FlowEngine {
|
|
|
8736
8859
|
} : {},
|
|
8737
8860
|
...this.unclosedGenerationPeriods ? { unclosedGenerationPeriods: this.unclosedGenerationPeriods } : {},
|
|
8738
8861
|
...this.lastBill ? { lastBill: { ...this.lastBill } } : {},
|
|
8862
|
+
...this.tariffBasis ? { tariffBasis: { ...this.tariffBasis } } : {},
|
|
8863
|
+
...this.demandHigh ? { demandHigh: { ...this.demandHigh } } : {},
|
|
8739
8864
|
...contractKW !== void 0 ? { contractKW } : {},
|
|
8740
8865
|
/* 물류 흐름 요청을 받은 적이 있나 — 있으면 이 트윈에 엉뚱한 명령이 오고 있다는 사실이다. */
|
|
8741
8866
|
...this.flowRequests.size ? { flowRequests: [...this.flowRequests.entries()].map(([hook, count]) => ({ hook, count })) } : {}
|
|
@@ -9781,6 +9906,52 @@ function isEnergyUsagePeriodRecord(record) {
|
|
|
9781
9906
|
const r = record;
|
|
9782
9907
|
return !!r && typeof r === "object" && r.meterId !== void 0 && r.from !== void 0 && r.to !== void 0 && r.kWh !== void 0;
|
|
9783
9908
|
}
|
|
9909
|
+
function isEnergyTariffBasisRecord(record) {
|
|
9910
|
+
const r = record;
|
|
9911
|
+
if (!r || typeof r !== "object") return false;
|
|
9912
|
+
if (r.from === void 0 || r.to === void 0) return false;
|
|
9913
|
+
return r.billingDemandKW !== void 0 || r.demandChargePerKW !== void 0;
|
|
9914
|
+
}
|
|
9915
|
+
function ingestEnergyTariffBasisRecords(records, opts) {
|
|
9916
|
+
const list = records === void 0 || records === null ? [] : Array.isArray(records) ? records : [records];
|
|
9917
|
+
const accepted = [];
|
|
9918
|
+
const rejected = [];
|
|
9919
|
+
let seq = 0;
|
|
9920
|
+
for (const r of list) {
|
|
9921
|
+
const errors = [];
|
|
9922
|
+
const span = readSpan(r, errors);
|
|
9923
|
+
const billingDemandKW = readAmount(r?.billingDemandKW, "billingDemandKW", errors);
|
|
9924
|
+
const demandChargePerKW = readAmount(r?.demandChargePerKW, "demandChargePerKW", errors);
|
|
9925
|
+
if (billingDemandKW === void 0 && demandChargePerKW === void 0) {
|
|
9926
|
+
errors.push("\uC694\uAE08 \uAE30\uC900\uC774 \uD558\uB098\uB3C4 \uC5C6\uB2E4 \u2014 \uC801\uC744 \uC0AC\uC2E4\uC774 \uC5C6\uB294 \uAE30\uC900\uC740 \uBC1B\uC9C0 \uC54A\uB294\uB2E4");
|
|
9927
|
+
}
|
|
9928
|
+
const currency = String(r?.currency ?? "").trim() || void 0;
|
|
9929
|
+
if (demandChargePerKW !== void 0 && !currency) errors.push("demandChargePerKW \uB97C \uC2E4\uC5C8\uB294\uB370 currency \uAC00 \uC5C6\uB2E4");
|
|
9930
|
+
if (r?.demandCharge !== void 0 || r?.total !== void 0) {
|
|
9931
|
+
errors.push("\uAE08\uC561(demandCharge\xB7total)\uC740 \uC774 \uBB38\uC774 \uBC1B\uC9C0 \uC54A\uB294\uB2E4 \u2014 \uC815\uC0B0 \uAE08\uC561\uC740 \uCCAD\uAD6C\uC11C \uBB38\uC73C\uB85C \uBCF4\uB0B8\uB2E4");
|
|
9932
|
+
}
|
|
9933
|
+
if (errors.length || !span) {
|
|
9934
|
+
rejected.push({ record: r, errors });
|
|
9935
|
+
continue;
|
|
9936
|
+
}
|
|
9937
|
+
const data = {
|
|
9938
|
+
from: span.from,
|
|
9939
|
+
to: span.to,
|
|
9940
|
+
...billingDemandKW !== void 0 ? { billingDemandKW } : {},
|
|
9941
|
+
...demandChargePerKW !== void 0 ? { demandChargePerKW } : {},
|
|
9942
|
+
...currency ? { currency } : {}
|
|
9943
|
+
};
|
|
9944
|
+
accepted.push({
|
|
9945
|
+
eventId: `${opts.tenantId}-tariff-basis-${++seq}`,
|
|
9946
|
+
eventType: ENERGY_EVENT.tariffBasis,
|
|
9947
|
+
/* **시작 시각**이다 — 그때부터 유효하다(§ 위 주석). */
|
|
9948
|
+
eventTime: span.from,
|
|
9949
|
+
tenantId: opts.tenantId,
|
|
9950
|
+
data
|
|
9951
|
+
});
|
|
9952
|
+
}
|
|
9953
|
+
return { accepted, rejected };
|
|
9954
|
+
}
|
|
9784
9955
|
function isEnergyBillRecord(record) {
|
|
9785
9956
|
const r = record;
|
|
9786
9957
|
if (!r || typeof r !== "object") return false;
|
|
@@ -10598,6 +10769,7 @@ function retiredVocabularyIn(line) {
|
|
|
10598
10769
|
ingestEnergyEquipmentRecords,
|
|
10599
10770
|
ingestEnergyGenerationRecords,
|
|
10600
10771
|
ingestEnergyRecords,
|
|
10772
|
+
ingestEnergyTariffBasisRecords,
|
|
10601
10773
|
ingestEnergyUsagePeriodRecords,
|
|
10602
10774
|
ingestMasterData,
|
|
10603
10775
|
ingestOperationalRecords,
|
|
@@ -10607,6 +10779,7 @@ function retiredVocabularyIn(line) {
|
|
|
10607
10779
|
isEnergyEquipmentRecord,
|
|
10608
10780
|
isEnergyGenerationRecord,
|
|
10609
10781
|
isEnergyRecord,
|
|
10782
|
+
isEnergyTariffBasisRecord,
|
|
10610
10783
|
isEnergyUsagePeriodRecord,
|
|
10611
10784
|
isEpcisEventType,
|
|
10612
10785
|
isEquipmentLevel,
|
package/package.json
CHANGED