@operato/twin-kernel 0.7.78 → 0.7.80
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 +36 -0
- package/dist/contract.js +17 -0
- package/dist/ems-kernel.d.ts +15 -0
- package/dist/ems-kernel.js +68 -0
- package/dist/energy-ingest.d.ts +15 -0
- package/dist/energy-ingest.js +47 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist-cjs/index.cjs +102 -0
- package/package.json +1 -1
package/dist/contract.d.ts
CHANGED
|
@@ -2052,6 +2052,12 @@ export interface EnergyState {
|
|
|
2052
2052
|
basis: 'ac' | 'dc';
|
|
2053
2053
|
from: 'equipment' | 'location';
|
|
2054
2054
|
};
|
|
2055
|
+
/**
|
|
2056
|
+
* 마지막으로 받은 **발전 단가** — 이 기간에 낸 전기 1kWh 의 값.
|
|
2057
|
+
*
|
|
2058
|
+
* 창 밖의 단가도 이 창에 적용될 수 있어(단가는 기간마다 정해진다) 상태로 이어 간다.
|
|
2059
|
+
*/
|
|
2060
|
+
generationPrice?: EnergyGenerationPriceData;
|
|
2055
2061
|
/**
|
|
2056
2062
|
* 이 청구 주기의 **최고 수요** — 기본요금이 이 값으로 다시 정해진다.
|
|
2057
2063
|
*
|
|
@@ -2491,6 +2497,23 @@ export declare const ENERGY_EVENT: {
|
|
|
2491
2497
|
* 쪽인지 말한다.
|
|
2492
2498
|
*/
|
|
2493
2499
|
readonly usagePeriod: "energy.usage.period";
|
|
2500
|
+
/**
|
|
2501
|
+
* **발전에 매겨지는 단가가 정해졌다** — 이 기간에 낸 전기 1kWh 의 값.
|
|
2502
|
+
*
|
|
2503
|
+
* 소비에 요금이 매겨지듯 발전에도 값이 매겨진다. 파는 발전소는 판 값이고, 자가소비형은 아꼈다고
|
|
2504
|
+
* 볼 수 있는 값이다 — 어느 쪽인지는 현장이 정한다.
|
|
2505
|
+
*
|
|
2506
|
+
* **단가는 날마다 바뀐다.** 그래서 선언이 아니라 기간을 가진 사실로 받는다. 선언에 적어 두면
|
|
2507
|
+
* 사람이 매일 갱신해야 하고, 안 하면 낡은 값으로 수익이 조용히 틀린다.
|
|
2508
|
+
*
|
|
2509
|
+
* 사건 시각은 기간의 시작이다 — 「이 시각부터 이 단가가 적용된다」이므로 기간이 끝나기 전에 성립한다.
|
|
2510
|
+
*
|
|
2511
|
+
* ── 담지 않은 것 ────────────────────────────────────────────────────────────
|
|
2512
|
+
* 제도마다 있는 가산·인증서·가중치는 담지 않는다. 그 셈은 제도가 정하고 나라마다 다르므로,
|
|
2513
|
+
* 커넥터가 계산해 **1kWh 당 얼마**로 옮겨 보낸다. 커널이 한 제도의 모양을 안으면 다른 제도에서
|
|
2514
|
+
* 그 자리가 거짓이 된다.
|
|
2515
|
+
*/
|
|
2516
|
+
readonly generationPrice: "energy.generation.price";
|
|
2494
2517
|
/**
|
|
2495
2518
|
* **이 주기에 적용되는 요금 기준이 정해졌다** — 요금적용전력과 기본요금 단가.
|
|
2496
2519
|
*
|
|
@@ -2764,6 +2787,19 @@ export interface EnergyUsagePeriodData {
|
|
|
2764
2787
|
* 사건 시각은 `from` 이다. 「이 시각부터 이 기준이 적용된다」이므로 주기가 끝나기 전에 성립하고,
|
|
2765
2788
|
* `to` 로 시각을 잡으면 아직 오지 않은 시각이 저널에 적힌다.
|
|
2766
2789
|
*/
|
|
2790
|
+
/**
|
|
2791
|
+
* 발전 단가 — `ENERGY_EVENT.generationPrice` 의 데이터.
|
|
2792
|
+
*
|
|
2793
|
+
* 사건 시각은 `from` 이다(그때부터 적용된다). 가산·인증서 같은 제도의 셈은 커넥터가 하고, 여기에는
|
|
2794
|
+
* **1kWh 당 얼마**만 온다.
|
|
2795
|
+
*/
|
|
2796
|
+
export interface EnergyGenerationPriceData {
|
|
2797
|
+
from: ISOTime;
|
|
2798
|
+
to: ISOTime;
|
|
2799
|
+
/** 1kWh 당 값. 통화는 `currency`. */
|
|
2800
|
+
unitPrice: number;
|
|
2801
|
+
currency: string;
|
|
2802
|
+
}
|
|
2767
2803
|
export interface EnergyTariffBasisData {
|
|
2768
2804
|
from: ISOTime;
|
|
2769
2805
|
to: ISOTime;
|
package/dist/contract.js
CHANGED
|
@@ -1046,6 +1046,23 @@ export const ENERGY_EVENT = {
|
|
|
1046
1046
|
* 쪽인지 말한다.
|
|
1047
1047
|
*/
|
|
1048
1048
|
usagePeriod: 'energy.usage.period',
|
|
1049
|
+
/**
|
|
1050
|
+
* **발전에 매겨지는 단가가 정해졌다** — 이 기간에 낸 전기 1kWh 의 값.
|
|
1051
|
+
*
|
|
1052
|
+
* 소비에 요금이 매겨지듯 발전에도 값이 매겨진다. 파는 발전소는 판 값이고, 자가소비형은 아꼈다고
|
|
1053
|
+
* 볼 수 있는 값이다 — 어느 쪽인지는 현장이 정한다.
|
|
1054
|
+
*
|
|
1055
|
+
* **단가는 날마다 바뀐다.** 그래서 선언이 아니라 기간을 가진 사실로 받는다. 선언에 적어 두면
|
|
1056
|
+
* 사람이 매일 갱신해야 하고, 안 하면 낡은 값으로 수익이 조용히 틀린다.
|
|
1057
|
+
*
|
|
1058
|
+
* 사건 시각은 기간의 시작이다 — 「이 시각부터 이 단가가 적용된다」이므로 기간이 끝나기 전에 성립한다.
|
|
1059
|
+
*
|
|
1060
|
+
* ── 담지 않은 것 ────────────────────────────────────────────────────────────
|
|
1061
|
+
* 제도마다 있는 가산·인증서·가중치는 담지 않는다. 그 셈은 제도가 정하고 나라마다 다르므로,
|
|
1062
|
+
* 커넥터가 계산해 **1kWh 당 얼마**로 옮겨 보낸다. 커널이 한 제도의 모양을 안으면 다른 제도에서
|
|
1063
|
+
* 그 자리가 거짓이 된다.
|
|
1064
|
+
*/
|
|
1065
|
+
generationPrice: 'energy.generation.price',
|
|
1049
1066
|
/**
|
|
1050
1067
|
* **이 주기에 적용되는 요금 기준이 정해졌다** — 요금적용전력과 기본요금 단가.
|
|
1051
1068
|
*
|
package/dist/ems-kernel.d.ts
CHANGED
|
@@ -122,6 +122,10 @@ export declare class EmsKernel extends FlowEngine {
|
|
|
122
122
|
* 모르는 지점이면 세어 둔다 — 조용히 버리면 「값이 왜 안 보이지」로만 남는다(계량과 같은 규율).
|
|
123
123
|
*/
|
|
124
124
|
private applyUsagePeriod;
|
|
125
|
+
/**
|
|
126
|
+
* 발전 단가를 상태에 둔다 — 늦게 온 지난 기간의 단가가 최신을 덮지 않게 시작 시각으로 가린다.
|
|
127
|
+
*/
|
|
128
|
+
private applyGenerationPrice;
|
|
125
129
|
/**
|
|
126
130
|
* 이 주기에 적용되는 요금 기준을 상태에 둔다.
|
|
127
131
|
*
|
|
@@ -165,6 +169,15 @@ export declare class EmsKernel extends FlowEngine {
|
|
|
165
169
|
* 않으면 늦게 온 옛 적산이 최신 값을 뒤로 돌린다 — 그리고 그것이 「계기가 되돌아갔다」로 잘못 적힌다.
|
|
166
170
|
*/
|
|
167
171
|
private applyGenerated;
|
|
172
|
+
/**
|
|
173
|
+
* 시계가 기간의 끝을 지났으면 마감한다 — **표본을 기다리지 않는다.**
|
|
174
|
+
*
|
|
175
|
+
* 값이 오지 않는 밤에도 자정이 지나면 어제가 닫힌다. 마감에 쓰는 총량은 **그날 마지막으로 본 값**
|
|
176
|
+
* 이므로 표본이 더 오지 않아도 정확하다(하루 누적은 그 값이 곧 그날의 총량이다).
|
|
177
|
+
*
|
|
178
|
+
* 이 자리는 계량의 `closeDue` 와 같은 성질이다 — 호스트가 시각을 주며 부른다.
|
|
179
|
+
*/
|
|
180
|
+
closeGenerationPeriodsDue(nowMs: number): void;
|
|
168
181
|
/**
|
|
169
182
|
* **기간이 끝났으면 그 기간의 발전량을 사실로 낸다.**
|
|
170
183
|
*
|
|
@@ -208,6 +221,8 @@ export declare class EmsKernel extends FlowEngine {
|
|
|
208
221
|
private lastBill?;
|
|
209
222
|
/** 마지막으로 받은 요금 기준 — 이 주기에 적용되는 요금적용전력과 단가. */
|
|
210
223
|
private tariffBasis?;
|
|
224
|
+
/** 마지막으로 받은 발전 단가 — 이 기간에 낸 전기 1kWh 의 값. */
|
|
225
|
+
private generationPrice?;
|
|
211
226
|
/**
|
|
212
227
|
* 이 청구 주기의 **최고 수요** — 기본요금이 이 값으로 다시 정해진다.
|
|
213
228
|
*
|
package/dist/ems-kernel.js
CHANGED
|
@@ -281,6 +281,10 @@ export class EmsKernel extends FlowEngine {
|
|
|
281
281
|
this.applyTariffBasis(envelope);
|
|
282
282
|
return;
|
|
283
283
|
}
|
|
284
|
+
if (envelope.eventType === ENERGY_EVENT.generationPrice) {
|
|
285
|
+
this.applyGenerationPrice(envelope);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
284
288
|
super.apply(envelope);
|
|
285
289
|
}
|
|
286
290
|
/**
|
|
@@ -341,6 +345,22 @@ export class EmsKernel extends FlowEngine {
|
|
|
341
345
|
}
|
|
342
346
|
this.revision++;
|
|
343
347
|
}
|
|
348
|
+
/**
|
|
349
|
+
* 발전 단가를 상태에 둔다 — 늦게 온 지난 기간의 단가가 최신을 덮지 않게 시작 시각으로 가린다.
|
|
350
|
+
*/
|
|
351
|
+
applyGenerationPrice(envelope) {
|
|
352
|
+
const d = envelope.data;
|
|
353
|
+
if (!d?.from || !d?.to)
|
|
354
|
+
return;
|
|
355
|
+
const fromMs = Date.parse(String(d.from));
|
|
356
|
+
if (!Number.isFinite(fromMs))
|
|
357
|
+
return;
|
|
358
|
+
const priorFromMs = Date.parse(String(this.generationPrice?.from ?? ''));
|
|
359
|
+
if (Number.isFinite(priorFromMs) && fromMs < priorFromMs)
|
|
360
|
+
return;
|
|
361
|
+
this.generationPrice = { ...d };
|
|
362
|
+
this.revision++;
|
|
363
|
+
}
|
|
344
364
|
/**
|
|
345
365
|
* 이 주기에 적용되는 요금 기준을 상태에 둔다.
|
|
346
366
|
*
|
|
@@ -512,6 +532,39 @@ export class EmsKernel extends FlowEngine {
|
|
|
512
532
|
this.closeGenerationPeriod(id, eq, { atMs, kWh, dayIndex, kind, since, priorSince, periodAdvanced, faulted: dropped && sameDayDrop });
|
|
513
533
|
this.revision++;
|
|
514
534
|
}
|
|
535
|
+
/**
|
|
536
|
+
* 시계가 기간의 끝을 지났으면 마감한다 — **표본을 기다리지 않는다.**
|
|
537
|
+
*
|
|
538
|
+
* 값이 오지 않는 밤에도 자정이 지나면 어제가 닫힌다. 마감에 쓰는 총량은 **그날 마지막으로 본 값**
|
|
539
|
+
* 이므로 표본이 더 오지 않아도 정확하다(하루 누적은 그 값이 곧 그날의 총량이다).
|
|
540
|
+
*
|
|
541
|
+
* 이 자리는 계량의 `closeDue` 와 같은 성질이다 — 호스트가 시각을 주며 부른다.
|
|
542
|
+
*/
|
|
543
|
+
closeGenerationPeriodsDue(nowMs) {
|
|
544
|
+
const today = this.localDayIndex(nowMs);
|
|
545
|
+
for (const [id, obs] of [...this.generationPeriodObs.entries()]) {
|
|
546
|
+
if (today <= obs.dayIndex)
|
|
547
|
+
continue;
|
|
548
|
+
const eq = this.equipment.get(id);
|
|
549
|
+
if (!eq)
|
|
550
|
+
continue;
|
|
551
|
+
const kind = eq.generatedKWhAccumulation;
|
|
552
|
+
/*
|
|
553
|
+
* 마감은 `applyGenerated` 와 **같은 함수**로 한다 — 규칙을 두 벌 만들면 표본으로 닫은 날과
|
|
554
|
+
* 시계로 닫은 날이 다른 값을 낸다.
|
|
555
|
+
*
|
|
556
|
+
* 그때 「지금 들어온 값」이 없으므로, 마지막으로 본 값을 그대로 넘긴다(그 값은 이미 상태에 있다).
|
|
557
|
+
*/
|
|
558
|
+
this.closeGenerationPeriod(id, eq, {
|
|
559
|
+
atMs: nowMs,
|
|
560
|
+
kWh: obs.lastKWh,
|
|
561
|
+
dayIndex: today,
|
|
562
|
+
kind,
|
|
563
|
+
periodAdvanced: false,
|
|
564
|
+
faulted: false
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
}
|
|
515
568
|
/**
|
|
516
569
|
* **기간이 끝났으면 그 기간의 발전량을 사실로 낸다.**
|
|
517
570
|
*
|
|
@@ -627,6 +680,8 @@ export class EmsKernel extends FlowEngine {
|
|
|
627
680
|
lastBill;
|
|
628
681
|
/** 마지막으로 받은 요금 기준 — 이 주기에 적용되는 요금적용전력과 단가. */
|
|
629
682
|
tariffBasis;
|
|
683
|
+
/** 마지막으로 받은 발전 단가 — 이 기간에 낸 전기 1kWh 의 값. */
|
|
684
|
+
generationPrice;
|
|
630
685
|
/**
|
|
631
686
|
* 이 청구 주기의 **최고 수요** — 기본요금이 이 값으로 다시 정해진다.
|
|
632
687
|
*
|
|
@@ -1501,6 +1556,16 @@ export class EmsKernel extends FlowEngine {
|
|
|
1501
1556
|
* 부하를 먼저 만들고 마감한다 — 마감이 먼저면 마지막 값이 다음 구간으로 밀린다.
|
|
1502
1557
|
*/
|
|
1503
1558
|
const at = this.nowMs();
|
|
1559
|
+
/*
|
|
1560
|
+
* ── 발전 기간도 **시계로** 마감한다 (2026-08-30) ───────────────────────────
|
|
1561
|
+
*
|
|
1562
|
+
* 앞 판은 새 표본이 올 때만 날짜가 바뀐 것을 알아챘다. 그런데 태양광은 해가 지면 값을 보내지
|
|
1563
|
+
* 않는다 — 어제 마지막 표본이 19:40 이면 어제 하루치가 **다음 날 아침 첫 표본**에야 확정된다.
|
|
1564
|
+
* 밤새 「어제 얼마 냈나」에 답이 없다.
|
|
1565
|
+
*
|
|
1566
|
+
* 계량은 이미 시계로 마감한다(`closeDue`). 한 커널 안에서 규칙이 두 벌일 이유가 없다.
|
|
1567
|
+
*/
|
|
1568
|
+
this.closeGenerationPeriodsDue(at);
|
|
1504
1569
|
/* 틱 길이를 함께 넘긴다 — 방전량을 에너지로 바꿀 때 필요하다(필드로 숨기면 어디서 온 값인지 흐려진다). */
|
|
1505
1570
|
this.deriveLoad(at, dtMs);
|
|
1506
1571
|
this.closeDue(at);
|
|
@@ -1560,6 +1625,8 @@ export class EmsKernel extends FlowEngine {
|
|
|
1560
1625
|
this.lastBill = { ...e.lastBill };
|
|
1561
1626
|
if (e.tariffBasis?.from && e.tariffBasis?.to)
|
|
1562
1627
|
this.tariffBasis = { ...e.tariffBasis };
|
|
1628
|
+
if (e.generationPrice?.from && e.generationPrice?.to)
|
|
1629
|
+
this.generationPrice = { ...e.generationPrice };
|
|
1563
1630
|
/* 이 주기의 최고도 이어받는다 — 재기동마다 0 으로 돌아가면 이미 넘은 것을 못 넘었다고 말한다. */
|
|
1564
1631
|
if (Number.isFinite(Number(e.demandHigh?.kW)) && e.demandHigh?.since)
|
|
1565
1632
|
this.demandHigh = { ...e.demandHigh };
|
|
@@ -1589,6 +1656,7 @@ export class EmsKernel extends FlowEngine {
|
|
|
1589
1656
|
...(this.unclosedGenerationPeriods ? { unclosedGenerationPeriods: this.unclosedGenerationPeriods } : {}),
|
|
1590
1657
|
...(this.lastBill ? { lastBill: { ...this.lastBill } } : {}),
|
|
1591
1658
|
...(this.tariffBasis ? { tariffBasis: { ...this.tariffBasis } } : {}),
|
|
1659
|
+
...(this.generationPrice ? { generationPrice: { ...this.generationPrice } } : {}),
|
|
1592
1660
|
...(() => {
|
|
1593
1661
|
const r = this.generationRated();
|
|
1594
1662
|
return r ? { generationRated: r } : {};
|
package/dist/energy-ingest.d.ts
CHANGED
|
@@ -115,6 +115,21 @@ export interface EnergyUsagePeriodRecord {
|
|
|
115
115
|
basis?: string;
|
|
116
116
|
}
|
|
117
117
|
export declare function isEnergyUsagePeriodRecord(record: unknown): boolean;
|
|
118
|
+
/** 발전 단가 — 이 기간에 낸 전기 1kWh 의 값. 날마다 바뀐다. */
|
|
119
|
+
export interface EnergyGenerationPriceRecord {
|
|
120
|
+
from: string;
|
|
121
|
+
to: string;
|
|
122
|
+
unitPrice: number;
|
|
123
|
+
currency: string;
|
|
124
|
+
}
|
|
125
|
+
export declare function isEnergyGenerationPriceRecord(record: unknown): boolean;
|
|
126
|
+
/**
|
|
127
|
+
* 발전 단가를 봉투로 — **사건 시각은 `from`** 이다(그때부터 적용된다).
|
|
128
|
+
*
|
|
129
|
+
* 제도의 가산·인증서 셈은 커넥터가 하고 여기에는 1kWh 당 얼마만 온다. 커널이 한 제도의 모양을
|
|
130
|
+
* 안으면 다른 제도에서 그 자리가 거짓이 된다.
|
|
131
|
+
*/
|
|
132
|
+
export declare function ingestEnergyGenerationPriceRecords(records: EnergyGenerationPriceRecord | EnergyGenerationPriceRecord[] | undefined | null, opts: EnergyIngestOptions): EnergyIngestResult;
|
|
118
133
|
/** 이 주기에 적용되는 요금 기준 — 공급자가 정하고 매 주기 다시 정해진다. */
|
|
119
134
|
export interface EnergyTariffBasisRecord {
|
|
120
135
|
from: string;
|
package/dist/energy-ingest.js
CHANGED
|
@@ -378,6 +378,53 @@ 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 isEnergyGenerationPriceRecord(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
|
+
/* 계량 지점·설비가 있으면 그 설비의 사실이지 단가가 아니다 — 문이 겹치지 않게. */
|
|
388
|
+
if (r.meterId !== undefined || r.equipmentId !== undefined)
|
|
389
|
+
return false;
|
|
390
|
+
return r.unitPrice !== undefined;
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* 발전 단가를 봉투로 — **사건 시각은 `from`** 이다(그때부터 적용된다).
|
|
394
|
+
*
|
|
395
|
+
* 제도의 가산·인증서 셈은 커넥터가 하고 여기에는 1kWh 당 얼마만 온다. 커널이 한 제도의 모양을
|
|
396
|
+
* 안으면 다른 제도에서 그 자리가 거짓이 된다.
|
|
397
|
+
*/
|
|
398
|
+
export function ingestEnergyGenerationPriceRecords(records, opts) {
|
|
399
|
+
const list = records === undefined || records === null ? [] : Array.isArray(records) ? records : [records];
|
|
400
|
+
const accepted = [];
|
|
401
|
+
const rejected = [];
|
|
402
|
+
let seq = 0;
|
|
403
|
+
for (const r of list) {
|
|
404
|
+
const errors = [];
|
|
405
|
+
const span = readSpan(r, errors);
|
|
406
|
+
const unitPrice = readAmount(r?.unitPrice, 'unitPrice', errors);
|
|
407
|
+
if (unitPrice === undefined && !errors.length)
|
|
408
|
+
errors.push('unitPrice 가 없다 — 이 문이 받는 값은 1kWh 당 값 하나다');
|
|
409
|
+
/* 단위 없는 값은 다른 금액과 더할 수 없다. */
|
|
410
|
+
const currency = String(r?.currency ?? '').trim();
|
|
411
|
+
if (!currency)
|
|
412
|
+
errors.push('currency 가 없다 — 단위 없는 값은 다른 금액과 더할 수 없다');
|
|
413
|
+
if (errors.length || !span || unitPrice === undefined) {
|
|
414
|
+
rejected.push({ record: r, errors });
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
const data = { from: span.from, to: span.to, unitPrice, currency };
|
|
418
|
+
accepted.push({
|
|
419
|
+
eventId: `${opts.tenantId}-generation-price-${++seq}`,
|
|
420
|
+
eventType: ENERGY_EVENT.generationPrice,
|
|
421
|
+
eventTime: span.from,
|
|
422
|
+
tenantId: opts.tenantId,
|
|
423
|
+
data
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
return { accepted, rejected };
|
|
427
|
+
}
|
|
381
428
|
export function isEnergyTariffBasisRecord(record) {
|
|
382
429
|
const r = record;
|
|
383
430
|
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, ingestEnergyTariffBasisRecords, isEnergyTariffBasisRecord } from './energy-ingest.ts';
|
|
33
|
+
export { ingestEnergyRecords, isEnergyRecord, ingestEnergyEquipmentRecords, isEnergyEquipmentRecord, ingestEnergyGenerationRecords, isEnergyGenerationRecord, ingestEnergyUsagePeriodRecords, isEnergyUsagePeriodRecord, ingestEnergyBillRecords, isEnergyBillRecord, ingestEnergyTariffBasisRecords, isEnergyTariffBasisRecord, ingestEnergyGenerationPriceRecords, isEnergyGenerationPriceRecord } 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, EnergyTariffBasisRecord, EnergyIngestOptions, EnergyIngestResult } from './energy-ingest.ts';
|
|
38
|
+
export type { EnergyRecord, EnergyEquipmentRecord, EnergyGenerationRecord, EnergyUsagePeriodRecord, EnergyBillRecord, EnergyTariffBasisRecord, EnergyGenerationPriceRecord, 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, ingestEnergyTariffBasisRecords, isEnergyTariffBasisRecord } from "./energy-ingest.js";
|
|
33
|
+
export { ingestEnergyRecords, isEnergyRecord, ingestEnergyEquipmentRecords, isEnergyEquipmentRecord, ingestEnergyGenerationRecords, isEnergyGenerationRecord, ingestEnergyUsagePeriodRecords, isEnergyUsagePeriodRecord, ingestEnergyBillRecords, isEnergyBillRecord, ingestEnergyTariffBasisRecords, isEnergyTariffBasisRecord, ingestEnergyGenerationPriceRecords, isEnergyGenerationPriceRecord } 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
|
@@ -127,6 +127,7 @@ __export(index_exports, {
|
|
|
127
127
|
ingest: () => ingest,
|
|
128
128
|
ingestEnergyBillRecords: () => ingestEnergyBillRecords,
|
|
129
129
|
ingestEnergyEquipmentRecords: () => ingestEnergyEquipmentRecords,
|
|
130
|
+
ingestEnergyGenerationPriceRecords: () => ingestEnergyGenerationPriceRecords,
|
|
130
131
|
ingestEnergyGenerationRecords: () => ingestEnergyGenerationRecords,
|
|
131
132
|
ingestEnergyRecords: () => ingestEnergyRecords,
|
|
132
133
|
ingestEnergyTariffBasisRecords: () => ingestEnergyTariffBasisRecords,
|
|
@@ -137,6 +138,7 @@ __export(index_exports, {
|
|
|
137
138
|
isElectricalLocationType: () => isElectricalLocationType,
|
|
138
139
|
isEnergyBillRecord: () => isEnergyBillRecord,
|
|
139
140
|
isEnergyEquipmentRecord: () => isEnergyEquipmentRecord,
|
|
141
|
+
isEnergyGenerationPriceRecord: () => isEnergyGenerationPriceRecord,
|
|
140
142
|
isEnergyGenerationRecord: () => isEnergyGenerationRecord,
|
|
141
143
|
isEnergyRecord: () => isEnergyRecord,
|
|
142
144
|
isEnergyTariffBasisRecord: () => isEnergyTariffBasisRecord,
|
|
@@ -711,6 +713,23 @@ var ENERGY_EVENT = {
|
|
|
711
713
|
* 쪽인지 말한다.
|
|
712
714
|
*/
|
|
713
715
|
usagePeriod: "energy.usage.period",
|
|
716
|
+
/**
|
|
717
|
+
* **발전에 매겨지는 단가가 정해졌다** — 이 기간에 낸 전기 1kWh 의 값.
|
|
718
|
+
*
|
|
719
|
+
* 소비에 요금이 매겨지듯 발전에도 값이 매겨진다. 파는 발전소는 판 값이고, 자가소비형은 아꼈다고
|
|
720
|
+
* 볼 수 있는 값이다 — 어느 쪽인지는 현장이 정한다.
|
|
721
|
+
*
|
|
722
|
+
* **단가는 날마다 바뀐다.** 그래서 선언이 아니라 기간을 가진 사실로 받는다. 선언에 적어 두면
|
|
723
|
+
* 사람이 매일 갱신해야 하고, 안 하면 낡은 값으로 수익이 조용히 틀린다.
|
|
724
|
+
*
|
|
725
|
+
* 사건 시각은 기간의 시작이다 — 「이 시각부터 이 단가가 적용된다」이므로 기간이 끝나기 전에 성립한다.
|
|
726
|
+
*
|
|
727
|
+
* ── 담지 않은 것 ────────────────────────────────────────────────────────────
|
|
728
|
+
* 제도마다 있는 가산·인증서·가중치는 담지 않는다. 그 셈은 제도가 정하고 나라마다 다르므로,
|
|
729
|
+
* 커넥터가 계산해 **1kWh 당 얼마**로 옮겨 보낸다. 커널이 한 제도의 모양을 안으면 다른 제도에서
|
|
730
|
+
* 그 자리가 거짓이 된다.
|
|
731
|
+
*/
|
|
732
|
+
generationPrice: "energy.generation.price",
|
|
714
733
|
/**
|
|
715
734
|
* **이 주기에 적용되는 요금 기준이 정해졌다** — 요금적용전력과 기본요금 단가.
|
|
716
735
|
*
|
|
@@ -7991,6 +8010,10 @@ var EmsKernel = class extends FlowEngine {
|
|
|
7991
8010
|
this.applyTariffBasis(envelope);
|
|
7992
8011
|
return;
|
|
7993
8012
|
}
|
|
8013
|
+
if (envelope.eventType === ENERGY_EVENT.generationPrice) {
|
|
8014
|
+
this.applyGenerationPrice(envelope);
|
|
8015
|
+
return;
|
|
8016
|
+
}
|
|
7994
8017
|
super.apply(envelope);
|
|
7995
8018
|
}
|
|
7996
8019
|
/**
|
|
@@ -8031,6 +8054,19 @@ var EmsKernel = class extends FlowEngine {
|
|
|
8031
8054
|
}
|
|
8032
8055
|
this.revision++;
|
|
8033
8056
|
}
|
|
8057
|
+
/**
|
|
8058
|
+
* 발전 단가를 상태에 둔다 — 늦게 온 지난 기간의 단가가 최신을 덮지 않게 시작 시각으로 가린다.
|
|
8059
|
+
*/
|
|
8060
|
+
applyGenerationPrice(envelope) {
|
|
8061
|
+
const d = envelope.data;
|
|
8062
|
+
if (!d?.from || !d?.to) return;
|
|
8063
|
+
const fromMs = Date.parse(String(d.from));
|
|
8064
|
+
if (!Number.isFinite(fromMs)) return;
|
|
8065
|
+
const priorFromMs = Date.parse(String(this.generationPrice?.from ?? ""));
|
|
8066
|
+
if (Number.isFinite(priorFromMs) && fromMs < priorFromMs) return;
|
|
8067
|
+
this.generationPrice = { ...d };
|
|
8068
|
+
this.revision++;
|
|
8069
|
+
}
|
|
8034
8070
|
/**
|
|
8035
8071
|
* 이 주기에 적용되는 요금 기준을 상태에 둔다.
|
|
8036
8072
|
*
|
|
@@ -8132,6 +8168,31 @@ var EmsKernel = class extends FlowEngine {
|
|
|
8132
8168
|
this.closeGenerationPeriod(id, eq, { atMs, kWh, dayIndex, kind, since, priorSince, periodAdvanced, faulted: dropped && sameDayDrop });
|
|
8133
8169
|
this.revision++;
|
|
8134
8170
|
}
|
|
8171
|
+
/**
|
|
8172
|
+
* 시계가 기간의 끝을 지났으면 마감한다 — **표본을 기다리지 않는다.**
|
|
8173
|
+
*
|
|
8174
|
+
* 값이 오지 않는 밤에도 자정이 지나면 어제가 닫힌다. 마감에 쓰는 총량은 **그날 마지막으로 본 값**
|
|
8175
|
+
* 이므로 표본이 더 오지 않아도 정확하다(하루 누적은 그 값이 곧 그날의 총량이다).
|
|
8176
|
+
*
|
|
8177
|
+
* 이 자리는 계량의 `closeDue` 와 같은 성질이다 — 호스트가 시각을 주며 부른다.
|
|
8178
|
+
*/
|
|
8179
|
+
closeGenerationPeriodsDue(nowMs) {
|
|
8180
|
+
const today = this.localDayIndex(nowMs);
|
|
8181
|
+
for (const [id, obs] of [...this.generationPeriodObs.entries()]) {
|
|
8182
|
+
if (today <= obs.dayIndex) continue;
|
|
8183
|
+
const eq = this.equipment.get(id);
|
|
8184
|
+
if (!eq) continue;
|
|
8185
|
+
const kind = eq.generatedKWhAccumulation;
|
|
8186
|
+
this.closeGenerationPeriod(id, eq, {
|
|
8187
|
+
atMs: nowMs,
|
|
8188
|
+
kWh: obs.lastKWh,
|
|
8189
|
+
dayIndex: today,
|
|
8190
|
+
kind,
|
|
8191
|
+
periodAdvanced: false,
|
|
8192
|
+
faulted: false
|
|
8193
|
+
});
|
|
8194
|
+
}
|
|
8195
|
+
}
|
|
8135
8196
|
/**
|
|
8136
8197
|
* **기간이 끝났으면 그 기간의 발전량을 사실로 낸다.**
|
|
8137
8198
|
*
|
|
@@ -8225,6 +8286,8 @@ var EmsKernel = class extends FlowEngine {
|
|
|
8225
8286
|
lastBill;
|
|
8226
8287
|
/** 마지막으로 받은 요금 기준 — 이 주기에 적용되는 요금적용전력과 단가. */
|
|
8227
8288
|
tariffBasis;
|
|
8289
|
+
/** 마지막으로 받은 발전 단가 — 이 기간에 낸 전기 1kWh 의 값. */
|
|
8290
|
+
generationPrice;
|
|
8228
8291
|
/**
|
|
8229
8292
|
* 이 청구 주기의 **최고 수요** — 기본요금이 이 값으로 다시 정해진다.
|
|
8230
8293
|
*
|
|
@@ -8860,6 +8923,7 @@ var EmsKernel = class extends FlowEngine {
|
|
|
8860
8923
|
tick(dtMs) {
|
|
8861
8924
|
super.tick(dtMs);
|
|
8862
8925
|
const at = this.nowMs();
|
|
8926
|
+
this.closeGenerationPeriodsDue(at);
|
|
8863
8927
|
this.deriveLoad(at, dtMs);
|
|
8864
8928
|
this.closeDue(at);
|
|
8865
8929
|
}
|
|
@@ -8899,6 +8963,7 @@ var EmsKernel = class extends FlowEngine {
|
|
|
8899
8963
|
if (Number.isFinite(e.unclosedGenerationPeriods)) this.unclosedGenerationPeriods = Number(e.unclosedGenerationPeriods);
|
|
8900
8964
|
if (e.lastBill?.from && e.lastBill?.to) this.lastBill = { ...e.lastBill };
|
|
8901
8965
|
if (e.tariffBasis?.from && e.tariffBasis?.to) this.tariffBasis = { ...e.tariffBasis };
|
|
8966
|
+
if (e.generationPrice?.from && e.generationPrice?.to) this.generationPrice = { ...e.generationPrice };
|
|
8902
8967
|
if (Number.isFinite(Number(e.demandHigh?.kW)) && e.demandHigh?.since) this.demandHigh = { ...e.demandHigh };
|
|
8903
8968
|
}
|
|
8904
8969
|
getSnapshot() {
|
|
@@ -8922,6 +8987,7 @@ var EmsKernel = class extends FlowEngine {
|
|
|
8922
8987
|
...this.unclosedGenerationPeriods ? { unclosedGenerationPeriods: this.unclosedGenerationPeriods } : {},
|
|
8923
8988
|
...this.lastBill ? { lastBill: { ...this.lastBill } } : {},
|
|
8924
8989
|
...this.tariffBasis ? { tariffBasis: { ...this.tariffBasis } } : {},
|
|
8990
|
+
...this.generationPrice ? { generationPrice: { ...this.generationPrice } } : {},
|
|
8925
8991
|
...(() => {
|
|
8926
8992
|
const r = this.generationRated();
|
|
8927
8993
|
return r ? { generationRated: r } : {};
|
|
@@ -9972,6 +10038,40 @@ function isEnergyUsagePeriodRecord(record) {
|
|
|
9972
10038
|
const r = record;
|
|
9973
10039
|
return !!r && typeof r === "object" && r.meterId !== void 0 && r.from !== void 0 && r.to !== void 0 && r.kWh !== void 0;
|
|
9974
10040
|
}
|
|
10041
|
+
function isEnergyGenerationPriceRecord(record) {
|
|
10042
|
+
const r = record;
|
|
10043
|
+
if (!r || typeof r !== "object") return false;
|
|
10044
|
+
if (r.from === void 0 || r.to === void 0) return false;
|
|
10045
|
+
if (r.meterId !== void 0 || r.equipmentId !== void 0) return false;
|
|
10046
|
+
return r.unitPrice !== void 0;
|
|
10047
|
+
}
|
|
10048
|
+
function ingestEnergyGenerationPriceRecords(records, opts) {
|
|
10049
|
+
const list = records === void 0 || records === null ? [] : Array.isArray(records) ? records : [records];
|
|
10050
|
+
const accepted = [];
|
|
10051
|
+
const rejected = [];
|
|
10052
|
+
let seq = 0;
|
|
10053
|
+
for (const r of list) {
|
|
10054
|
+
const errors = [];
|
|
10055
|
+
const span = readSpan(r, errors);
|
|
10056
|
+
const unitPrice = readAmount(r?.unitPrice, "unitPrice", errors);
|
|
10057
|
+
if (unitPrice === void 0 && !errors.length) errors.push("unitPrice \uAC00 \uC5C6\uB2E4 \u2014 \uC774 \uBB38\uC774 \uBC1B\uB294 \uAC12\uC740 1kWh \uB2F9 \uAC12 \uD558\uB098\uB2E4");
|
|
10058
|
+
const currency = String(r?.currency ?? "").trim();
|
|
10059
|
+
if (!currency) errors.push("currency \uAC00 \uC5C6\uB2E4 \u2014 \uB2E8\uC704 \uC5C6\uB294 \uAC12\uC740 \uB2E4\uB978 \uAE08\uC561\uACFC \uB354\uD560 \uC218 \uC5C6\uB2E4");
|
|
10060
|
+
if (errors.length || !span || unitPrice === void 0) {
|
|
10061
|
+
rejected.push({ record: r, errors });
|
|
10062
|
+
continue;
|
|
10063
|
+
}
|
|
10064
|
+
const data = { from: span.from, to: span.to, unitPrice, currency };
|
|
10065
|
+
accepted.push({
|
|
10066
|
+
eventId: `${opts.tenantId}-generation-price-${++seq}`,
|
|
10067
|
+
eventType: ENERGY_EVENT.generationPrice,
|
|
10068
|
+
eventTime: span.from,
|
|
10069
|
+
tenantId: opts.tenantId,
|
|
10070
|
+
data
|
|
10071
|
+
});
|
|
10072
|
+
}
|
|
10073
|
+
return { accepted, rejected };
|
|
10074
|
+
}
|
|
9975
10075
|
function isEnergyTariffBasisRecord(record) {
|
|
9976
10076
|
const r = record;
|
|
9977
10077
|
if (!r || typeof r !== "object") return false;
|
|
@@ -10834,6 +10934,7 @@ function retiredVocabularyIn(line) {
|
|
|
10834
10934
|
ingest,
|
|
10835
10935
|
ingestEnergyBillRecords,
|
|
10836
10936
|
ingestEnergyEquipmentRecords,
|
|
10937
|
+
ingestEnergyGenerationPriceRecords,
|
|
10837
10938
|
ingestEnergyGenerationRecords,
|
|
10838
10939
|
ingestEnergyRecords,
|
|
10839
10940
|
ingestEnergyTariffBasisRecords,
|
|
@@ -10844,6 +10945,7 @@ function retiredVocabularyIn(line) {
|
|
|
10844
10945
|
isElectricalLocationType,
|
|
10845
10946
|
isEnergyBillRecord,
|
|
10846
10947
|
isEnergyEquipmentRecord,
|
|
10948
|
+
isEnergyGenerationPriceRecord,
|
|
10847
10949
|
isEnergyGenerationRecord,
|
|
10848
10950
|
isEnergyRecord,
|
|
10849
10951
|
isEnergyTariffBasisRecord,
|
package/package.json
CHANGED