@operato/twin-kernel 0.7.19 → 0.7.20
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 +19 -0
- package/dist/ems-kernel.js +11 -3
- package/dist/ems-profile.d.ts +19 -0
- package/dist/ems-profile.js +27 -0
- package/dist-cjs/index.cjs +18 -1
- package/package.json +1 -1
package/dist/contract.d.ts
CHANGED
|
@@ -699,6 +699,25 @@ export interface LocationState {
|
|
|
699
699
|
* 중간 단이 생기는 순간 그 아래 자리를 **집계에서 조용히 빠뜨린다**(정합성이 아니라 침묵이 문제다).
|
|
700
700
|
*/
|
|
701
701
|
parentId?: string;
|
|
702
|
+
/**
|
|
703
|
+
* **전기적으로 어디서 받는가** — 이 자리에 전기를 주는 상류 자리(수전·분기).
|
|
704
|
+
*
|
|
705
|
+
* ── 왜 `parentId` 와 따로 두나 (2026-08-18) ─────────────────────────────────
|
|
706
|
+
* 한동안 분기의 `parentId` 에 수전을 적었다. 그러면 한 필드가 두 뜻을 겸한다: **공간 포함**(이 분기는
|
|
707
|
+
* 공장 안에 있다)과 **전기 상류**(이 분기는 수전에서 받는다). 겸용의 값은 조용히 새어 나간다 — 공간
|
|
708
|
+
* 해석기가 수전을 「구역」으로 읽어 매 인제스트마다 거짓 경보를 냈고(「area 가 없는 것을 가리킨다」),
|
|
709
|
+
* 계통을 읽는 쪽은 공간 부모를 전기 상류로 오해할 위험을 안고 있었다.
|
|
710
|
+
*
|
|
711
|
+
* 두 관계는 실제로 다르다: 분기는 공장 **안에** 있고(공간), 수전에서 **받는다**(전기). 한 자리가 둘을
|
|
712
|
+
* 함께 가질 수 있으므로 필드도 둘이어야 한다.
|
|
713
|
+
*
|
|
714
|
+
* **옛 세대는 읽는 경계에서 흡수한다**(`electricalUpstreamOf`): 이 값이 없고 `parentId` 가 전기 자리를
|
|
715
|
+
* 가리키면 그것을 상류로 읽는다. 저장된 모델을 고치지 않고도 두 세대가 같은 답을 낸다.
|
|
716
|
+
*
|
|
717
|
+
* ⚠️ **물류 흐름의 상·하류가 아니다.** 흐름 쪽에는 같은 낱말이 다른 뜻으로 있다(씬의 `downstreamRef` 는
|
|
718
|
+
* 물건이 다음에 갈 곳이다). 이 값은 **전기를 어디서 받는가**이고, 물건의 이동과 무관하다.
|
|
719
|
+
*/
|
|
720
|
+
upstreamId?: string;
|
|
702
721
|
/**
|
|
703
722
|
* 이 자리를 **어떻게 알게 됐는가** — `master`(원 시스템 마스터/저작이 말해 준 자리) ·
|
|
704
723
|
* `observed`(이벤트에 등장해서 알게 된 자리). 소비처가 둘을 구별해야 한다: 관측으로 알게 된 자리는
|
package/dist/ems-kernel.js
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
import { FlowEngine } from "./flow-engine.js";
|
|
29
29
|
import { firstFitPolicy } from "./allocation-policy.js";
|
|
30
30
|
import { ENERGY_EVENT } from "./contract.js";
|
|
31
|
-
import { EMS_PROPERTY } from "./ems-profile.js";
|
|
31
|
+
import { EMS_PROPERTY, electricalUpstreamOf } from "./ems-profile.js";
|
|
32
32
|
/** 수요 구간 — 요금의 알갱이다. 15분은 한국·다수 요금제의 최대수요 산정 단위다. */
|
|
33
33
|
export const DEMAND_WINDOW_MS = 15 * 60 * 1000;
|
|
34
34
|
/** 그 시각이 속한 구간의 시작 — 벽시계 경계(00·15·30·45분)에 맞춘다. */
|
|
@@ -128,9 +128,17 @@ export class EmsKernel extends FlowEngine {
|
|
|
128
128
|
const locOf = new Map();
|
|
129
129
|
for (const e of this.boardDef?.equipment ?? [])
|
|
130
130
|
locOf.set(e.id, e.homeLocation);
|
|
131
|
+
/*
|
|
132
|
+
* 계층을 **전기 상류**로 걷는다 — 공간 부모가 아니다.
|
|
133
|
+
*
|
|
134
|
+
* 예전에는 `parentId` 를 그대로 걸었다. 그때는 분기의 `parentId` 에 수전이 적혀 있어서 답이 같았지만,
|
|
135
|
+
* 공간 부모(공장·구역)를 적은 모델에서는 그 사슬이 **전기적으로 뜻이 없는 길**을 따라간다. 상류의
|
|
136
|
+
* 뜻을 읽는 규칙은 한 곳에 있다(`electricalUpstreamOf` — 옛 세대도 그 안에서 흡수한다).
|
|
137
|
+
*/
|
|
138
|
+
const locs = this.boardDef?.locations ?? [];
|
|
131
139
|
const parentOf = new Map();
|
|
132
|
-
for (const l of
|
|
133
|
-
parentOf.set(l.id, l.
|
|
140
|
+
for (const l of locs)
|
|
141
|
+
parentOf.set(l.id, electricalUpstreamOf(locs, l.id));
|
|
134
142
|
/* 계량되고 있는 자리들 — 표본이 실제로 온 계량기의 자리만 센다(선언만 있고 값이 없는 계량기는
|
|
135
143
|
부하에 기여하지 않으므로 계층 판정에서도 제외한다). */
|
|
136
144
|
const meteredLocs = new Set();
|
package/dist/ems-profile.d.ts
CHANGED
|
@@ -67,3 +67,22 @@ export interface EmsPropertySpec {
|
|
|
67
67
|
}
|
|
68
68
|
export declare const EMS_PROPERTY_SPEC: Record<string, EmsPropertySpec>;
|
|
69
69
|
export declare const EMS_TYPES: TwinTypeInfo[];
|
|
70
|
+
/**
|
|
71
|
+
* 이 자리의 **전기 상류** — 어디서 전기를 받는가.
|
|
72
|
+
*
|
|
73
|
+
* ── 왜 함수로 두나 (2026-08-18) ─────────────────────────────────────────────
|
|
74
|
+
* 두 세대가 섞여 있다. 새 모델은 `upstream` 을 적고, 그 전 모델은 분기의 `parentId` 에 수전을 적었다.
|
|
75
|
+
* 판정을 소비처마다 쓰면(호스트·계통도·커널) 한쪽만 고쳐지고 그때부터 화면과 계산이 다른 계통을 말한다.
|
|
76
|
+
* 그래서 **읽는 규칙을 한 곳**에 둔다.
|
|
77
|
+
*
|
|
78
|
+
* 옛 세대를 흡수하는 조건이 좁다: `parentId` 가 **전기 자리**(수전·분기·구역 계량)를 가리킬 때만
|
|
79
|
+
* 상류로 읽는다. 공장·구역 같은 공간 부모를 상류로 읽으면 없는 결선을 만들어 낸다.
|
|
80
|
+
*/
|
|
81
|
+
export declare function electricalUpstreamOf(locations: readonly {
|
|
82
|
+
id: string;
|
|
83
|
+
type?: string;
|
|
84
|
+
parentId?: string;
|
|
85
|
+
upstreamId?: string;
|
|
86
|
+
}[] | undefined | null, id: string): string | undefined;
|
|
87
|
+
/** 전기 계통의 자리인가 — 수전·분기·구역 계량. */
|
|
88
|
+
export declare function isElectricalLocationType(type: string): boolean;
|
package/dist/ems-profile.js
CHANGED
|
@@ -210,3 +210,30 @@ export const EMS_TYPES = [
|
|
|
210
210
|
capabilities: ['curtailable', 'metered', 'operable']
|
|
211
211
|
}
|
|
212
212
|
];
|
|
213
|
+
/**
|
|
214
|
+
* 이 자리의 **전기 상류** — 어디서 전기를 받는가.
|
|
215
|
+
*
|
|
216
|
+
* ── 왜 함수로 두나 (2026-08-18) ─────────────────────────────────────────────
|
|
217
|
+
* 두 세대가 섞여 있다. 새 모델은 `upstream` 을 적고, 그 전 모델은 분기의 `parentId` 에 수전을 적었다.
|
|
218
|
+
* 판정을 소비처마다 쓰면(호스트·계통도·커널) 한쪽만 고쳐지고 그때부터 화면과 계산이 다른 계통을 말한다.
|
|
219
|
+
* 그래서 **읽는 규칙을 한 곳**에 둔다.
|
|
220
|
+
*
|
|
221
|
+
* 옛 세대를 흡수하는 조건이 좁다: `parentId` 가 **전기 자리**(수전·분기·구역 계량)를 가리킬 때만
|
|
222
|
+
* 상류로 읽는다. 공장·구역 같은 공간 부모를 상류로 읽으면 없는 결선을 만들어 낸다.
|
|
223
|
+
*/
|
|
224
|
+
export function electricalUpstreamOf(locations, id) {
|
|
225
|
+
const byId = new Map((locations ?? []).filter(l => l?.id).map(l => [String(l.id), l]));
|
|
226
|
+
const self = byId.get(String(id));
|
|
227
|
+
if (!self)
|
|
228
|
+
return undefined;
|
|
229
|
+
if (self.upstreamId)
|
|
230
|
+
return String(self.upstreamId);
|
|
231
|
+
const parent = self.parentId ? byId.get(String(self.parentId)) : undefined;
|
|
232
|
+
if (!parent)
|
|
233
|
+
return undefined;
|
|
234
|
+
return isElectricalLocationType(String(parent.type ?? '')) ? String(parent.id) : undefined;
|
|
235
|
+
}
|
|
236
|
+
/** 전기 계통의 자리인가 — 수전·분기·구역 계량. */
|
|
237
|
+
export function isElectricalLocationType(type) {
|
|
238
|
+
return EMS_LOCATION_TYPES.includes(type);
|
|
239
|
+
}
|
package/dist-cjs/index.cjs
CHANGED
|
@@ -97,6 +97,7 @@ __export(index_exports, {
|
|
|
97
97
|
documentPath: () => documentPath,
|
|
98
98
|
dueStatusOf: () => dueStatusOf,
|
|
99
99
|
effectivityAt: () => effectivityAt,
|
|
100
|
+
electricalUpstreamOf: () => electricalUpstreamOf,
|
|
100
101
|
electricityCost: () => electricityCost,
|
|
101
102
|
energyIntensity: () => energyIntensity,
|
|
102
103
|
energyOfWindows: () => energyOfWindows,
|
|
@@ -112,6 +113,7 @@ __export(index_exports, {
|
|
|
112
113
|
ingest: () => ingest,
|
|
113
114
|
ingestEnergyEquipmentRecords: () => ingestEnergyEquipmentRecords,
|
|
114
115
|
ingestEnergyRecords: () => ingestEnergyRecords,
|
|
116
|
+
isElectricalLocationType: () => isElectricalLocationType,
|
|
115
117
|
isEnergyEquipmentRecord: () => isEnergyEquipmentRecord,
|
|
116
118
|
isEnergyRecord: () => isEnergyRecord,
|
|
117
119
|
isEquipmentLevel: () => isEquipmentLevel,
|
|
@@ -1894,6 +1896,18 @@ var EMS_TYPES = [
|
|
|
1894
1896
|
capabilities: ["curtailable", "metered", "operable"]
|
|
1895
1897
|
}
|
|
1896
1898
|
];
|
|
1899
|
+
function electricalUpstreamOf(locations, id) {
|
|
1900
|
+
const byId = new Map((locations ?? []).filter((l) => l?.id).map((l) => [String(l.id), l]));
|
|
1901
|
+
const self = byId.get(String(id));
|
|
1902
|
+
if (!self) return void 0;
|
|
1903
|
+
if (self.upstreamId) return String(self.upstreamId);
|
|
1904
|
+
const parent = self.parentId ? byId.get(String(self.parentId)) : void 0;
|
|
1905
|
+
if (!parent) return void 0;
|
|
1906
|
+
return isElectricalLocationType(String(parent.type ?? "")) ? String(parent.id) : void 0;
|
|
1907
|
+
}
|
|
1908
|
+
function isElectricalLocationType(type) {
|
|
1909
|
+
return EMS_LOCATION_TYPES.includes(type);
|
|
1910
|
+
}
|
|
1897
1911
|
|
|
1898
1912
|
// src/capability.ts
|
|
1899
1913
|
var CAPABILITIES = {
|
|
@@ -6049,8 +6063,9 @@ var EmsKernel = class extends FlowEngine {
|
|
|
6049
6063
|
rootMeterIds() {
|
|
6050
6064
|
const locOf = /* @__PURE__ */ new Map();
|
|
6051
6065
|
for (const e of this.boardDef?.equipment ?? []) locOf.set(e.id, e.homeLocation);
|
|
6066
|
+
const locs = this.boardDef?.locations ?? [];
|
|
6052
6067
|
const parentOf = /* @__PURE__ */ new Map();
|
|
6053
|
-
for (const l of
|
|
6068
|
+
for (const l of locs) parentOf.set(l.id, electricalUpstreamOf(locs, l.id));
|
|
6054
6069
|
const meteredLocs = /* @__PURE__ */ new Set();
|
|
6055
6070
|
for (const id of this.points.keys()) {
|
|
6056
6071
|
const loc = locOf.get(id);
|
|
@@ -6942,6 +6957,7 @@ function retiredVocabularyIn(line) {
|
|
|
6942
6957
|
documentPath,
|
|
6943
6958
|
dueStatusOf,
|
|
6944
6959
|
effectivityAt,
|
|
6960
|
+
electricalUpstreamOf,
|
|
6945
6961
|
electricityCost,
|
|
6946
6962
|
energyIntensity,
|
|
6947
6963
|
energyOfWindows,
|
|
@@ -6957,6 +6973,7 @@ function retiredVocabularyIn(line) {
|
|
|
6957
6973
|
ingest,
|
|
6958
6974
|
ingestEnergyEquipmentRecords,
|
|
6959
6975
|
ingestEnergyRecords,
|
|
6976
|
+
isElectricalLocationType,
|
|
6960
6977
|
isEnergyEquipmentRecord,
|
|
6961
6978
|
isEnergyRecord,
|
|
6962
6979
|
isEquipmentLevel,
|
package/package.json
CHANGED