@operato/twin-kernel 0.7.86 → 0.7.88
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 +17 -0
- package/dist/ems-kernel.d.ts +11 -0
- package/dist/ems-kernel.js +82 -14
- package/dist/energy-ingest.d.ts +5 -8
- package/dist/energy-ingest.js +44 -0
- package/dist-cjs/index.cjs +78 -10
- package/package.json +1 -1
package/dist/contract.d.ts
CHANGED
|
@@ -2041,6 +2041,19 @@ export interface EnergyState {
|
|
|
2041
2041
|
* 청구서보다 이쪽이 먼저다: 청구서는 끝난 기간의 정산이고 이것은 지금 적용되는 값이다.
|
|
2042
2042
|
*/
|
|
2043
2043
|
tariffBasis?: EnergyTariffBasisData;
|
|
2044
|
+
/**
|
|
2045
|
+
* **선언된 요금 기준들** — 유효 구간별. `tariffBasis` 는 이 중 지금 유효한 것이다.
|
|
2046
|
+
*
|
|
2047
|
+
* ── 왜 여럿을 드나 (2026-08-30) ──────────────────────────────────────────
|
|
2048
|
+
* 선언은 **예정될 수 있다.** 한전은 다음 달 요금을 미리 공표한다. 하나만 들면 9월 선언이 들어오는
|
|
2049
|
+
* 순간 8월 선언을 덮어써서 **오늘 요금이 다음 달 단가로 계산된다.** 오류는 나지 않는다.
|
|
2050
|
+
*
|
|
2051
|
+
* 처음에는 예정된 선언을 유입에서 거부해 이 상황을 막았다. 그것은 정상적인 사실을 막는 것이라,
|
|
2052
|
+
* 담을 자리를 만들고 거부를 풀었다.
|
|
2053
|
+
*
|
|
2054
|
+
* 고르는 규칙은 **커널에 한 벌만 둔다.** 읽는 쪽이 각자 고르면 화면마다 다른 요금이 나온다.
|
|
2055
|
+
*/
|
|
2056
|
+
tariffBasisSchedule?: EnergyTariffBasisData[];
|
|
2044
2057
|
/**
|
|
2045
2058
|
* 이 트윈의 **발전 정격**과 그 근거 — 이용률의 분모(§ `generationRated`).
|
|
2046
2059
|
*
|
|
@@ -2056,8 +2069,12 @@ export interface EnergyState {
|
|
|
2056
2069
|
* 마지막으로 받은 **발전 단가** — 이 기간에 낸 전기 1kWh 의 값.
|
|
2057
2070
|
*
|
|
2058
2071
|
* 창 밖의 단가도 이 창에 적용될 수 있어(단가는 기간마다 정해진다) 상태로 이어 간다.
|
|
2072
|
+
*
|
|
2073
|
+
* 예정된 단가는 `generationPriceSchedule` 에 함께 든다 — 이 칸은 **지금 유효한 것**이다.
|
|
2059
2074
|
*/
|
|
2060
2075
|
generationPrice?: EnergyGenerationPriceData;
|
|
2076
|
+
/** **선언된 발전 단가들** — 유효 구간별. 예정된 것을 담는다(§`tariffBasisSchedule`). */
|
|
2077
|
+
generationPriceSchedule?: EnergyGenerationPriceData[];
|
|
2061
2078
|
/**
|
|
2062
2079
|
* 이 청구 주기의 **최고 수요** — 기본요금이 이 값으로 다시 정해진다.
|
|
2063
2080
|
*
|
package/dist/ems-kernel.d.ts
CHANGED
|
@@ -133,6 +133,13 @@ export declare class EmsKernel extends FlowEngine {
|
|
|
133
133
|
* 새것을 쓴다 — 사업자가 그 주기의 기준을 고칠 수 있다.
|
|
134
134
|
*/
|
|
135
135
|
private applyTariffBasis;
|
|
136
|
+
/**
|
|
137
|
+
* 시계가 선언의 경계를 넘으면 **지금 유효한 선언이 바뀐다.**
|
|
138
|
+
*
|
|
139
|
+
* 표본이 들어올 때만 다시 고르면, 9월 1일 0시에 단가가 바뀌었는데 다음 표본이 올 때까지 8월 단가로
|
|
140
|
+
* 계산한다. 태양광은 밤에 표본이 없어 그 간격이 길다.
|
|
141
|
+
*/
|
|
142
|
+
protected refreshDeclarationsInEffect(nowMs: number): void;
|
|
136
143
|
/**
|
|
137
144
|
* 청구서를 상태에 둔다 — **우리 계산을 덮지 않는다.**
|
|
138
145
|
*
|
|
@@ -221,8 +228,12 @@ export declare class EmsKernel extends FlowEngine {
|
|
|
221
228
|
private lastBill?;
|
|
222
229
|
/** 마지막으로 받은 요금 기준 — 이 주기에 적용되는 요금적용전력과 단가. */
|
|
223
230
|
private tariffBasis?;
|
|
231
|
+
/** 선언된 요금 기준들 — 예정된 것을 포함한다(§`EnergyState.tariffBasisSchedule`). */
|
|
232
|
+
private tariffBasisSchedule?;
|
|
224
233
|
/** 마지막으로 받은 발전 단가 — 이 기간에 낸 전기 1kWh 의 값. */
|
|
225
234
|
private generationPrice?;
|
|
235
|
+
/** 선언된 발전 단가들 — 예정된 것을 포함한다. */
|
|
236
|
+
private generationPriceSchedule?;
|
|
226
237
|
/**
|
|
227
238
|
* 이 청구 주기의 **최고 수요** — 기본요금이 이 값으로 다시 정해진다.
|
|
228
239
|
*
|
package/dist/ems-kernel.js
CHANGED
|
@@ -354,13 +354,8 @@ export class EmsKernel extends FlowEngine {
|
|
|
354
354
|
const d = envelope.data;
|
|
355
355
|
if (!d?.from || !d?.to)
|
|
356
356
|
return;
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
return;
|
|
360
|
-
const priorFromMs = Date.parse(String(this.generationPrice?.from ?? ''));
|
|
361
|
-
if (Number.isFinite(priorFromMs) && fromMs < priorFromMs)
|
|
362
|
-
return;
|
|
363
|
-
this.generationPrice = { ...d };
|
|
357
|
+
this.generationPriceSchedule = putDeclaration(this.generationPriceSchedule, d);
|
|
358
|
+
this.generationPrice = inEffectAt(this.generationPriceSchedule, this.nowMs());
|
|
364
359
|
this.revision++;
|
|
365
360
|
}
|
|
366
361
|
/**
|
|
@@ -373,15 +368,25 @@ export class EmsKernel extends FlowEngine {
|
|
|
373
368
|
const d = envelope.data;
|
|
374
369
|
if (!d?.from || !d?.to)
|
|
375
370
|
return;
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
return;
|
|
379
|
-
const priorFromMs = Date.parse(String(this.tariffBasis?.from ?? ''));
|
|
380
|
-
if (Number.isFinite(priorFromMs) && fromMs < priorFromMs)
|
|
381
|
-
return;
|
|
382
|
-
this.tariffBasis = { ...d };
|
|
371
|
+
this.tariffBasisSchedule = putDeclaration(this.tariffBasisSchedule, d);
|
|
372
|
+
this.tariffBasis = inEffectAt(this.tariffBasisSchedule, this.nowMs());
|
|
383
373
|
this.revision++;
|
|
384
374
|
}
|
|
375
|
+
/**
|
|
376
|
+
* 시계가 선언의 경계를 넘으면 **지금 유효한 선언이 바뀐다.**
|
|
377
|
+
*
|
|
378
|
+
* 표본이 들어올 때만 다시 고르면, 9월 1일 0시에 단가가 바뀌었는데 다음 표본이 올 때까지 8월 단가로
|
|
379
|
+
* 계산한다. 태양광은 밤에 표본이 없어 그 간격이 길다.
|
|
380
|
+
*/
|
|
381
|
+
refreshDeclarationsInEffect(nowMs) {
|
|
382
|
+
const tariff = inEffectAt(this.tariffBasisSchedule, nowMs);
|
|
383
|
+
const price = inEffectAt(this.generationPriceSchedule, nowMs);
|
|
384
|
+
if (tariff !== this.tariffBasis || price !== this.generationPrice) {
|
|
385
|
+
this.tariffBasis = tariff;
|
|
386
|
+
this.generationPrice = price;
|
|
387
|
+
this.revision++;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
385
390
|
/**
|
|
386
391
|
* 청구서를 상태에 둔다 — **우리 계산을 덮지 않는다.**
|
|
387
392
|
*
|
|
@@ -713,8 +718,12 @@ export class EmsKernel extends FlowEngine {
|
|
|
713
718
|
lastBill;
|
|
714
719
|
/** 마지막으로 받은 요금 기준 — 이 주기에 적용되는 요금적용전력과 단가. */
|
|
715
720
|
tariffBasis;
|
|
721
|
+
/** 선언된 요금 기준들 — 예정된 것을 포함한다(§`EnergyState.tariffBasisSchedule`). */
|
|
722
|
+
tariffBasisSchedule;
|
|
716
723
|
/** 마지막으로 받은 발전 단가 — 이 기간에 낸 전기 1kWh 의 값. */
|
|
717
724
|
generationPrice;
|
|
725
|
+
/** 선언된 발전 단가들 — 예정된 것을 포함한다. */
|
|
726
|
+
generationPriceSchedule;
|
|
718
727
|
/**
|
|
719
728
|
* 이 청구 주기의 **최고 수요** — 기본요금이 이 값으로 다시 정해진다.
|
|
720
729
|
*
|
|
@@ -1599,6 +1608,8 @@ export class EmsKernel extends FlowEngine {
|
|
|
1599
1608
|
* 계량은 이미 시계로 마감한다(`closeDue`). 한 커널 안에서 규칙이 두 벌일 이유가 없다.
|
|
1600
1609
|
*/
|
|
1601
1610
|
this.closeGenerationPeriodsDue(at);
|
|
1611
|
+
/* 시계가 선언의 경계를 넘으면 지금 유효한 선언이 바뀐다 — 표본을 기다리지 않는다. */
|
|
1612
|
+
this.refreshDeclarationsInEffect(at);
|
|
1602
1613
|
/* 틱 길이를 함께 넘긴다 — 방전량을 에너지로 바꿀 때 필요하다(필드로 숨기면 어디서 온 값인지 흐려진다). */
|
|
1603
1614
|
this.deriveLoad(at, dtMs);
|
|
1604
1615
|
this.closeDue(at);
|
|
@@ -1657,10 +1668,21 @@ export class EmsKernel extends FlowEngine {
|
|
|
1657
1668
|
/* 청구서도 이어받는다 — 재기동마다 사라지면 우리 계산과 견줄 기준이 없어진다. */
|
|
1658
1669
|
if (e.lastBill?.from && e.lastBill?.to)
|
|
1659
1670
|
this.lastBill = { ...e.lastBill };
|
|
1671
|
+
/*
|
|
1672
|
+
* 선언은 **목록째** 이어받는다 — 예정된 것이 사라지면 재기동 뒤에 시행일이 와도 옛 단가를 쓴다.
|
|
1673
|
+
* 이어받은 뒤 지금 시각으로 다시 고른다: 멈춰 있는 동안 시행일이 지났을 수 있다.
|
|
1674
|
+
*/
|
|
1675
|
+
if (Array.isArray(e.tariffBasisSchedule))
|
|
1676
|
+
this.tariffBasisSchedule = e.tariffBasisSchedule.map(d => ({ ...d }));
|
|
1677
|
+
if (Array.isArray(e.generationPriceSchedule))
|
|
1678
|
+
this.generationPriceSchedule = e.generationPriceSchedule.map(d => ({ ...d }));
|
|
1660
1679
|
if (e.tariffBasis?.from && e.tariffBasis?.to)
|
|
1661
1680
|
this.tariffBasis = { ...e.tariffBasis };
|
|
1662
1681
|
if (e.generationPrice?.from && e.generationPrice?.to)
|
|
1663
1682
|
this.generationPrice = { ...e.generationPrice };
|
|
1683
|
+
if (this.tariffBasisSchedule?.length || this.generationPriceSchedule?.length) {
|
|
1684
|
+
this.refreshDeclarationsInEffect(this.nowMs());
|
|
1685
|
+
}
|
|
1664
1686
|
/* 이 주기의 최고도 이어받는다 — 재기동마다 0 으로 돌아가면 이미 넘은 것을 못 넘었다고 말한다. */
|
|
1665
1687
|
if (Number.isFinite(Number(e.demandHigh?.kW)) && e.demandHigh?.since)
|
|
1666
1688
|
this.demandHigh = { ...e.demandHigh };
|
|
@@ -1691,6 +1713,14 @@ export class EmsKernel extends FlowEngine {
|
|
|
1691
1713
|
...(this.lastBill ? { lastBill: { ...this.lastBill } } : {}),
|
|
1692
1714
|
...(this.tariffBasis ? { tariffBasis: { ...this.tariffBasis } } : {}),
|
|
1693
1715
|
...(this.generationPrice ? { generationPrice: { ...this.generationPrice } } : {}),
|
|
1716
|
+
/*
|
|
1717
|
+
* **예정된 선언도 함께 낸다.** 지금 유효한 것만 이어받으면, 재기동한 뒤 9월이 되어도 8월 단가로
|
|
1718
|
+
* 계산한다 — 9월 선언이 어디에도 남아 있지 않기 때문이다. 오늘 이 부류를 세 번 겪었다.
|
|
1719
|
+
*/
|
|
1720
|
+
...(this.tariffBasisSchedule?.length ? { tariffBasisSchedule: this.tariffBasisSchedule.map(d => ({ ...d })) } : {}),
|
|
1721
|
+
...(this.generationPriceSchedule?.length
|
|
1722
|
+
? { generationPriceSchedule: this.generationPriceSchedule.map(d => ({ ...d })) }
|
|
1723
|
+
: {}),
|
|
1694
1724
|
...(() => {
|
|
1695
1725
|
const r = this.generationRated();
|
|
1696
1726
|
return r ? { generationRated: r } : {};
|
|
@@ -1705,3 +1735,41 @@ export class EmsKernel extends FlowEngine {
|
|
|
1705
1735
|
return { ...snap, energy };
|
|
1706
1736
|
}
|
|
1707
1737
|
}
|
|
1738
|
+
/**
|
|
1739
|
+
* 선언 하나를 목록에 넣는다 — **같은 유효 구간이면 새것이 이긴다.**
|
|
1740
|
+
*
|
|
1741
|
+
* 사업자가 그 주기의 기준을 고칠 수 있다. 같은 구간에 두 선언이 남으면 어느 것이 맞는지 말할 수 없다.
|
|
1742
|
+
*
|
|
1743
|
+
* 목록은 시작 시각 순으로 두고 **24개까지** 든다. 선언은 주기마다 하나이므로 24개면 두 해치다 —
|
|
1744
|
+
* 그보다 오래된 것은 지난 기록에서 읽는다(상태는 「지금」을 위한 것이다).
|
|
1745
|
+
*/
|
|
1746
|
+
function putDeclaration(list, d) {
|
|
1747
|
+
const rest = (list ?? []).filter(x => !(x.from === d.from && x.to === d.to));
|
|
1748
|
+
const next = [...rest, { ...d }].sort((a, b) => Date.parse(a.from) - Date.parse(b.from));
|
|
1749
|
+
return next.length > 24 ? next.slice(next.length - 24) : next;
|
|
1750
|
+
}
|
|
1751
|
+
/**
|
|
1752
|
+
* **이 시각에 유효한 선언** — 시작했고 아직 끝나지 않은 것.
|
|
1753
|
+
*
|
|
1754
|
+
* 유효한 것이 없으면 답하지 않는다. 가장 가까운 것을 골라 주지 않는다 — 아직 시작하지 않은 단가로
|
|
1755
|
+
* 오늘을 계산하거나, 이미 끝난 단가를 지금 것인 양 쓰게 된다. 둘 다 오류 없이 틀린 값을 만든다.
|
|
1756
|
+
*
|
|
1757
|
+
* 구간이 겹치면 **나중에 시작한 것**이 이긴다(더 좁게 정한 선언이 앞선다).
|
|
1758
|
+
*/
|
|
1759
|
+
function inEffectAt(list, atMs) {
|
|
1760
|
+
let best;
|
|
1761
|
+
let bestFrom = -Infinity;
|
|
1762
|
+
for (const d of list ?? []) {
|
|
1763
|
+
const from = Date.parse(String(d.from));
|
|
1764
|
+
const to = Date.parse(String(d.to));
|
|
1765
|
+
if (!Number.isFinite(from) || !Number.isFinite(to))
|
|
1766
|
+
continue;
|
|
1767
|
+
if (atMs < from || atMs >= to)
|
|
1768
|
+
continue;
|
|
1769
|
+
if (from > bestFrom) {
|
|
1770
|
+
best = d;
|
|
1771
|
+
bestFrom = from;
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
return best;
|
|
1775
|
+
}
|
package/dist/energy-ingest.d.ts
CHANGED
|
@@ -35,6 +35,11 @@ export interface EnergyIngestOptions {
|
|
|
35
35
|
* 그때도 `twin-local` 이되 범위는 비어 있다.
|
|
36
36
|
*/
|
|
37
37
|
scopeId?: string;
|
|
38
|
+
/**
|
|
39
|
+
* **지금** — 아직 오지 않은 시각의 사실을 받지 않기 위해 쓴다. 주지 않으면 `defaultEventTime` 을 쓰고,
|
|
40
|
+
* 그것도 없으면 판단하지 않는다(없는 기준으로 거절하지 않는다).
|
|
41
|
+
*/
|
|
42
|
+
nowISO?: string;
|
|
38
43
|
}
|
|
39
44
|
export interface EnergyIngestResult {
|
|
40
45
|
accepted: CanonicalEnvelope[];
|
|
@@ -200,14 +205,6 @@ export interface EnergyBillRecord {
|
|
|
200
205
|
billingDemandKW?: number;
|
|
201
206
|
}
|
|
202
207
|
export declare function isEnergyBillRecord(record: unknown): boolean;
|
|
203
|
-
/**
|
|
204
|
-
* 이 사실의 **주체를 무엇으로 부를까** — 선언이 있으면 그 이름, 없으면 이름표와 그 범위.
|
|
205
|
-
*
|
|
206
|
-
* 선언이 없을 때 트윈 범위를 이름에 넣는 이유는 **겹침을 숨기지 않기 위해서**다. 넣지 않으면 서로 다른
|
|
207
|
-
* 설비의 같은 날이 한 사실이 되고, 여러 현장을 함께 계산하는 곳에서 한쪽이 조용히 사라진다(실측).
|
|
208
|
-
* 넣되 `subjectBasis` 로 **이 이름이 트윈 안에서만 통한다**고 밝힌다 — 그릇으로 정한 이름을 고유한
|
|
209
|
-
* 척하지 않는다.
|
|
210
|
-
*/
|
|
211
208
|
export declare function resolveSubject(kind: 'equipment' | 'meter', localId: string, opts: EnergyIngestOptions): {
|
|
212
209
|
subject: string;
|
|
213
210
|
basis: 'declared' | 'twin-local';
|
package/dist/energy-ingest.js
CHANGED
|
@@ -452,6 +452,11 @@ export function ingestEnergyGenerationPeriodRecords(records, opts) {
|
|
|
452
452
|
const subj = resolveSubject('equipment', equipmentId, opts);
|
|
453
453
|
data.subject = subj.subject;
|
|
454
454
|
data.subjectBasis = subj.basis;
|
|
455
|
+
const futureError = futureFactError(span.to, opts);
|
|
456
|
+
if (futureError) {
|
|
457
|
+
rejected.push({ record: r, errors: [futureError] });
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
455
460
|
accepted.push({
|
|
456
461
|
eventId: periodFactId(opts.tenantId, ENERGY_EVENT.generationPeriod, subj.subject, span.from, span.to),
|
|
457
462
|
eventType: ENERGY_EVENT.generationPeriod,
|
|
@@ -607,6 +612,35 @@ export function isEnergyBillRecord(record) {
|
|
|
607
612
|
* 넣되 `subjectBasis` 로 **이 이름이 트윈 안에서만 통한다**고 밝힌다 — 그릇으로 정한 이름을 고유한
|
|
608
613
|
* 척하지 않는다.
|
|
609
614
|
*/
|
|
615
|
+
/**
|
|
616
|
+
* **아직 오지 않은 시각의 사실은 받지 않는다.**
|
|
617
|
+
*
|
|
618
|
+
* ── 왜 필요한가 (2026-08-30) ──────────────────────────────────────────────
|
|
619
|
+
* 한 원본이 **끝나지 않은 이번 달**의 요금 정보를 청구서로 보냈다. 청구서의 사건 시각은 기간의 끝이라
|
|
620
|
+
* 그 사실이 **내일** 일어난 것으로 저널에 적혔다. 성과 계산은 「가장 늦은 사건」으로 구간을 잡으므로
|
|
621
|
+
* 구간 전체가 미래로 밀렸고, 그 현장의 화면에서 어제 발전량이 사라졌다. 오류는 하나도 나지 않았다.
|
|
622
|
+
*
|
|
623
|
+
* 지난 기록 채우기는 이미 이 판단을 하고 있었다(§`judgeBackfill`). 라이브 문에만 없어서 **한 종류에
|
|
624
|
+
* 규칙이 두 벌**이었다. 같은 사실이 어느 문으로 들어오느냐에 따라 받아지기도 하고 거절되기도 했다.
|
|
625
|
+
*
|
|
626
|
+
* ── 기준 선언은 이 판정을 받지 않는다 (2026-08-30, 두 번째 판단) ────────────
|
|
627
|
+
* 요금 기준·발전 단가는 **예정될 수 있다.** 한전은 다음 달 요금을 미리 공표하고, 공정 조건도 시행일을
|
|
628
|
+
* 앞두고 정해진다. 「9월부터 이 단가」는 오늘 성립하는 사실이다.
|
|
629
|
+
*
|
|
630
|
+
* 처음에는 이 둘도 거부했다. 그러면 정상적인 사실이 막힌다. 받되, **예정된 선언이 지금 것을 덮지 않게**
|
|
631
|
+
* 커널이 유효 구간으로 고른다(§`EnergyState.tariffBasisSchedule`). 받아 놓고 아무 데나 쓰면
|
|
632
|
+
* 오늘 요금이 다음 달 단가로 계산된다 — 그것이 이 문을 열기 전에 먼저 만들어야 했던 자리다.
|
|
633
|
+
*/
|
|
634
|
+
function futureFactError(eventTime, opts) {
|
|
635
|
+
const nowText = opts.nowISO ?? opts.defaultEventTime;
|
|
636
|
+
if (!nowText)
|
|
637
|
+
return undefined;
|
|
638
|
+
const nowMs = Date.parse(nowText);
|
|
639
|
+
const atMs = Date.parse(eventTime);
|
|
640
|
+
if (!Number.isFinite(nowMs) || !Number.isFinite(atMs) || atMs <= nowMs)
|
|
641
|
+
return undefined;
|
|
642
|
+
return `아직 오지 않은 시각의 사실이다(${eventTime}) — 끝나지 않은 기간은 마감된 사실이 아니다`;
|
|
643
|
+
}
|
|
610
644
|
export function resolveSubject(kind, localId, opts) {
|
|
611
645
|
const declared = opts.identityOf?.(kind, localId);
|
|
612
646
|
if (declared)
|
|
@@ -699,6 +733,11 @@ export function ingestEnergyUsagePeriodRecords(records, opts) {
|
|
|
699
733
|
...(currency ? { currency } : {}),
|
|
700
734
|
...(basis ? { basis } : {})
|
|
701
735
|
};
|
|
736
|
+
const futureError = futureFactError(span.to, opts);
|
|
737
|
+
if (futureError) {
|
|
738
|
+
rejected.push({ record: r, errors: [futureError] });
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
702
741
|
accepted.push({
|
|
703
742
|
eventId: periodFactId(opts.tenantId, ENERGY_EVENT.usagePeriod, subj.subject, span.from, span.to),
|
|
704
743
|
eventType: ENERGY_EVENT.usagePeriod,
|
|
@@ -745,6 +784,11 @@ export function ingestEnergyBillRecords(records, opts) {
|
|
|
745
784
|
...(currency ? { currency } : {}),
|
|
746
785
|
...(billingDemandKW !== undefined ? { billingDemandKW } : {})
|
|
747
786
|
};
|
|
787
|
+
const futureError = futureFactError(span.to, opts);
|
|
788
|
+
if (futureError) {
|
|
789
|
+
rejected.push({ record: r, errors: [futureError] });
|
|
790
|
+
continue;
|
|
791
|
+
}
|
|
748
792
|
accepted.push({
|
|
749
793
|
eventId: periodFactId(opts.tenantId, ENERGY_EVENT.bill, '', span.from, span.to),
|
|
750
794
|
eventType: ENERGY_EVENT.bill,
|
package/dist-cjs/index.cjs
CHANGED
|
@@ -2824,6 +2824,11 @@ function ingestEnergyGenerationPeriodRecords(records, opts) {
|
|
|
2824
2824
|
const subj = resolveSubject("equipment", equipmentId, opts);
|
|
2825
2825
|
data.subject = subj.subject;
|
|
2826
2826
|
data.subjectBasis = subj.basis;
|
|
2827
|
+
const futureError = futureFactError(span.to, opts);
|
|
2828
|
+
if (futureError) {
|
|
2829
|
+
rejected.push({ record: r, errors: [futureError] });
|
|
2830
|
+
continue;
|
|
2831
|
+
}
|
|
2827
2832
|
accepted.push({
|
|
2828
2833
|
eventId: periodFactId(opts.tenantId, ENERGY_EVENT.generationPeriod, subj.subject, span.from, span.to),
|
|
2829
2834
|
eventType: ENERGY_EVENT.generationPeriod,
|
|
@@ -2919,6 +2924,14 @@ function isEnergyBillRecord(record) {
|
|
|
2919
2924
|
if (r.from === void 0 || r.to === void 0) return false;
|
|
2920
2925
|
return r.energyCharge !== void 0 || r.demandCharge !== void 0 || r.total !== void 0;
|
|
2921
2926
|
}
|
|
2927
|
+
function futureFactError(eventTime, opts) {
|
|
2928
|
+
const nowText = opts.nowISO ?? opts.defaultEventTime;
|
|
2929
|
+
if (!nowText) return void 0;
|
|
2930
|
+
const nowMs = Date.parse(nowText);
|
|
2931
|
+
const atMs = Date.parse(eventTime);
|
|
2932
|
+
if (!Number.isFinite(nowMs) || !Number.isFinite(atMs) || atMs <= nowMs) return void 0;
|
|
2933
|
+
return `\uC544\uC9C1 \uC624\uC9C0 \uC54A\uC740 \uC2DC\uAC01\uC758 \uC0AC\uC2E4\uC774\uB2E4(${eventTime}) \u2014 \uB05D\uB098\uC9C0 \uC54A\uC740 \uAE30\uAC04\uC740 \uB9C8\uAC10\uB41C \uC0AC\uC2E4\uC774 \uC544\uB2C8\uB2E4`;
|
|
2934
|
+
}
|
|
2922
2935
|
function resolveSubject(kind, localId, opts) {
|
|
2923
2936
|
const declared = opts.identityOf?.(kind, localId);
|
|
2924
2937
|
if (declared) return { subject: declared, basis: "declared" };
|
|
@@ -2997,6 +3010,11 @@ function ingestEnergyUsagePeriodRecords(records, opts) {
|
|
|
2997
3010
|
...currency ? { currency } : {},
|
|
2998
3011
|
...basis ? { basis } : {}
|
|
2999
3012
|
};
|
|
3013
|
+
const futureError = futureFactError(span.to, opts);
|
|
3014
|
+
if (futureError) {
|
|
3015
|
+
rejected.push({ record: r, errors: [futureError] });
|
|
3016
|
+
continue;
|
|
3017
|
+
}
|
|
3000
3018
|
accepted.push({
|
|
3001
3019
|
eventId: periodFactId(opts.tenantId, ENERGY_EVENT.usagePeriod, subj.subject, span.from, span.to),
|
|
3002
3020
|
eventType: ENERGY_EVENT.usagePeriod,
|
|
@@ -3037,6 +3055,11 @@ function ingestEnergyBillRecords(records, opts) {
|
|
|
3037
3055
|
...currency ? { currency } : {},
|
|
3038
3056
|
...billingDemandKW !== void 0 ? { billingDemandKW } : {}
|
|
3039
3057
|
};
|
|
3058
|
+
const futureError = futureFactError(span.to, opts);
|
|
3059
|
+
if (futureError) {
|
|
3060
|
+
rejected.push({ record: r, errors: [futureError] });
|
|
3061
|
+
continue;
|
|
3062
|
+
}
|
|
3040
3063
|
accepted.push({
|
|
3041
3064
|
eventId: periodFactId(opts.tenantId, ENERGY_EVENT.bill, "", span.from, span.to),
|
|
3042
3065
|
eventType: ENERGY_EVENT.bill,
|
|
@@ -8640,11 +8663,8 @@ var EmsKernel = class extends FlowEngine {
|
|
|
8640
8663
|
applyGenerationPrice(envelope) {
|
|
8641
8664
|
const d = envelope.data;
|
|
8642
8665
|
if (!d?.from || !d?.to) return;
|
|
8643
|
-
|
|
8644
|
-
|
|
8645
|
-
const priorFromMs = Date.parse(String(this.generationPrice?.from ?? ""));
|
|
8646
|
-
if (Number.isFinite(priorFromMs) && fromMs < priorFromMs) return;
|
|
8647
|
-
this.generationPrice = { ...d };
|
|
8666
|
+
this.generationPriceSchedule = putDeclaration(this.generationPriceSchedule, d);
|
|
8667
|
+
this.generationPrice = inEffectAt(this.generationPriceSchedule, this.nowMs());
|
|
8648
8668
|
this.revision++;
|
|
8649
8669
|
}
|
|
8650
8670
|
/**
|
|
@@ -8656,13 +8676,25 @@ var EmsKernel = class extends FlowEngine {
|
|
|
8656
8676
|
applyTariffBasis(envelope) {
|
|
8657
8677
|
const d = envelope.data;
|
|
8658
8678
|
if (!d?.from || !d?.to) return;
|
|
8659
|
-
|
|
8660
|
-
|
|
8661
|
-
const priorFromMs = Date.parse(String(this.tariffBasis?.from ?? ""));
|
|
8662
|
-
if (Number.isFinite(priorFromMs) && fromMs < priorFromMs) return;
|
|
8663
|
-
this.tariffBasis = { ...d };
|
|
8679
|
+
this.tariffBasisSchedule = putDeclaration(this.tariffBasisSchedule, d);
|
|
8680
|
+
this.tariffBasis = inEffectAt(this.tariffBasisSchedule, this.nowMs());
|
|
8664
8681
|
this.revision++;
|
|
8665
8682
|
}
|
|
8683
|
+
/**
|
|
8684
|
+
* 시계가 선언의 경계를 넘으면 **지금 유효한 선언이 바뀐다.**
|
|
8685
|
+
*
|
|
8686
|
+
* 표본이 들어올 때만 다시 고르면, 9월 1일 0시에 단가가 바뀌었는데 다음 표본이 올 때까지 8월 단가로
|
|
8687
|
+
* 계산한다. 태양광은 밤에 표본이 없어 그 간격이 길다.
|
|
8688
|
+
*/
|
|
8689
|
+
refreshDeclarationsInEffect(nowMs) {
|
|
8690
|
+
const tariff = inEffectAt(this.tariffBasisSchedule, nowMs);
|
|
8691
|
+
const price = inEffectAt(this.generationPriceSchedule, nowMs);
|
|
8692
|
+
if (tariff !== this.tariffBasis || price !== this.generationPrice) {
|
|
8693
|
+
this.tariffBasis = tariff;
|
|
8694
|
+
this.generationPrice = price;
|
|
8695
|
+
this.revision++;
|
|
8696
|
+
}
|
|
8697
|
+
}
|
|
8666
8698
|
/**
|
|
8667
8699
|
* 청구서를 상태에 둔다 — **우리 계산을 덮지 않는다.**
|
|
8668
8700
|
*
|
|
@@ -8881,8 +8913,12 @@ var EmsKernel = class extends FlowEngine {
|
|
|
8881
8913
|
lastBill;
|
|
8882
8914
|
/** 마지막으로 받은 요금 기준 — 이 주기에 적용되는 요금적용전력과 단가. */
|
|
8883
8915
|
tariffBasis;
|
|
8916
|
+
/** 선언된 요금 기준들 — 예정된 것을 포함한다(§`EnergyState.tariffBasisSchedule`). */
|
|
8917
|
+
tariffBasisSchedule;
|
|
8884
8918
|
/** 마지막으로 받은 발전 단가 — 이 기간에 낸 전기 1kWh 의 값. */
|
|
8885
8919
|
generationPrice;
|
|
8920
|
+
/** 선언된 발전 단가들 — 예정된 것을 포함한다. */
|
|
8921
|
+
generationPriceSchedule;
|
|
8886
8922
|
/**
|
|
8887
8923
|
* 이 청구 주기의 **최고 수요** — 기본요금이 이 값으로 다시 정해진다.
|
|
8888
8924
|
*
|
|
@@ -9519,6 +9555,7 @@ var EmsKernel = class extends FlowEngine {
|
|
|
9519
9555
|
super.tick(dtMs);
|
|
9520
9556
|
const at = this.nowMs();
|
|
9521
9557
|
this.closeGenerationPeriodsDue(at);
|
|
9558
|
+
this.refreshDeclarationsInEffect(at);
|
|
9522
9559
|
this.deriveLoad(at, dtMs);
|
|
9523
9560
|
this.closeDue(at);
|
|
9524
9561
|
}
|
|
@@ -9558,8 +9595,13 @@ var EmsKernel = class extends FlowEngine {
|
|
|
9558
9595
|
}
|
|
9559
9596
|
if (Number.isFinite(e.unclosedGenerationPeriods)) this.unclosedGenerationPeriods = Number(e.unclosedGenerationPeriods);
|
|
9560
9597
|
if (e.lastBill?.from && e.lastBill?.to) this.lastBill = { ...e.lastBill };
|
|
9598
|
+
if (Array.isArray(e.tariffBasisSchedule)) this.tariffBasisSchedule = e.tariffBasisSchedule.map((d) => ({ ...d }));
|
|
9599
|
+
if (Array.isArray(e.generationPriceSchedule)) this.generationPriceSchedule = e.generationPriceSchedule.map((d) => ({ ...d }));
|
|
9561
9600
|
if (e.tariffBasis?.from && e.tariffBasis?.to) this.tariffBasis = { ...e.tariffBasis };
|
|
9562
9601
|
if (e.generationPrice?.from && e.generationPrice?.to) this.generationPrice = { ...e.generationPrice };
|
|
9602
|
+
if (this.tariffBasisSchedule?.length || this.generationPriceSchedule?.length) {
|
|
9603
|
+
this.refreshDeclarationsInEffect(this.nowMs());
|
|
9604
|
+
}
|
|
9563
9605
|
if (Number.isFinite(Number(e.demandHigh?.kW)) && e.demandHigh?.since) this.demandHigh = { ...e.demandHigh };
|
|
9564
9606
|
}
|
|
9565
9607
|
getSnapshot() {
|
|
@@ -9584,6 +9626,12 @@ var EmsKernel = class extends FlowEngine {
|
|
|
9584
9626
|
...this.lastBill ? { lastBill: { ...this.lastBill } } : {},
|
|
9585
9627
|
...this.tariffBasis ? { tariffBasis: { ...this.tariffBasis } } : {},
|
|
9586
9628
|
...this.generationPrice ? { generationPrice: { ...this.generationPrice } } : {},
|
|
9629
|
+
/*
|
|
9630
|
+
* **예정된 선언도 함께 낸다.** 지금 유효한 것만 이어받으면, 재기동한 뒤 9월이 되어도 8월 단가로
|
|
9631
|
+
* 계산한다 — 9월 선언이 어디에도 남아 있지 않기 때문이다. 오늘 이 부류를 세 번 겪었다.
|
|
9632
|
+
*/
|
|
9633
|
+
...this.tariffBasisSchedule?.length ? { tariffBasisSchedule: this.tariffBasisSchedule.map((d) => ({ ...d })) } : {},
|
|
9634
|
+
...this.generationPriceSchedule?.length ? { generationPriceSchedule: this.generationPriceSchedule.map((d) => ({ ...d })) } : {},
|
|
9587
9635
|
...(() => {
|
|
9588
9636
|
const r = this.generationRated();
|
|
9589
9637
|
return r ? { generationRated: r } : {};
|
|
@@ -9596,6 +9644,26 @@ var EmsKernel = class extends FlowEngine {
|
|
|
9596
9644
|
return { ...snap, energy };
|
|
9597
9645
|
}
|
|
9598
9646
|
};
|
|
9647
|
+
function putDeclaration(list, d) {
|
|
9648
|
+
const rest = (list ?? []).filter((x) => !(x.from === d.from && x.to === d.to));
|
|
9649
|
+
const next = [...rest, { ...d }].sort((a, b) => Date.parse(a.from) - Date.parse(b.from));
|
|
9650
|
+
return next.length > 24 ? next.slice(next.length - 24) : next;
|
|
9651
|
+
}
|
|
9652
|
+
function inEffectAt(list, atMs) {
|
|
9653
|
+
let best;
|
|
9654
|
+
let bestFrom = -Infinity;
|
|
9655
|
+
for (const d of list ?? []) {
|
|
9656
|
+
const from = Date.parse(String(d.from));
|
|
9657
|
+
const to = Date.parse(String(d.to));
|
|
9658
|
+
if (!Number.isFinite(from) || !Number.isFinite(to)) continue;
|
|
9659
|
+
if (atMs < from || atMs >= to) continue;
|
|
9660
|
+
if (from > bestFrom) {
|
|
9661
|
+
best = d;
|
|
9662
|
+
bestFrom = from;
|
|
9663
|
+
}
|
|
9664
|
+
}
|
|
9665
|
+
return best;
|
|
9666
|
+
}
|
|
9599
9667
|
|
|
9600
9668
|
// src/event-journal.ts
|
|
9601
9669
|
var DOMAIN_KERNELS = { wms: WmsKernel, yms: YmsKernel, mes: MesKernel, ems: EmsKernel };
|
package/package.json
CHANGED