@operato/twin-kernel 0.7.80 → 0.7.82
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 +12 -0
- package/dist/ems-kernel.js +11 -4
- package/dist/energy-ingest.js +25 -8
- package/dist-cjs/index.cjs +12 -12
- package/package.json +1 -1
package/dist/contract.d.ts
CHANGED
|
@@ -2096,6 +2096,18 @@ export interface GenerationPeriodObservation {
|
|
|
2096
2096
|
/** 이 기간에서 마지막으로 본 적산과 그 시각 — 기간 누적은 이것이 곧 총량이다. */
|
|
2097
2097
|
lastKWh: number;
|
|
2098
2098
|
lastMs: number;
|
|
2099
|
+
/**
|
|
2100
|
+
* 그 적산이 **어떤 누적인가** — 마감할 때 총량을 얻는 방법이 이것으로 갈린다.
|
|
2101
|
+
*
|
|
2102
|
+
* ── 왜 여기 함께 두나 (2026-08-30) ────────────────────────────────────────
|
|
2103
|
+
* 설비 상태는 원천이 다시 보내 주므로 재기동에서 되살리지 않는다(관측 축은 원천에서). 그 규율은
|
|
2104
|
+
* 맞다. 그런데 **누적의 종류는 관측이 아니라 그 원본이 어떤 값을 주는지에 대한 사실**이다 —
|
|
2105
|
+
* 값은 다음 표본에 다시 오지만 종류도 그때까지 없다.
|
|
2106
|
+
*
|
|
2107
|
+
* 태양광은 밤에 표본을 보내지 않는다. 그래서 밤에 재기동하면 종류가 사라지고, 시계가 자정을
|
|
2108
|
+
* 지나도 **어느 기간의 값인지 몰라 마감하지 못한다.** 실제로 그렇게 됐다(2026-08-30 04:54).
|
|
2109
|
+
*/
|
|
2110
|
+
accumulation?: 'lifetime' | 'daily' | 'monthly' | 'billing' | 'unknown';
|
|
2099
2111
|
}
|
|
2100
2112
|
/**
|
|
2101
2113
|
* 조치방향 — code=안정 조치 키(언어 중립). command 있으면 원클릭 실행, 없으면 권고.
|
package/dist/ems-kernel.js
CHANGED
|
@@ -548,7 +548,12 @@ export class EmsKernel extends FlowEngine {
|
|
|
548
548
|
const eq = this.equipment.get(id);
|
|
549
549
|
if (!eq)
|
|
550
550
|
continue;
|
|
551
|
-
|
|
551
|
+
/*
|
|
552
|
+
* 종류는 **관측과 함께 이어받은 것**이 먼저다. 설비 상태는 재기동에서 되살아나지 않으므로
|
|
553
|
+
* (관측 축은 원천에서), 밤에 재기동하면 상태의 종류가 비어 있다 — 그러면 자정이 지나도
|
|
554
|
+
* 「어느 기간의 값인지 모른다」로 마감하지 못한다.
|
|
555
|
+
*/
|
|
556
|
+
const kind = obs.accumulation ?? eq.generatedKWhAccumulation;
|
|
552
557
|
/*
|
|
553
558
|
* 마감은 `applyGenerated` 와 **같은 함수**로 한다 — 규칙을 두 벌 만들면 표본으로 닫은 날과
|
|
554
559
|
* 시계로 닫은 날이 다른 값을 낸다.
|
|
@@ -591,7 +596,8 @@ export class EmsKernel extends FlowEngine {
|
|
|
591
596
|
*/
|
|
592
597
|
closeGenerationPeriod(id, eq, at) {
|
|
593
598
|
const obs = this.generationPeriodObs.get(id);
|
|
594
|
-
|
|
599
|
+
/* 종류를 함께 들고 간다 — 재기동에서 설비 상태는 사라지지만 이 관측은 이어진다. */
|
|
600
|
+
const fresh = { dayIndex: at.dayIndex, fromMs: at.atMs, lastKWh: at.kWh, lastMs: at.atMs, accumulation: at.kind };
|
|
595
601
|
/* 계기가 되돌아간 뒤의 관측은 쓸 수 없다 — 되돌아간 시점 앞뒤가 한 기간에 섞인다. */
|
|
596
602
|
if (at.faulted || !obs || at.kind === undefined || at.kind === 'unknown') {
|
|
597
603
|
this.generationPeriodObs.set(id, fresh);
|
|
@@ -622,7 +628,7 @@ export class EmsKernel extends FlowEngine {
|
|
|
622
628
|
if (at.dayIndex !== obs.dayIndex)
|
|
623
629
|
this.generationPeriodObs.set(id, fresh);
|
|
624
630
|
else
|
|
625
|
-
this.generationPeriodObs.set(id, { ...obs, lastKWh: at.kWh, lastMs: at.atMs });
|
|
631
|
+
this.generationPeriodObs.set(id, { ...obs, lastKWh: at.kWh, lastMs: at.atMs, accumulation: (at.kind ?? obs.accumulation) });
|
|
626
632
|
return;
|
|
627
633
|
}
|
|
628
634
|
/*
|
|
@@ -1614,7 +1620,8 @@ export class EmsKernel extends FlowEngine {
|
|
|
1614
1620
|
dayIndex: Number(o.dayIndex),
|
|
1615
1621
|
fromMs: Number(o.fromMs),
|
|
1616
1622
|
lastKWh: Number(o.lastKWh),
|
|
1617
|
-
lastMs: Number(o.lastMs)
|
|
1623
|
+
lastMs: Number(o.lastMs),
|
|
1624
|
+
...(o.accumulation ? { accumulation: o.accumulation } : {})
|
|
1618
1625
|
});
|
|
1619
1626
|
}
|
|
1620
1627
|
/* 못 마감한 수도 이어받는다 — 이어받지 않으면 재기동마다 0 으로 돌아가 문제가 사라진 것처럼 보인다. */
|
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
|
@@ -8182,7 +8182,7 @@ var EmsKernel = class extends FlowEngine {
|
|
|
8182
8182
|
if (today <= obs.dayIndex) continue;
|
|
8183
8183
|
const eq = this.equipment.get(id);
|
|
8184
8184
|
if (!eq) continue;
|
|
8185
|
-
const kind = eq.generatedKWhAccumulation;
|
|
8185
|
+
const kind = obs.accumulation ?? eq.generatedKWhAccumulation;
|
|
8186
8186
|
this.closeGenerationPeriod(id, eq, {
|
|
8187
8187
|
atMs: nowMs,
|
|
8188
8188
|
kWh: obs.lastKWh,
|
|
@@ -8219,7 +8219,7 @@ var EmsKernel = class extends FlowEngine {
|
|
|
8219
8219
|
*/
|
|
8220
8220
|
closeGenerationPeriod(id, eq, at) {
|
|
8221
8221
|
const obs = this.generationPeriodObs.get(id);
|
|
8222
|
-
const fresh = { dayIndex: at.dayIndex, fromMs: at.atMs, lastKWh: at.kWh, lastMs: at.atMs };
|
|
8222
|
+
const fresh = { dayIndex: at.dayIndex, fromMs: at.atMs, lastKWh: at.kWh, lastMs: at.atMs, accumulation: at.kind };
|
|
8223
8223
|
if (at.faulted || !obs || at.kind === void 0 || at.kind === "unknown") {
|
|
8224
8224
|
this.generationPeriodObs.set(id, fresh);
|
|
8225
8225
|
return;
|
|
@@ -8241,7 +8241,7 @@ var EmsKernel = class extends FlowEngine {
|
|
|
8241
8241
|
if (startMs === void 0 || endMs === void 0 || boundary === void 0 || !Number.isFinite(startMs) || !Number.isFinite(endMs)) {
|
|
8242
8242
|
if (at.dayIndex > obs.dayIndex) this.unclosedGenerationPeriods++;
|
|
8243
8243
|
if (at.dayIndex !== obs.dayIndex) this.generationPeriodObs.set(id, fresh);
|
|
8244
|
-
else this.generationPeriodObs.set(id, { ...obs, lastKWh: at.kWh, lastMs: at.atMs });
|
|
8244
|
+
else this.generationPeriodObs.set(id, { ...obs, lastKWh: at.kWh, lastMs: at.atMs, accumulation: at.kind ?? obs.accumulation });
|
|
8245
8245
|
return;
|
|
8246
8246
|
}
|
|
8247
8247
|
const total = obs.lastKWh;
|
|
@@ -8957,7 +8957,8 @@ var EmsKernel = class extends FlowEngine {
|
|
|
8957
8957
|
dayIndex: Number(o.dayIndex),
|
|
8958
8958
|
fromMs: Number(o.fromMs),
|
|
8959
8959
|
lastKWh: Number(o.lastKWh),
|
|
8960
|
-
lastMs: Number(o.lastMs)
|
|
8960
|
+
lastMs: Number(o.lastMs),
|
|
8961
|
+
...o.accumulation ? { accumulation: o.accumulation } : {}
|
|
8961
8962
|
});
|
|
8962
8963
|
}
|
|
8963
8964
|
if (Number.isFinite(e.unclosedGenerationPeriods)) this.unclosedGenerationPeriods = Number(e.unclosedGenerationPeriods);
|
|
@@ -10049,7 +10050,6 @@ function ingestEnergyGenerationPriceRecords(records, opts) {
|
|
|
10049
10050
|
const list = records === void 0 || records === null ? [] : Array.isArray(records) ? records : [records];
|
|
10050
10051
|
const accepted = [];
|
|
10051
10052
|
const rejected = [];
|
|
10052
|
-
let seq = 0;
|
|
10053
10053
|
for (const r of list) {
|
|
10054
10054
|
const errors = [];
|
|
10055
10055
|
const span = readSpan(r, errors);
|
|
@@ -10063,7 +10063,7 @@ function ingestEnergyGenerationPriceRecords(records, opts) {
|
|
|
10063
10063
|
}
|
|
10064
10064
|
const data = { from: span.from, to: span.to, unitPrice, currency };
|
|
10065
10065
|
accepted.push({
|
|
10066
|
-
eventId:
|
|
10066
|
+
eventId: periodFactId(opts.tenantId, ENERGY_EVENT.generationPrice, "", span.from, span.to),
|
|
10067
10067
|
eventType: ENERGY_EVENT.generationPrice,
|
|
10068
10068
|
eventTime: span.from,
|
|
10069
10069
|
tenantId: opts.tenantId,
|
|
@@ -10083,7 +10083,6 @@ function ingestEnergyTariffBasisRecords(records, opts) {
|
|
|
10083
10083
|
const list = records === void 0 || records === null ? [] : Array.isArray(records) ? records : [records];
|
|
10084
10084
|
const accepted = [];
|
|
10085
10085
|
const rejected = [];
|
|
10086
|
-
let seq = 0;
|
|
10087
10086
|
for (const r of list) {
|
|
10088
10087
|
const errors = [];
|
|
10089
10088
|
const span = readSpan(r, errors);
|
|
@@ -10109,7 +10108,7 @@ function ingestEnergyTariffBasisRecords(records, opts) {
|
|
|
10109
10108
|
...currency ? { currency } : {}
|
|
10110
10109
|
};
|
|
10111
10110
|
accepted.push({
|
|
10112
|
-
eventId:
|
|
10111
|
+
eventId: periodFactId(opts.tenantId, ENERGY_EVENT.tariffBasis, "", span.from, span.to),
|
|
10113
10112
|
eventType: ENERGY_EVENT.tariffBasis,
|
|
10114
10113
|
/* **시작 시각**이다 — 그때부터 유효하다(§ 위 주석). */
|
|
10115
10114
|
eventTime: span.from,
|
|
@@ -10125,6 +10124,9 @@ function isEnergyBillRecord(record) {
|
|
|
10125
10124
|
if (r.from === void 0 || r.to === void 0) return false;
|
|
10126
10125
|
return r.energyCharge !== void 0 || r.demandCharge !== void 0 || r.total !== void 0;
|
|
10127
10126
|
}
|
|
10127
|
+
function periodFactId(tenantId, kind, subject, from, to) {
|
|
10128
|
+
return [tenantId, kind, subject, from, to].join("|");
|
|
10129
|
+
}
|
|
10128
10130
|
function readSpan(r, errors) {
|
|
10129
10131
|
const from = String(r?.from ?? "").trim();
|
|
10130
10132
|
const to = String(r?.to ?? "").trim();
|
|
@@ -10154,7 +10156,6 @@ function ingestEnergyUsagePeriodRecords(records, opts) {
|
|
|
10154
10156
|
const list = records === void 0 || records === null ? [] : Array.isArray(records) ? records : [records];
|
|
10155
10157
|
const accepted = [];
|
|
10156
10158
|
const rejected = [];
|
|
10157
|
-
let seq = 0;
|
|
10158
10159
|
for (const r of list) {
|
|
10159
10160
|
const errors = [];
|
|
10160
10161
|
const meterId = String(r?.meterId ?? "").trim();
|
|
@@ -10194,7 +10195,7 @@ function ingestEnergyUsagePeriodRecords(records, opts) {
|
|
|
10194
10195
|
...basis ? { basis } : {}
|
|
10195
10196
|
};
|
|
10196
10197
|
accepted.push({
|
|
10197
|
-
eventId:
|
|
10198
|
+
eventId: periodFactId(opts.tenantId, ENERGY_EVENT.usagePeriod, meterId, span.from, span.to),
|
|
10198
10199
|
eventType: ENERGY_EVENT.usagePeriod,
|
|
10199
10200
|
/* 구간의 **끝**이 이 사실이 성립한 시각이다 — 시작으로 달면 저널이 그 구간을 미리 안 것이 된다. */
|
|
10200
10201
|
eventTime: span.to,
|
|
@@ -10208,7 +10209,6 @@ function ingestEnergyBillRecords(records, opts) {
|
|
|
10208
10209
|
const list = records === void 0 || records === null ? [] : Array.isArray(records) ? records : [records];
|
|
10209
10210
|
const accepted = [];
|
|
10210
10211
|
const rejected = [];
|
|
10211
|
-
let seq = 0;
|
|
10212
10212
|
for (const r of list) {
|
|
10213
10213
|
const errors = [];
|
|
10214
10214
|
const span = readSpan(r, errors);
|
|
@@ -10235,7 +10235,7 @@ function ingestEnergyBillRecords(records, opts) {
|
|
|
10235
10235
|
...billingDemandKW !== void 0 ? { billingDemandKW } : {}
|
|
10236
10236
|
};
|
|
10237
10237
|
accepted.push({
|
|
10238
|
-
eventId:
|
|
10238
|
+
eventId: periodFactId(opts.tenantId, ENERGY_EVENT.bill, "", span.from, span.to),
|
|
10239
10239
|
eventType: ENERGY_EVENT.bill,
|
|
10240
10240
|
eventTime: span.to,
|
|
10241
10241
|
tenantId: opts.tenantId,
|
package/package.json
CHANGED