@operato/twin-kernel 0.7.79 → 0.7.81
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/ems-kernel.d.ts +9 -0
- package/dist/ems-kernel.js +43 -0
- package/dist/energy-ingest.js +25 -8
- package/dist-cjs/index.cjs +33 -8
- package/package.json +1 -1
package/dist/ems-kernel.d.ts
CHANGED
|
@@ -169,6 +169,15 @@ export declare class EmsKernel extends FlowEngine {
|
|
|
169
169
|
* 않으면 늦게 온 옛 적산이 최신 값을 뒤로 돌린다 — 그리고 그것이 「계기가 되돌아갔다」로 잘못 적힌다.
|
|
170
170
|
*/
|
|
171
171
|
private applyGenerated;
|
|
172
|
+
/**
|
|
173
|
+
* 시계가 기간의 끝을 지났으면 마감한다 — **표본을 기다리지 않는다.**
|
|
174
|
+
*
|
|
175
|
+
* 값이 오지 않는 밤에도 자정이 지나면 어제가 닫힌다. 마감에 쓰는 총량은 **그날 마지막으로 본 값**
|
|
176
|
+
* 이므로 표본이 더 오지 않아도 정확하다(하루 누적은 그 값이 곧 그날의 총량이다).
|
|
177
|
+
*
|
|
178
|
+
* 이 자리는 계량의 `closeDue` 와 같은 성질이다 — 호스트가 시각을 주며 부른다.
|
|
179
|
+
*/
|
|
180
|
+
closeGenerationPeriodsDue(nowMs: number): void;
|
|
172
181
|
/**
|
|
173
182
|
* **기간이 끝났으면 그 기간의 발전량을 사실로 낸다.**
|
|
174
183
|
*
|
package/dist/ems-kernel.js
CHANGED
|
@@ -532,6 +532,39 @@ export class EmsKernel extends FlowEngine {
|
|
|
532
532
|
this.closeGenerationPeriod(id, eq, { atMs, kWh, dayIndex, kind, since, priorSince, periodAdvanced, faulted: dropped && sameDayDrop });
|
|
533
533
|
this.revision++;
|
|
534
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
|
+
}
|
|
535
568
|
/**
|
|
536
569
|
* **기간이 끝났으면 그 기간의 발전량을 사실로 낸다.**
|
|
537
570
|
*
|
|
@@ -1523,6 +1556,16 @@ export class EmsKernel extends FlowEngine {
|
|
|
1523
1556
|
* 부하를 먼저 만들고 마감한다 — 마감이 먼저면 마지막 값이 다음 구간으로 밀린다.
|
|
1524
1557
|
*/
|
|
1525
1558
|
const at = this.nowMs();
|
|
1559
|
+
/*
|
|
1560
|
+
* ── 발전 기간도 **시계로** 마감한다 (2026-08-30) ───────────────────────────
|
|
1561
|
+
*
|
|
1562
|
+
* 앞 판은 새 표본이 올 때만 날짜가 바뀐 것을 알아챘다. 그런데 태양광은 해가 지면 값을 보내지
|
|
1563
|
+
* 않는다 — 어제 마지막 표본이 19:40 이면 어제 하루치가 **다음 날 아침 첫 표본**에야 확정된다.
|
|
1564
|
+
* 밤새 「어제 얼마 냈나」에 답이 없다.
|
|
1565
|
+
*
|
|
1566
|
+
* 계량은 이미 시계로 마감한다(`closeDue`). 한 커널 안에서 규칙이 두 벌일 이유가 없다.
|
|
1567
|
+
*/
|
|
1568
|
+
this.closeGenerationPeriodsDue(at);
|
|
1526
1569
|
/* 틱 길이를 함께 넘긴다 — 방전량을 에너지로 바꿀 때 필요하다(필드로 숨기면 어디서 온 값인지 흐려진다). */
|
|
1527
1570
|
this.deriveLoad(at, dtMs);
|
|
1528
1571
|
this.closeDue(at);
|
package/dist/energy-ingest.js
CHANGED
|
@@ -399,7 +399,6 @@ export function ingestEnergyGenerationPriceRecords(records, opts) {
|
|
|
399
399
|
const list = records === undefined || records === null ? [] : Array.isArray(records) ? records : [records];
|
|
400
400
|
const accepted = [];
|
|
401
401
|
const rejected = [];
|
|
402
|
-
let seq = 0;
|
|
403
402
|
for (const r of list) {
|
|
404
403
|
const errors = [];
|
|
405
404
|
const span = readSpan(r, errors);
|
|
@@ -416,7 +415,7 @@ export function ingestEnergyGenerationPriceRecords(records, opts) {
|
|
|
416
415
|
}
|
|
417
416
|
const data = { from: span.from, to: span.to, unitPrice, currency };
|
|
418
417
|
accepted.push({
|
|
419
|
-
eventId:
|
|
418
|
+
eventId: periodFactId(opts.tenantId, ENERGY_EVENT.generationPrice, '', span.from, span.to),
|
|
420
419
|
eventType: ENERGY_EVENT.generationPrice,
|
|
421
420
|
eventTime: span.from,
|
|
422
421
|
tenantId: opts.tenantId,
|
|
@@ -453,7 +452,6 @@ export function ingestEnergyTariffBasisRecords(records, opts) {
|
|
|
453
452
|
const list = records === undefined || records === null ? [] : Array.isArray(records) ? records : [records];
|
|
454
453
|
const accepted = [];
|
|
455
454
|
const rejected = [];
|
|
456
|
-
let seq = 0;
|
|
457
455
|
for (const r of list) {
|
|
458
456
|
const errors = [];
|
|
459
457
|
const span = readSpan(r, errors);
|
|
@@ -482,7 +480,7 @@ export function ingestEnergyTariffBasisRecords(records, opts) {
|
|
|
482
480
|
...(currency ? { currency } : {})
|
|
483
481
|
};
|
|
484
482
|
accepted.push({
|
|
485
|
-
eventId:
|
|
483
|
+
eventId: periodFactId(opts.tenantId, ENERGY_EVENT.tariffBasis, '', span.from, span.to),
|
|
486
484
|
eventType: ENERGY_EVENT.tariffBasis,
|
|
487
485
|
/* **시작 시각**이다 — 그때부터 유효하다(§ 위 주석). */
|
|
488
486
|
eventTime: span.from,
|
|
@@ -500,6 +498,27 @@ export function isEnergyBillRecord(record) {
|
|
|
500
498
|
return false;
|
|
501
499
|
return r.energyCharge !== undefined || r.demandCharge !== undefined || r.total !== undefined;
|
|
502
500
|
}
|
|
501
|
+
/*
|
|
502
|
+
* ── 마감된 구간 사실의 **정체성** (2026-08-30) ───────────────────────────────
|
|
503
|
+
*
|
|
504
|
+
* 커널이 다루는 사실에는 두 종류가 있다.
|
|
505
|
+
*
|
|
506
|
+
* 시점의 관측 「이 시각에 이 값이었다」 같은 값이 두 번 오면 **두 사실**이다
|
|
507
|
+
* 마감된 구간 「이 기간에 이만큼이었다」 같은 기간이 두 번 오면 **한 사실**이다
|
|
508
|
+
*
|
|
509
|
+
* 둘째 종류는 정체성이 내용에 있다 — **종류·대상·시작·끝**이 같으면 같은 사실이다. 어느 커넥터가
|
|
510
|
+
* 어떻게 보내든, 재기동을 몇 번 하든 그렇다.
|
|
511
|
+
*
|
|
512
|
+
* 그래서 이 문들의 봉투 id 를 **순번이 아니라 내용에서** 만든다. 순번으로 만들면 같은 기간을 다시
|
|
513
|
+
* 보낼 때마다 다른 id 가 나오고, 읽는 쪽이 같은 사실인지 알 방법이 없다.
|
|
514
|
+
*
|
|
515
|
+
* **이것만으로 중복이 막히지는 않는다.** 저널은 아직 이 id 를 유일성으로 쓰지 않는다(그 판단은
|
|
516
|
+
* 호스트의 것이고 드라이버마다 다르다). 여기서 하는 일은 **막을 근거를 만드는 것**이다.
|
|
517
|
+
*/
|
|
518
|
+
function periodFactId(tenantId, kind, subject, from, to) {
|
|
519
|
+
/* 대상이 없는 사실(요금 기준·청구서·발전 단가)은 그 자리를 비운다 — 없는 것을 지어내지 않는다. */
|
|
520
|
+
return [tenantId, kind, subject, from, to].join('|');
|
|
521
|
+
}
|
|
503
522
|
/** 두 시각을 읽고 순서를 본다 — 뒤집힌 구간은 받지 않는다(합이 음수가 된다). */
|
|
504
523
|
function readSpan(r, errors) {
|
|
505
524
|
const from = String(r?.from ?? '').trim();
|
|
@@ -534,7 +553,6 @@ export function ingestEnergyUsagePeriodRecords(records, opts) {
|
|
|
534
553
|
const list = records === undefined || records === null ? [] : Array.isArray(records) ? records : [records];
|
|
535
554
|
const accepted = [];
|
|
536
555
|
const rejected = [];
|
|
537
|
-
let seq = 0;
|
|
538
556
|
for (const r of list) {
|
|
539
557
|
const errors = [];
|
|
540
558
|
const meterId = String(r?.meterId ?? '').trim();
|
|
@@ -581,7 +599,7 @@ export function ingestEnergyUsagePeriodRecords(records, opts) {
|
|
|
581
599
|
...(basis ? { basis } : {})
|
|
582
600
|
};
|
|
583
601
|
accepted.push({
|
|
584
|
-
eventId:
|
|
602
|
+
eventId: periodFactId(opts.tenantId, ENERGY_EVENT.usagePeriod, meterId, span.from, span.to),
|
|
585
603
|
eventType: ENERGY_EVENT.usagePeriod,
|
|
586
604
|
/* 구간의 **끝**이 이 사실이 성립한 시각이다 — 시작으로 달면 저널이 그 구간을 미리 안 것이 된다. */
|
|
587
605
|
eventTime: span.to,
|
|
@@ -595,7 +613,6 @@ export function ingestEnergyBillRecords(records, opts) {
|
|
|
595
613
|
const list = records === undefined || records === null ? [] : Array.isArray(records) ? records : [records];
|
|
596
614
|
const accepted = [];
|
|
597
615
|
const rejected = [];
|
|
598
|
-
let seq = 0;
|
|
599
616
|
for (const r of list) {
|
|
600
617
|
const errors = [];
|
|
601
618
|
const span = readSpan(r, errors);
|
|
@@ -628,7 +645,7 @@ export function ingestEnergyBillRecords(records, opts) {
|
|
|
628
645
|
...(billingDemandKW !== undefined ? { billingDemandKW } : {})
|
|
629
646
|
};
|
|
630
647
|
accepted.push({
|
|
631
|
-
eventId:
|
|
648
|
+
eventId: periodFactId(opts.tenantId, ENERGY_EVENT.bill, '', span.from, span.to),
|
|
632
649
|
eventType: ENERGY_EVENT.bill,
|
|
633
650
|
eventTime: span.to,
|
|
634
651
|
tenantId: opts.tenantId,
|
package/dist-cjs/index.cjs
CHANGED
|
@@ -8168,6 +8168,31 @@ var EmsKernel = class extends FlowEngine {
|
|
|
8168
8168
|
this.closeGenerationPeriod(id, eq, { atMs, kWh, dayIndex, kind, since, priorSince, periodAdvanced, faulted: dropped && sameDayDrop });
|
|
8169
8169
|
this.revision++;
|
|
8170
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
|
+
}
|
|
8171
8196
|
/**
|
|
8172
8197
|
* **기간이 끝났으면 그 기간의 발전량을 사실로 낸다.**
|
|
8173
8198
|
*
|
|
@@ -8898,6 +8923,7 @@ var EmsKernel = class extends FlowEngine {
|
|
|
8898
8923
|
tick(dtMs) {
|
|
8899
8924
|
super.tick(dtMs);
|
|
8900
8925
|
const at = this.nowMs();
|
|
8926
|
+
this.closeGenerationPeriodsDue(at);
|
|
8901
8927
|
this.deriveLoad(at, dtMs);
|
|
8902
8928
|
this.closeDue(at);
|
|
8903
8929
|
}
|
|
@@ -10023,7 +10049,6 @@ function ingestEnergyGenerationPriceRecords(records, opts) {
|
|
|
10023
10049
|
const list = records === void 0 || records === null ? [] : Array.isArray(records) ? records : [records];
|
|
10024
10050
|
const accepted = [];
|
|
10025
10051
|
const rejected = [];
|
|
10026
|
-
let seq = 0;
|
|
10027
10052
|
for (const r of list) {
|
|
10028
10053
|
const errors = [];
|
|
10029
10054
|
const span = readSpan(r, errors);
|
|
@@ -10037,7 +10062,7 @@ function ingestEnergyGenerationPriceRecords(records, opts) {
|
|
|
10037
10062
|
}
|
|
10038
10063
|
const data = { from: span.from, to: span.to, unitPrice, currency };
|
|
10039
10064
|
accepted.push({
|
|
10040
|
-
eventId:
|
|
10065
|
+
eventId: periodFactId(opts.tenantId, ENERGY_EVENT.generationPrice, "", span.from, span.to),
|
|
10041
10066
|
eventType: ENERGY_EVENT.generationPrice,
|
|
10042
10067
|
eventTime: span.from,
|
|
10043
10068
|
tenantId: opts.tenantId,
|
|
@@ -10057,7 +10082,6 @@ function ingestEnergyTariffBasisRecords(records, opts) {
|
|
|
10057
10082
|
const list = records === void 0 || records === null ? [] : Array.isArray(records) ? records : [records];
|
|
10058
10083
|
const accepted = [];
|
|
10059
10084
|
const rejected = [];
|
|
10060
|
-
let seq = 0;
|
|
10061
10085
|
for (const r of list) {
|
|
10062
10086
|
const errors = [];
|
|
10063
10087
|
const span = readSpan(r, errors);
|
|
@@ -10083,7 +10107,7 @@ function ingestEnergyTariffBasisRecords(records, opts) {
|
|
|
10083
10107
|
...currency ? { currency } : {}
|
|
10084
10108
|
};
|
|
10085
10109
|
accepted.push({
|
|
10086
|
-
eventId:
|
|
10110
|
+
eventId: periodFactId(opts.tenantId, ENERGY_EVENT.tariffBasis, "", span.from, span.to),
|
|
10087
10111
|
eventType: ENERGY_EVENT.tariffBasis,
|
|
10088
10112
|
/* **시작 시각**이다 — 그때부터 유효하다(§ 위 주석). */
|
|
10089
10113
|
eventTime: span.from,
|
|
@@ -10099,6 +10123,9 @@ function isEnergyBillRecord(record) {
|
|
|
10099
10123
|
if (r.from === void 0 || r.to === void 0) return false;
|
|
10100
10124
|
return r.energyCharge !== void 0 || r.demandCharge !== void 0 || r.total !== void 0;
|
|
10101
10125
|
}
|
|
10126
|
+
function periodFactId(tenantId, kind, subject, from, to) {
|
|
10127
|
+
return [tenantId, kind, subject, from, to].join("|");
|
|
10128
|
+
}
|
|
10102
10129
|
function readSpan(r, errors) {
|
|
10103
10130
|
const from = String(r?.from ?? "").trim();
|
|
10104
10131
|
const to = String(r?.to ?? "").trim();
|
|
@@ -10128,7 +10155,6 @@ function ingestEnergyUsagePeriodRecords(records, opts) {
|
|
|
10128
10155
|
const list = records === void 0 || records === null ? [] : Array.isArray(records) ? records : [records];
|
|
10129
10156
|
const accepted = [];
|
|
10130
10157
|
const rejected = [];
|
|
10131
|
-
let seq = 0;
|
|
10132
10158
|
for (const r of list) {
|
|
10133
10159
|
const errors = [];
|
|
10134
10160
|
const meterId = String(r?.meterId ?? "").trim();
|
|
@@ -10168,7 +10194,7 @@ function ingestEnergyUsagePeriodRecords(records, opts) {
|
|
|
10168
10194
|
...basis ? { basis } : {}
|
|
10169
10195
|
};
|
|
10170
10196
|
accepted.push({
|
|
10171
|
-
eventId:
|
|
10197
|
+
eventId: periodFactId(opts.tenantId, ENERGY_EVENT.usagePeriod, meterId, span.from, span.to),
|
|
10172
10198
|
eventType: ENERGY_EVENT.usagePeriod,
|
|
10173
10199
|
/* 구간의 **끝**이 이 사실이 성립한 시각이다 — 시작으로 달면 저널이 그 구간을 미리 안 것이 된다. */
|
|
10174
10200
|
eventTime: span.to,
|
|
@@ -10182,7 +10208,6 @@ function ingestEnergyBillRecords(records, opts) {
|
|
|
10182
10208
|
const list = records === void 0 || records === null ? [] : Array.isArray(records) ? records : [records];
|
|
10183
10209
|
const accepted = [];
|
|
10184
10210
|
const rejected = [];
|
|
10185
|
-
let seq = 0;
|
|
10186
10211
|
for (const r of list) {
|
|
10187
10212
|
const errors = [];
|
|
10188
10213
|
const span = readSpan(r, errors);
|
|
@@ -10209,7 +10234,7 @@ function ingestEnergyBillRecords(records, opts) {
|
|
|
10209
10234
|
...billingDemandKW !== void 0 ? { billingDemandKW } : {}
|
|
10210
10235
|
};
|
|
10211
10236
|
accepted.push({
|
|
10212
|
-
eventId:
|
|
10237
|
+
eventId: periodFactId(opts.tenantId, ENERGY_EVENT.bill, "", span.from, span.to),
|
|
10213
10238
|
eventType: ENERGY_EVENT.bill,
|
|
10214
10239
|
eventTime: span.to,
|
|
10215
10240
|
tenantId: opts.tenantId,
|
package/package.json
CHANGED