@operato/twin-kernel 0.7.87 → 0.7.89
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 +38 -1
- package/dist/contract.js +2 -12
- package/dist/ems-kernel.d.ts +11 -0
- package/dist/ems-kernel.js +92 -14
- package/dist/energy-ingest.js +7 -12
- package/dist-cjs/index.cjs +58 -22
- package/package.json +1 -1
package/dist/contract.d.ts
CHANGED
|
@@ -671,7 +671,27 @@ export type Effectivity = 'not-yet' | 'expired';
|
|
|
671
671
|
* `at` 를 주지 않으면 **판단하지 않는다**(`undefined`). 모르는 시각으로 폐기를 단정하면, 시각을 안 넘긴
|
|
672
672
|
* 소비처 전부가 자원을 잃는다. 파싱 불가한 시각도 같다 — 짐작해 고치지 않는다.
|
|
673
673
|
*/
|
|
674
|
-
|
|
674
|
+
/**
|
|
675
|
+
* 끝나는 시각을 **포함하는가** — 축마다 다르다. 규칙은 한 벌이고 규약만 밝힌다.
|
|
676
|
+
*
|
|
677
|
+
* ── 왜 이 칸이 생겼나 (2026-08-30) ────────────────────────────────────────
|
|
678
|
+
* 처음에는 이 판정이 한 곳뿐이었고 끝을 포함했다. 자원에는 그것이 맞다 — 폐기 일자가 있는 지게차는
|
|
679
|
+
* 띄엄띄엄 있고, 두 자원의 기간이 한 순간을 함께 갖는 일이 문제가 되지 않는다.
|
|
680
|
+
*
|
|
681
|
+
* 그런데 **선언은 빈틈없이 이어진다.** 8월 요금 주기가 끝나는 순간이 9월 주기가 시작하는 순간이다.
|
|
682
|
+
* 끝을 포함하면 그 한 순간에 두 단가가 동시에 유효해지고, 어느 것으로 계산했는지 말할 수 없다.
|
|
683
|
+
*
|
|
684
|
+
* 그때 같은 판정을 하는 함수를 하나 더 만들었다. 규칙이 두 벌이 되면 한쪽만 고쳐지는 날이 온다 —
|
|
685
|
+
* 이 저장소가 오늘 하루 그 부류를 여러 번 고쳤다. 그래서 함수는 하나로 두고 **규약을 인자로** 받는다.
|
|
686
|
+
*/
|
|
687
|
+
export interface EffectivityOptions {
|
|
688
|
+
/**
|
|
689
|
+
* `'inclusive'`(기본) — 끝나는 시각까지 유효하다. 자원의 유효 기간이 이쪽이다.
|
|
690
|
+
* `'exclusive'` — 끝나는 시각은 다음 구간의 것이다. 이어지는 주기(요금·단가)가 이쪽이다.
|
|
691
|
+
*/
|
|
692
|
+
end?: 'inclusive' | 'exclusive';
|
|
693
|
+
}
|
|
694
|
+
export declare function effectivityAt(p: EffectivePeriod | undefined, at?: ISOTime, opts?: EffectivityOptions): Effectivity | undefined;
|
|
675
695
|
/**
|
|
676
696
|
* 자원 **등급 정의** — ISA-95 가 세 자원에 똑같이 정의한 한 모양.
|
|
677
697
|
*
|
|
@@ -2041,6 +2061,19 @@ export interface EnergyState {
|
|
|
2041
2061
|
* 청구서보다 이쪽이 먼저다: 청구서는 끝난 기간의 정산이고 이것은 지금 적용되는 값이다.
|
|
2042
2062
|
*/
|
|
2043
2063
|
tariffBasis?: EnergyTariffBasisData;
|
|
2064
|
+
/**
|
|
2065
|
+
* **선언된 요금 기준들** — 유효 구간별. `tariffBasis` 는 이 중 지금 유효한 것이다.
|
|
2066
|
+
*
|
|
2067
|
+
* ── 왜 여럿을 드나 (2026-08-30) ──────────────────────────────────────────
|
|
2068
|
+
* 선언은 **예정될 수 있다.** 한전은 다음 달 요금을 미리 공표한다. 하나만 들면 9월 선언이 들어오는
|
|
2069
|
+
* 순간 8월 선언을 덮어써서 **오늘 요금이 다음 달 단가로 계산된다.** 오류는 나지 않는다.
|
|
2070
|
+
*
|
|
2071
|
+
* 처음에는 예정된 선언을 유입에서 거부해 이 상황을 막았다. 그것은 정상적인 사실을 막는 것이라,
|
|
2072
|
+
* 담을 자리를 만들고 거부를 풀었다.
|
|
2073
|
+
*
|
|
2074
|
+
* 고르는 규칙은 **커널에 한 벌만 둔다.** 읽는 쪽이 각자 고르면 화면마다 다른 요금이 나온다.
|
|
2075
|
+
*/
|
|
2076
|
+
tariffBasisSchedule?: EnergyTariffBasisData[];
|
|
2044
2077
|
/**
|
|
2045
2078
|
* 이 트윈의 **발전 정격**과 그 근거 — 이용률의 분모(§ `generationRated`).
|
|
2046
2079
|
*
|
|
@@ -2056,8 +2089,12 @@ export interface EnergyState {
|
|
|
2056
2089
|
* 마지막으로 받은 **발전 단가** — 이 기간에 낸 전기 1kWh 의 값.
|
|
2057
2090
|
*
|
|
2058
2091
|
* 창 밖의 단가도 이 창에 적용될 수 있어(단가는 기간마다 정해진다) 상태로 이어 간다.
|
|
2092
|
+
*
|
|
2093
|
+
* 예정된 단가는 `generationPriceSchedule` 에 함께 든다 — 이 칸은 **지금 유효한 것**이다.
|
|
2059
2094
|
*/
|
|
2060
2095
|
generationPrice?: EnergyGenerationPriceData;
|
|
2096
|
+
/** **선언된 발전 단가들** — 유효 구간별. 예정된 것을 담는다(§`tariffBasisSchedule`). */
|
|
2097
|
+
generationPriceSchedule?: EnergyGenerationPriceData[];
|
|
2061
2098
|
/**
|
|
2062
2099
|
* 이 청구 주기의 **최고 수요** — 기본요금이 이 값으로 다시 정해진다.
|
|
2063
2100
|
*
|
package/dist/contract.js
CHANGED
|
@@ -376,17 +376,7 @@ export function judgeAgainstSpec(spec, result) {
|
|
|
376
376
|
}
|
|
377
377
|
return allJudged ? 'pass' : undefined;
|
|
378
378
|
}
|
|
379
|
-
|
|
380
|
-
* 이 시각에 유효 기간 밖인가 — **한 규칙**으로 개체·등급·설비↔자산 매핑을 모두 판정한다.
|
|
381
|
-
*
|
|
382
|
-
* **표준은 날짜만 정하고 "밖이면 어떻게 되는가" 는 정하지 않는다.** 그 판단은 우리 것이므로 여기 밝힌다:
|
|
383
|
-
* 기간 밖이면 **그 자원은 그 시각의 모델에 참여하지 않는다**(배정되지 않고, 가용 분모에 들지 않는다).
|
|
384
|
-
* 지우지는 않는다 — 이유를 달아 남긴다. 조용히 사라지면 "왜 없어졌나" 를 아무도 답할 수 없다.
|
|
385
|
-
*
|
|
386
|
-
* `at` 를 주지 않으면 **판단하지 않는다**(`undefined`). 모르는 시각으로 폐기를 단정하면, 시각을 안 넘긴
|
|
387
|
-
* 소비처 전부가 자원을 잃는다. 파싱 불가한 시각도 같다 — 짐작해 고치지 않는다.
|
|
388
|
-
*/
|
|
389
|
-
export function effectivityAt(p, at) {
|
|
379
|
+
export function effectivityAt(p, at, opts) {
|
|
390
380
|
if (!p || !at)
|
|
391
381
|
return undefined;
|
|
392
382
|
const atMs = parsedMs(at);
|
|
@@ -396,7 +386,7 @@ export function effectivityAt(p, at) {
|
|
|
396
386
|
if (Number.isFinite(from) && atMs < from)
|
|
397
387
|
return 'not-yet';
|
|
398
388
|
const to = p.effectiveEnd ? parsedMs(p.effectiveEnd) : NaN;
|
|
399
|
-
if (Number.isFinite(to) && atMs > to)
|
|
389
|
+
if (Number.isFinite(to) && (opts?.end === 'exclusive' ? atMs >= to : atMs > to))
|
|
400
390
|
return 'expired';
|
|
401
391
|
return undefined;
|
|
402
392
|
}
|
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
|
@@ -28,6 +28,8 @@
|
|
|
28
28
|
import { FlowEngine } from "./flow-engine.js";
|
|
29
29
|
/* 기간 사실의 이름은 유입 문과 한 규칙이다(§`periodFactId`). */
|
|
30
30
|
import { periodFactId } from "./energy-ingest.js";
|
|
31
|
+
/* 「이 시각에 유효한가」는 커널에 규칙이 한 벌이다 — 선언 축도 그것을 쓴다. */
|
|
32
|
+
import { effectivityAt } from "./contract.js";
|
|
31
33
|
import { firstFitPolicy } from "./allocation-policy.js";
|
|
32
34
|
import { CMD, ENERGY_EVENT, minuteOfDayAt, localDayIndexAt, localDayStartMs } from "./contract.js";
|
|
33
35
|
import { EMS_PROPERTY, METER_DIRECTION, electricalUpstreamOf, generationFractionAt } from "./ems-profile.js";
|
|
@@ -354,13 +356,8 @@ export class EmsKernel extends FlowEngine {
|
|
|
354
356
|
const d = envelope.data;
|
|
355
357
|
if (!d?.from || !d?.to)
|
|
356
358
|
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 };
|
|
359
|
+
this.generationPriceSchedule = putDeclaration(this.generationPriceSchedule, d);
|
|
360
|
+
this.generationPrice = inEffectAt(this.generationPriceSchedule, this.nowMs());
|
|
364
361
|
this.revision++;
|
|
365
362
|
}
|
|
366
363
|
/**
|
|
@@ -373,15 +370,25 @@ export class EmsKernel extends FlowEngine {
|
|
|
373
370
|
const d = envelope.data;
|
|
374
371
|
if (!d?.from || !d?.to)
|
|
375
372
|
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 };
|
|
373
|
+
this.tariffBasisSchedule = putDeclaration(this.tariffBasisSchedule, d);
|
|
374
|
+
this.tariffBasis = inEffectAt(this.tariffBasisSchedule, this.nowMs());
|
|
383
375
|
this.revision++;
|
|
384
376
|
}
|
|
377
|
+
/**
|
|
378
|
+
* 시계가 선언의 경계를 넘으면 **지금 유효한 선언이 바뀐다.**
|
|
379
|
+
*
|
|
380
|
+
* 표본이 들어올 때만 다시 고르면, 9월 1일 0시에 단가가 바뀌었는데 다음 표본이 올 때까지 8월 단가로
|
|
381
|
+
* 계산한다. 태양광은 밤에 표본이 없어 그 간격이 길다.
|
|
382
|
+
*/
|
|
383
|
+
refreshDeclarationsInEffect(nowMs) {
|
|
384
|
+
const tariff = inEffectAt(this.tariffBasisSchedule, nowMs);
|
|
385
|
+
const price = inEffectAt(this.generationPriceSchedule, nowMs);
|
|
386
|
+
if (tariff !== this.tariffBasis || price !== this.generationPrice) {
|
|
387
|
+
this.tariffBasis = tariff;
|
|
388
|
+
this.generationPrice = price;
|
|
389
|
+
this.revision++;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
385
392
|
/**
|
|
386
393
|
* 청구서를 상태에 둔다 — **우리 계산을 덮지 않는다.**
|
|
387
394
|
*
|
|
@@ -713,8 +720,12 @@ export class EmsKernel extends FlowEngine {
|
|
|
713
720
|
lastBill;
|
|
714
721
|
/** 마지막으로 받은 요금 기준 — 이 주기에 적용되는 요금적용전력과 단가. */
|
|
715
722
|
tariffBasis;
|
|
723
|
+
/** 선언된 요금 기준들 — 예정된 것을 포함한다(§`EnergyState.tariffBasisSchedule`). */
|
|
724
|
+
tariffBasisSchedule;
|
|
716
725
|
/** 마지막으로 받은 발전 단가 — 이 기간에 낸 전기 1kWh 의 값. */
|
|
717
726
|
generationPrice;
|
|
727
|
+
/** 선언된 발전 단가들 — 예정된 것을 포함한다. */
|
|
728
|
+
generationPriceSchedule;
|
|
718
729
|
/**
|
|
719
730
|
* 이 청구 주기의 **최고 수요** — 기본요금이 이 값으로 다시 정해진다.
|
|
720
731
|
*
|
|
@@ -1599,6 +1610,8 @@ export class EmsKernel extends FlowEngine {
|
|
|
1599
1610
|
* 계량은 이미 시계로 마감한다(`closeDue`). 한 커널 안에서 규칙이 두 벌일 이유가 없다.
|
|
1600
1611
|
*/
|
|
1601
1612
|
this.closeGenerationPeriodsDue(at);
|
|
1613
|
+
/* 시계가 선언의 경계를 넘으면 지금 유효한 선언이 바뀐다 — 표본을 기다리지 않는다. */
|
|
1614
|
+
this.refreshDeclarationsInEffect(at);
|
|
1602
1615
|
/* 틱 길이를 함께 넘긴다 — 방전량을 에너지로 바꿀 때 필요하다(필드로 숨기면 어디서 온 값인지 흐려진다). */
|
|
1603
1616
|
this.deriveLoad(at, dtMs);
|
|
1604
1617
|
this.closeDue(at);
|
|
@@ -1657,10 +1670,21 @@ export class EmsKernel extends FlowEngine {
|
|
|
1657
1670
|
/* 청구서도 이어받는다 — 재기동마다 사라지면 우리 계산과 견줄 기준이 없어진다. */
|
|
1658
1671
|
if (e.lastBill?.from && e.lastBill?.to)
|
|
1659
1672
|
this.lastBill = { ...e.lastBill };
|
|
1673
|
+
/*
|
|
1674
|
+
* 선언은 **목록째** 이어받는다 — 예정된 것이 사라지면 재기동 뒤에 시행일이 와도 옛 단가를 쓴다.
|
|
1675
|
+
* 이어받은 뒤 지금 시각으로 다시 고른다: 멈춰 있는 동안 시행일이 지났을 수 있다.
|
|
1676
|
+
*/
|
|
1677
|
+
if (Array.isArray(e.tariffBasisSchedule))
|
|
1678
|
+
this.tariffBasisSchedule = e.tariffBasisSchedule.map(d => ({ ...d }));
|
|
1679
|
+
if (Array.isArray(e.generationPriceSchedule))
|
|
1680
|
+
this.generationPriceSchedule = e.generationPriceSchedule.map(d => ({ ...d }));
|
|
1660
1681
|
if (e.tariffBasis?.from && e.tariffBasis?.to)
|
|
1661
1682
|
this.tariffBasis = { ...e.tariffBasis };
|
|
1662
1683
|
if (e.generationPrice?.from && e.generationPrice?.to)
|
|
1663
1684
|
this.generationPrice = { ...e.generationPrice };
|
|
1685
|
+
if (this.tariffBasisSchedule?.length || this.generationPriceSchedule?.length) {
|
|
1686
|
+
this.refreshDeclarationsInEffect(this.nowMs());
|
|
1687
|
+
}
|
|
1664
1688
|
/* 이 주기의 최고도 이어받는다 — 재기동마다 0 으로 돌아가면 이미 넘은 것을 못 넘었다고 말한다. */
|
|
1665
1689
|
if (Number.isFinite(Number(e.demandHigh?.kW)) && e.demandHigh?.since)
|
|
1666
1690
|
this.demandHigh = { ...e.demandHigh };
|
|
@@ -1691,6 +1715,14 @@ export class EmsKernel extends FlowEngine {
|
|
|
1691
1715
|
...(this.lastBill ? { lastBill: { ...this.lastBill } } : {}),
|
|
1692
1716
|
...(this.tariffBasis ? { tariffBasis: { ...this.tariffBasis } } : {}),
|
|
1693
1717
|
...(this.generationPrice ? { generationPrice: { ...this.generationPrice } } : {}),
|
|
1718
|
+
/*
|
|
1719
|
+
* **예정된 선언도 함께 낸다.** 지금 유효한 것만 이어받으면, 재기동한 뒤 9월이 되어도 8월 단가로
|
|
1720
|
+
* 계산한다 — 9월 선언이 어디에도 남아 있지 않기 때문이다. 오늘 이 부류를 세 번 겪었다.
|
|
1721
|
+
*/
|
|
1722
|
+
...(this.tariffBasisSchedule?.length ? { tariffBasisSchedule: this.tariffBasisSchedule.map(d => ({ ...d })) } : {}),
|
|
1723
|
+
...(this.generationPriceSchedule?.length
|
|
1724
|
+
? { generationPriceSchedule: this.generationPriceSchedule.map(d => ({ ...d })) }
|
|
1725
|
+
: {}),
|
|
1694
1726
|
...(() => {
|
|
1695
1727
|
const r = this.generationRated();
|
|
1696
1728
|
return r ? { generationRated: r } : {};
|
|
@@ -1705,3 +1737,49 @@ export class EmsKernel extends FlowEngine {
|
|
|
1705
1737
|
return { ...snap, energy };
|
|
1706
1738
|
}
|
|
1707
1739
|
}
|
|
1740
|
+
/**
|
|
1741
|
+
* 선언 하나를 목록에 넣는다 — **같은 유효 구간이면 새것이 이긴다.**
|
|
1742
|
+
*
|
|
1743
|
+
* 사업자가 그 주기의 기준을 고칠 수 있다. 같은 구간에 두 선언이 남으면 어느 것이 맞는지 말할 수 없다.
|
|
1744
|
+
*
|
|
1745
|
+
* 목록은 시작 시각 순으로 두고 **24개까지** 든다. 선언은 주기마다 하나이므로 24개면 두 해치다 —
|
|
1746
|
+
* 그보다 오래된 것은 지난 기록에서 읽는다(상태는 「지금」을 위한 것이다).
|
|
1747
|
+
*/
|
|
1748
|
+
function putDeclaration(list, d) {
|
|
1749
|
+
const rest = (list ?? []).filter(x => !(x.from === d.from && x.to === d.to));
|
|
1750
|
+
const next = [...rest, { ...d }].sort((a, b) => Date.parse(a.from) - Date.parse(b.from));
|
|
1751
|
+
return next.length > 24 ? next.slice(next.length - 24) : next;
|
|
1752
|
+
}
|
|
1753
|
+
/**
|
|
1754
|
+
* **이 시각에 유효한 선언** — 판정은 커널의 한 규칙이 한다(§`effectivityAt`).
|
|
1755
|
+
*
|
|
1756
|
+
* 여기서 하는 일은 **고르는 것**뿐이다. 「유효한가」를 여기서 다시 판정하면 규칙이 두 벌이 되고,
|
|
1757
|
+
* 한쪽만 고쳐지는 날이 온다. 실제로 이 함수는 처음에 자기 판정을 들고 있었다.
|
|
1758
|
+
*
|
|
1759
|
+
* 선언은 **빈틈없이 이어진다** — 8월 주기가 끝나는 순간이 9월 주기가 시작하는 순간이다. 그래서 끝을
|
|
1760
|
+
* 포함하지 않는다(`end: 'exclusive'`). 포함하면 그 한 순간에 두 단가가 동시에 유효해진다.
|
|
1761
|
+
*
|
|
1762
|
+
* 유효한 것이 없으면 답하지 않는다. 가장 가까운 것을 골라 주지 않는다 — 아직 시작하지 않은 단가로
|
|
1763
|
+
* 오늘을 계산하거나 이미 끝난 단가를 지금 것인 양 쓰게 된다. 둘 다 오류 없이 틀린 값을 만든다.
|
|
1764
|
+
*
|
|
1765
|
+
* 구간이 겹치면 **나중에 시작한 것**이 이긴다(더 좁게 정한 선언이 앞선다).
|
|
1766
|
+
*/
|
|
1767
|
+
function inEffectAt(list, atMs) {
|
|
1768
|
+
if (!list?.length)
|
|
1769
|
+
return undefined;
|
|
1770
|
+
const at = new Date(atMs).toISOString();
|
|
1771
|
+
let best;
|
|
1772
|
+
let bestFrom = -Infinity;
|
|
1773
|
+
for (const d of list) {
|
|
1774
|
+
if (effectivityAt({ effectiveStart: d.from, effectiveEnd: d.to }, at, { end: 'exclusive' }) !== undefined)
|
|
1775
|
+
continue;
|
|
1776
|
+
const from = Date.parse(String(d.from));
|
|
1777
|
+
if (!Number.isFinite(from))
|
|
1778
|
+
continue;
|
|
1779
|
+
if (from > bestFrom) {
|
|
1780
|
+
best = d;
|
|
1781
|
+
bestFrom = from;
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
return best;
|
|
1785
|
+
}
|
package/dist/energy-ingest.js
CHANGED
|
@@ -503,11 +503,6 @@ export function ingestEnergyGenerationPriceRecords(records, opts) {
|
|
|
503
503
|
continue;
|
|
504
504
|
}
|
|
505
505
|
const data = { from: span.from, to: span.to, unitPrice, currency };
|
|
506
|
-
const futureError = futureFactError(span.from, opts);
|
|
507
|
-
if (futureError) {
|
|
508
|
-
rejected.push({ record: r, errors: [futureError] });
|
|
509
|
-
continue;
|
|
510
|
-
}
|
|
511
506
|
accepted.push({
|
|
512
507
|
eventId: periodFactId(opts.tenantId, ENERGY_EVENT.generationPrice, '', span.from, span.to),
|
|
513
508
|
eventType: ENERGY_EVENT.generationPrice,
|
|
@@ -573,11 +568,6 @@ export function ingestEnergyTariffBasisRecords(records, opts) {
|
|
|
573
568
|
...(demandChargePerKW !== undefined ? { demandChargePerKW } : {}),
|
|
574
569
|
...(currency ? { currency } : {})
|
|
575
570
|
};
|
|
576
|
-
const futureError = futureFactError(span.from, opts);
|
|
577
|
-
if (futureError) {
|
|
578
|
-
rejected.push({ record: r, errors: [futureError] });
|
|
579
|
-
continue;
|
|
580
|
-
}
|
|
581
571
|
accepted.push({
|
|
582
572
|
eventId: periodFactId(opts.tenantId, ENERGY_EVENT.tariffBasis, '', span.from, span.to),
|
|
583
573
|
eventType: ENERGY_EVENT.tariffBasis,
|
|
@@ -633,8 +623,13 @@ export function isEnergyBillRecord(record) {
|
|
|
633
623
|
* 지난 기록 채우기는 이미 이 판단을 하고 있었다(§`judgeBackfill`). 라이브 문에만 없어서 **한 종류에
|
|
634
624
|
* 규칙이 두 벌**이었다. 같은 사실이 어느 문으로 들어오느냐에 따라 받아지기도 하고 거절되기도 했다.
|
|
635
625
|
*
|
|
636
|
-
* 기준
|
|
637
|
-
*
|
|
626
|
+
* ── 기준 선언은 이 판정을 받지 않는다 (2026-08-30, 두 번째 판단) ────────────
|
|
627
|
+
* 요금 기준·발전 단가는 **예정될 수 있다.** 한전은 다음 달 요금을 미리 공표하고, 공정 조건도 시행일을
|
|
628
|
+
* 앞두고 정해진다. 「9월부터 이 단가」는 오늘 성립하는 사실이다.
|
|
629
|
+
*
|
|
630
|
+
* 처음에는 이 둘도 거부했다. 그러면 정상적인 사실이 막힌다. 받되, **예정된 선언이 지금 것을 덮지 않게**
|
|
631
|
+
* 커널이 유효 구간으로 고른다(§`EnergyState.tariffBasisSchedule`). 받아 놓고 아무 데나 쓰면
|
|
632
|
+
* 오늘 요금이 다음 달 단가로 계산된다 — 그것이 이 문을 열기 전에 먼저 만들어야 했던 자리다.
|
|
638
633
|
*/
|
|
639
634
|
function futureFactError(eventTime, opts) {
|
|
640
635
|
const nowText = opts.nowISO ?? opts.defaultEventTime;
|
package/dist-cjs/index.cjs
CHANGED
|
@@ -366,14 +366,14 @@ function judgeAgainstSpec(spec, result) {
|
|
|
366
366
|
}
|
|
367
367
|
return allJudged ? "pass" : void 0;
|
|
368
368
|
}
|
|
369
|
-
function effectivityAt(p, at) {
|
|
369
|
+
function effectivityAt(p, at, opts) {
|
|
370
370
|
if (!p || !at) return void 0;
|
|
371
371
|
const atMs = parsedMs(at);
|
|
372
372
|
if (!Number.isFinite(atMs)) return void 0;
|
|
373
373
|
const from = p.effectiveStart ? parsedMs(p.effectiveStart) : NaN;
|
|
374
374
|
if (Number.isFinite(from) && atMs < from) return "not-yet";
|
|
375
375
|
const to = p.effectiveEnd ? parsedMs(p.effectiveEnd) : NaN;
|
|
376
|
-
if (Number.isFinite(to) && atMs > to) return "expired";
|
|
376
|
+
if (Number.isFinite(to) && (opts?.end === "exclusive" ? atMs >= to : atMs > to)) return "expired";
|
|
377
377
|
return void 0;
|
|
378
378
|
}
|
|
379
379
|
var PARSED_MAX = 64;
|
|
@@ -2862,11 +2862,6 @@ function ingestEnergyGenerationPriceRecords(records, opts) {
|
|
|
2862
2862
|
continue;
|
|
2863
2863
|
}
|
|
2864
2864
|
const data = { from: span.from, to: span.to, unitPrice, currency };
|
|
2865
|
-
const futureError = futureFactError(span.from, opts);
|
|
2866
|
-
if (futureError) {
|
|
2867
|
-
rejected.push({ record: r, errors: [futureError] });
|
|
2868
|
-
continue;
|
|
2869
|
-
}
|
|
2870
2865
|
accepted.push({
|
|
2871
2866
|
eventId: periodFactId(opts.tenantId, ENERGY_EVENT.generationPrice, "", span.from, span.to),
|
|
2872
2867
|
eventType: ENERGY_EVENT.generationPrice,
|
|
@@ -2912,11 +2907,6 @@ function ingestEnergyTariffBasisRecords(records, opts) {
|
|
|
2912
2907
|
...demandChargePerKW !== void 0 ? { demandChargePerKW } : {},
|
|
2913
2908
|
...currency ? { currency } : {}
|
|
2914
2909
|
};
|
|
2915
|
-
const futureError = futureFactError(span.from, opts);
|
|
2916
|
-
if (futureError) {
|
|
2917
|
-
rejected.push({ record: r, errors: [futureError] });
|
|
2918
|
-
continue;
|
|
2919
|
-
}
|
|
2920
2910
|
accepted.push({
|
|
2921
2911
|
eventId: periodFactId(opts.tenantId, ENERGY_EVENT.tariffBasis, "", span.from, span.to),
|
|
2922
2912
|
eventType: ENERGY_EVENT.tariffBasis,
|
|
@@ -8673,11 +8663,8 @@ var EmsKernel = class extends FlowEngine {
|
|
|
8673
8663
|
applyGenerationPrice(envelope) {
|
|
8674
8664
|
const d = envelope.data;
|
|
8675
8665
|
if (!d?.from || !d?.to) return;
|
|
8676
|
-
|
|
8677
|
-
|
|
8678
|
-
const priorFromMs = Date.parse(String(this.generationPrice?.from ?? ""));
|
|
8679
|
-
if (Number.isFinite(priorFromMs) && fromMs < priorFromMs) return;
|
|
8680
|
-
this.generationPrice = { ...d };
|
|
8666
|
+
this.generationPriceSchedule = putDeclaration(this.generationPriceSchedule, d);
|
|
8667
|
+
this.generationPrice = inEffectAt(this.generationPriceSchedule, this.nowMs());
|
|
8681
8668
|
this.revision++;
|
|
8682
8669
|
}
|
|
8683
8670
|
/**
|
|
@@ -8689,13 +8676,25 @@ var EmsKernel = class extends FlowEngine {
|
|
|
8689
8676
|
applyTariffBasis(envelope) {
|
|
8690
8677
|
const d = envelope.data;
|
|
8691
8678
|
if (!d?.from || !d?.to) return;
|
|
8692
|
-
|
|
8693
|
-
|
|
8694
|
-
const priorFromMs = Date.parse(String(this.tariffBasis?.from ?? ""));
|
|
8695
|
-
if (Number.isFinite(priorFromMs) && fromMs < priorFromMs) return;
|
|
8696
|
-
this.tariffBasis = { ...d };
|
|
8679
|
+
this.tariffBasisSchedule = putDeclaration(this.tariffBasisSchedule, d);
|
|
8680
|
+
this.tariffBasis = inEffectAt(this.tariffBasisSchedule, this.nowMs());
|
|
8697
8681
|
this.revision++;
|
|
8698
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
|
+
}
|
|
8699
8698
|
/**
|
|
8700
8699
|
* 청구서를 상태에 둔다 — **우리 계산을 덮지 않는다.**
|
|
8701
8700
|
*
|
|
@@ -8914,8 +8913,12 @@ var EmsKernel = class extends FlowEngine {
|
|
|
8914
8913
|
lastBill;
|
|
8915
8914
|
/** 마지막으로 받은 요금 기준 — 이 주기에 적용되는 요금적용전력과 단가. */
|
|
8916
8915
|
tariffBasis;
|
|
8916
|
+
/** 선언된 요금 기준들 — 예정된 것을 포함한다(§`EnergyState.tariffBasisSchedule`). */
|
|
8917
|
+
tariffBasisSchedule;
|
|
8917
8918
|
/** 마지막으로 받은 발전 단가 — 이 기간에 낸 전기 1kWh 의 값. */
|
|
8918
8919
|
generationPrice;
|
|
8920
|
+
/** 선언된 발전 단가들 — 예정된 것을 포함한다. */
|
|
8921
|
+
generationPriceSchedule;
|
|
8919
8922
|
/**
|
|
8920
8923
|
* 이 청구 주기의 **최고 수요** — 기본요금이 이 값으로 다시 정해진다.
|
|
8921
8924
|
*
|
|
@@ -9552,6 +9555,7 @@ var EmsKernel = class extends FlowEngine {
|
|
|
9552
9555
|
super.tick(dtMs);
|
|
9553
9556
|
const at = this.nowMs();
|
|
9554
9557
|
this.closeGenerationPeriodsDue(at);
|
|
9558
|
+
this.refreshDeclarationsInEffect(at);
|
|
9555
9559
|
this.deriveLoad(at, dtMs);
|
|
9556
9560
|
this.closeDue(at);
|
|
9557
9561
|
}
|
|
@@ -9591,8 +9595,13 @@ var EmsKernel = class extends FlowEngine {
|
|
|
9591
9595
|
}
|
|
9592
9596
|
if (Number.isFinite(e.unclosedGenerationPeriods)) this.unclosedGenerationPeriods = Number(e.unclosedGenerationPeriods);
|
|
9593
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 }));
|
|
9594
9600
|
if (e.tariffBasis?.from && e.tariffBasis?.to) this.tariffBasis = { ...e.tariffBasis };
|
|
9595
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
|
+
}
|
|
9596
9605
|
if (Number.isFinite(Number(e.demandHigh?.kW)) && e.demandHigh?.since) this.demandHigh = { ...e.demandHigh };
|
|
9597
9606
|
}
|
|
9598
9607
|
getSnapshot() {
|
|
@@ -9617,6 +9626,12 @@ var EmsKernel = class extends FlowEngine {
|
|
|
9617
9626
|
...this.lastBill ? { lastBill: { ...this.lastBill } } : {},
|
|
9618
9627
|
...this.tariffBasis ? { tariffBasis: { ...this.tariffBasis } } : {},
|
|
9619
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 })) } : {},
|
|
9620
9635
|
...(() => {
|
|
9621
9636
|
const r = this.generationRated();
|
|
9622
9637
|
return r ? { generationRated: r } : {};
|
|
@@ -9629,6 +9644,27 @@ var EmsKernel = class extends FlowEngine {
|
|
|
9629
9644
|
return { ...snap, energy };
|
|
9630
9645
|
}
|
|
9631
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
|
+
if (!list?.length) return void 0;
|
|
9654
|
+
const at = new Date(atMs).toISOString();
|
|
9655
|
+
let best;
|
|
9656
|
+
let bestFrom = -Infinity;
|
|
9657
|
+
for (const d of list) {
|
|
9658
|
+
if (effectivityAt({ effectiveStart: d.from, effectiveEnd: d.to }, at, { end: "exclusive" }) !== void 0) continue;
|
|
9659
|
+
const from = Date.parse(String(d.from));
|
|
9660
|
+
if (!Number.isFinite(from)) continue;
|
|
9661
|
+
if (from > bestFrom) {
|
|
9662
|
+
best = d;
|
|
9663
|
+
bestFrom = from;
|
|
9664
|
+
}
|
|
9665
|
+
}
|
|
9666
|
+
return best;
|
|
9667
|
+
}
|
|
9632
9668
|
|
|
9633
9669
|
// src/event-journal.ts
|
|
9634
9670
|
var DOMAIN_KERNELS = { wms: WmsKernel, yms: YmsKernel, mes: MesKernel, ems: EmsKernel };
|
package/package.json
CHANGED