forked from jappel/leistungsbilanz-ts
Add recursive distribution board load projection
This commit is contained in:
parent
855a04e655
commit
dfeb0e3cb6
11 changed files with 701 additions and 138 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
import crypto from "node:crypto";
|
import crypto from "node:crypto";
|
||||||
import { and, asc, eq } from "drizzle-orm";
|
import { and, asc, eq, inArray } from "drizzle-orm";
|
||||||
import { defaultCircuitSectionDefinitions } from "../../domain/models/distribution-board-structure-project-command.model.js";
|
import { defaultCircuitSectionDefinitions } from "../../domain/models/distribution-board-structure-project-command.model.js";
|
||||||
import type { AppDatabase } from "../database-context.js";
|
import type { AppDatabase } from "../database-context.js";
|
||||||
import { circuitSections } from "../schema/circuit-sections.js";
|
import { circuitSections } from "../schema/circuit-sections.js";
|
||||||
|
|
@ -30,6 +30,15 @@ export class CircuitSectionRepository {
|
||||||
.orderBy(asc(circuitSections.sortOrder));
|
.orderBy(asc(circuitSections.sortOrder));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async listByCircuitLists(circuitListIds: string[]) {
|
||||||
|
if (circuitListIds.length === 0) return [];
|
||||||
|
return this.database
|
||||||
|
.select()
|
||||||
|
.from(circuitSections)
|
||||||
|
.where(inArray(circuitSections.circuitListId, circuitListIds))
|
||||||
|
.orderBy(asc(circuitSections.sortOrder), asc(circuitSections.id));
|
||||||
|
}
|
||||||
|
|
||||||
async createDefaults(circuitListId: string) {
|
async createDefaults(circuitListId: string) {
|
||||||
const db = this.database;
|
const db = this.database;
|
||||||
for (const entry of createDefaultCircuitSectionValues(circuitListId)) {
|
for (const entry of createDefaultCircuitSectionValues(circuitListId)) {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { asc, eq } from "drizzle-orm";
|
import { asc, eq, inArray } from "drizzle-orm";
|
||||||
import type { AppDatabase } from "../database-context.js";
|
import type { AppDatabase } from "../database-context.js";
|
||||||
import { circuits } from "../schema/circuits.js";
|
import { circuits } from "../schema/circuits.js";
|
||||||
|
|
||||||
|
|
@ -14,6 +14,15 @@ export class CircuitRepository {
|
||||||
.orderBy(asc(circuits.sortOrder), asc(circuits.equipmentIdentifier));
|
.orderBy(asc(circuits.sortOrder), asc(circuits.equipmentIdentifier));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async listByCircuitLists(circuitListIds: string[]) {
|
||||||
|
if (circuitListIds.length === 0) return [];
|
||||||
|
return this.database
|
||||||
|
.select()
|
||||||
|
.from(circuits)
|
||||||
|
.where(inArray(circuits.circuitListId, circuitListIds))
|
||||||
|
.orderBy(asc(circuits.sortOrder), asc(circuits.equipmentIdentifier));
|
||||||
|
}
|
||||||
|
|
||||||
async listBySection(sectionId: string) {
|
async listBySection(sectionId: string) {
|
||||||
const db = this.database;
|
const db = this.database;
|
||||||
return db
|
return db
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,18 @@ export class DistributionBoardComponentRepository {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async listByCircuitLists(circuitListIds: string[]) {
|
||||||
|
if (circuitListIds.length === 0) return [];
|
||||||
|
return this.database
|
||||||
|
.select()
|
||||||
|
.from(distributionBoardComponents)
|
||||||
|
.where(inArray(distributionBoardComponents.circuitListId, circuitListIds))
|
||||||
|
.orderBy(
|
||||||
|
asc(distributionBoardComponents.sortOrder),
|
||||||
|
asc(distributionBoardComponents.id)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async listProtectionByComponentIds(componentIds: string[]) {
|
async listProtectionByComponentIds(componentIds: string[]) {
|
||||||
if (componentIds.length === 0) {
|
if (componentIds.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
|
|
|
||||||
217
src/domain/calculations/distribution-board-load-calculation.ts
Normal file
217
src/domain/calculations/distribution-board-load-calculation.ts
Normal file
|
|
@ -0,0 +1,217 @@
|
||||||
|
export interface DistributionBoardLoadBoardInput {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
boardNumber: string | null;
|
||||||
|
simultaneityFactor: number;
|
||||||
|
feedingDistributionBoardId: string | null;
|
||||||
|
feedingCircuitId: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DistributionBoardLoadCircuitInput {
|
||||||
|
id: string;
|
||||||
|
distributionBoardId: string;
|
||||||
|
rows: Array<{
|
||||||
|
activePowerKw: number;
|
||||||
|
cosPhi: number | null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DownstreamDistributionBoardLoad {
|
||||||
|
distributionBoardId: string;
|
||||||
|
name: string;
|
||||||
|
boardNumber: string | null;
|
||||||
|
activePowerKw: number;
|
||||||
|
apparentPowerKva: number;
|
||||||
|
cosPhi: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CalculatedCircuitLoad {
|
||||||
|
circuitId: string;
|
||||||
|
deviceActivePowerKw: number;
|
||||||
|
deviceApparentPowerKva: number;
|
||||||
|
activePowerKw: number;
|
||||||
|
apparentPowerKva: number;
|
||||||
|
cosPhi: number;
|
||||||
|
downstreamBoards: DownstreamDistributionBoardLoad[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CalculatedDistributionBoardLoad {
|
||||||
|
distributionBoardId: string;
|
||||||
|
activePowerBeforeSimultaneityKw: number;
|
||||||
|
apparentPowerBeforeSimultaneityKva: number;
|
||||||
|
demandActivePowerKw: number;
|
||||||
|
demandApparentPowerKva: number;
|
||||||
|
cosPhi: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DistributionBoardLoadCalculation {
|
||||||
|
boards: Map<string, CalculatedDistributionBoardLoad>;
|
||||||
|
circuits: Map<string, CalculatedCircuitLoad>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates one project graph. A child board enters exactly one feeder
|
||||||
|
* circuit with its demand after the child's own board simultaneity factor.
|
||||||
|
* Board-only relations form a hierarchy but intentionally add no load.
|
||||||
|
*/
|
||||||
|
export function calculateDistributionBoardLoads(input: {
|
||||||
|
boards: DistributionBoardLoadBoardInput[];
|
||||||
|
circuits: DistributionBoardLoadCircuitInput[];
|
||||||
|
}): DistributionBoardLoadCalculation {
|
||||||
|
const boardById = new Map(input.boards.map((board) => [board.id, board]));
|
||||||
|
const circuitById = new Map(input.circuits.map((circuit) => [circuit.id, circuit]));
|
||||||
|
const circuitsByBoardId = groupBy(
|
||||||
|
input.circuits,
|
||||||
|
(circuit) => circuit.distributionBoardId
|
||||||
|
);
|
||||||
|
const childrenByCircuitId = new Map<string, DistributionBoardLoadBoardInput[]>();
|
||||||
|
for (const board of input.boards) {
|
||||||
|
assertBoardInput(board, boardById, circuitById);
|
||||||
|
if (board.feedingCircuitId === null) continue;
|
||||||
|
const children = childrenByCircuitId.get(board.feedingCircuitId) ?? [];
|
||||||
|
children.push(board);
|
||||||
|
childrenByCircuitId.set(board.feedingCircuitId, children);
|
||||||
|
}
|
||||||
|
|
||||||
|
const boards = new Map<string, CalculatedDistributionBoardLoad>();
|
||||||
|
const circuits = new Map<string, CalculatedCircuitLoad>();
|
||||||
|
const activePath = new Set<string>();
|
||||||
|
|
||||||
|
const calculateBoard = (boardId: string): CalculatedDistributionBoardLoad => {
|
||||||
|
const cached = boards.get(boardId);
|
||||||
|
if (cached) return cached;
|
||||||
|
if (activePath.has(boardId)) {
|
||||||
|
throw new Error("Distribution-board feeding graph contains a cycle.");
|
||||||
|
}
|
||||||
|
const board = boardById.get(boardId);
|
||||||
|
if (!board) throw new Error("Distribution board is missing from load graph.");
|
||||||
|
activePath.add(boardId);
|
||||||
|
|
||||||
|
let activePowerBeforeSimultaneityKw = 0;
|
||||||
|
let apparentPowerBeforeSimultaneityKva = 0;
|
||||||
|
for (const circuit of circuitsByBoardId.get(boardId) ?? []) {
|
||||||
|
let deviceActivePowerKw = 0;
|
||||||
|
let deviceApparentPowerKva = 0;
|
||||||
|
for (const row of circuit.rows) {
|
||||||
|
assertNonNegativeFinite(row.activePowerKw, "row active power");
|
||||||
|
const cosPhi = row.cosPhi ?? 1;
|
||||||
|
if (!Number.isFinite(cosPhi) || cosPhi <= 0 || cosPhi > 1) {
|
||||||
|
throw new Error("Row cos phi must be greater than zero and at most one.");
|
||||||
|
}
|
||||||
|
deviceActivePowerKw += row.activePowerKw;
|
||||||
|
deviceApparentPowerKva += row.activePowerKw / cosPhi;
|
||||||
|
}
|
||||||
|
const downstreamBoards = (childrenByCircuitId.get(circuit.id) ?? [])
|
||||||
|
.map((child) => {
|
||||||
|
const childLoad = calculateBoard(child.id);
|
||||||
|
return {
|
||||||
|
distributionBoardId: child.id,
|
||||||
|
name: child.name,
|
||||||
|
boardNumber: child.boardNumber,
|
||||||
|
activePowerKw: childLoad.demandActivePowerKw,
|
||||||
|
apparentPowerKva: childLoad.demandApparentPowerKva,
|
||||||
|
cosPhi: effectiveCosPhi(
|
||||||
|
childLoad.demandActivePowerKw,
|
||||||
|
childLoad.demandApparentPowerKva
|
||||||
|
),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const activePowerKw =
|
||||||
|
deviceActivePowerKw +
|
||||||
|
downstreamBoards.reduce((sum, child) => sum + child.activePowerKw, 0);
|
||||||
|
const apparentPowerKva =
|
||||||
|
deviceApparentPowerKva +
|
||||||
|
downstreamBoards.reduce((sum, child) => sum + child.apparentPowerKva, 0);
|
||||||
|
circuits.set(circuit.id, {
|
||||||
|
circuitId: circuit.id,
|
||||||
|
deviceActivePowerKw,
|
||||||
|
deviceApparentPowerKva,
|
||||||
|
activePowerKw,
|
||||||
|
apparentPowerKva,
|
||||||
|
cosPhi: effectiveCosPhi(activePowerKw, apparentPowerKva),
|
||||||
|
downstreamBoards,
|
||||||
|
});
|
||||||
|
activePowerBeforeSimultaneityKw += activePowerKw;
|
||||||
|
apparentPowerBeforeSimultaneityKva += apparentPowerKva;
|
||||||
|
}
|
||||||
|
|
||||||
|
activePath.delete(boardId);
|
||||||
|
const result = {
|
||||||
|
distributionBoardId: boardId,
|
||||||
|
activePowerBeforeSimultaneityKw,
|
||||||
|
apparentPowerBeforeSimultaneityKva,
|
||||||
|
demandActivePowerKw:
|
||||||
|
activePowerBeforeSimultaneityKw * board.simultaneityFactor,
|
||||||
|
demandApparentPowerKva:
|
||||||
|
apparentPowerBeforeSimultaneityKva * board.simultaneityFactor,
|
||||||
|
cosPhi: effectiveCosPhi(
|
||||||
|
activePowerBeforeSimultaneityKw,
|
||||||
|
apparentPowerBeforeSimultaneityKva
|
||||||
|
),
|
||||||
|
};
|
||||||
|
boards.set(boardId, result);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const board of input.boards) calculateBoard(board.id);
|
||||||
|
return { boards, circuits };
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertBoardInput(
|
||||||
|
board: DistributionBoardLoadBoardInput,
|
||||||
|
boardById: Map<string, DistributionBoardLoadBoardInput>,
|
||||||
|
circuitById: Map<string, DistributionBoardLoadCircuitInput>
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
!Number.isFinite(board.simultaneityFactor) ||
|
||||||
|
board.simultaneityFactor < 0 ||
|
||||||
|
board.simultaneityFactor > 1
|
||||||
|
) {
|
||||||
|
throw new Error("Board simultaneity factor must be between zero and one.");
|
||||||
|
}
|
||||||
|
if (board.feedingDistributionBoardId === board.id) {
|
||||||
|
throw new Error("Distribution board cannot feed itself.");
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
board.feedingDistributionBoardId !== null &&
|
||||||
|
!boardById.has(board.feedingDistributionBoardId)
|
||||||
|
) {
|
||||||
|
throw new Error("Feeding distribution board is missing from load graph.");
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
board.feedingCircuitId !== null &&
|
||||||
|
board.feedingDistributionBoardId === null
|
||||||
|
) {
|
||||||
|
throw new Error("Feeding circuit requires a feeding distribution board.");
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
board.feedingCircuitId !== null &&
|
||||||
|
circuitById.get(board.feedingCircuitId)?.distributionBoardId !==
|
||||||
|
board.feedingDistributionBoardId
|
||||||
|
) {
|
||||||
|
throw new Error("Feeding circuit belongs to another distribution board.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function effectiveCosPhi(activePowerKw: number, apparentPowerKva: number) {
|
||||||
|
return activePowerKw > 0 && apparentPowerKva > 0
|
||||||
|
? activePowerKw / apparentPowerKva
|
||||||
|
: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertNonNegativeFinite(value: number, label: string) {
|
||||||
|
if (!Number.isFinite(value) || value < 0) {
|
||||||
|
throw new Error(`${label} must be a non-negative finite number.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function groupBy<T>(values: T[], keyOf: (value: T) => string) {
|
||||||
|
const groups = new Map<string, T[]>();
|
||||||
|
for (const value of values) {
|
||||||
|
const key = keyOf(value);
|
||||||
|
const group = groups.get(key) ?? [];
|
||||||
|
group.push(value);
|
||||||
|
groups.set(key, group);
|
||||||
|
}
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
|
@ -40,7 +40,10 @@ export interface CircuitTreeCircuit {
|
||||||
isReserve: boolean;
|
isReserve: boolean;
|
||||||
remark?: string;
|
remark?: string;
|
||||||
circuitTotalPower: number;
|
circuitTotalPower: number;
|
||||||
|
circuitApparentPower: number;
|
||||||
|
circuitCosPhi: number;
|
||||||
deviceRows: CircuitTreeDeviceRow[];
|
deviceRows: CircuitTreeDeviceRow[];
|
||||||
|
downstreamBoardLoads: CircuitTreeDownstreamBoardLoad[];
|
||||||
protectionDevice?: CircuitTreeProtectionDevice;
|
protectionDevice?: CircuitTreeProtectionDevice;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -59,6 +62,11 @@ export interface CircuitTreeSectionBlock {
|
||||||
|
|
||||||
export interface CircuitTreeResponse {
|
export interface CircuitTreeResponse {
|
||||||
circuitListId: string;
|
circuitListId: string;
|
||||||
|
distributionBoardId: string;
|
||||||
|
distributionBoardName: string;
|
||||||
|
distributionBoardNumber?: string;
|
||||||
|
distributionBoardSupplyType?: DistributionBoardSupplyType;
|
||||||
|
feedingReference?: CircuitTreeFeedingReference;
|
||||||
currentRevision: number;
|
currentRevision: number;
|
||||||
singlePhaseVoltageV: number;
|
singlePhaseVoltageV: number;
|
||||||
threePhaseVoltageV: number;
|
threePhaseVoltageV: number;
|
||||||
|
|
@ -70,7 +78,35 @@ export interface CircuitTreeResponse {
|
||||||
footerComponents: CircuitTreeComponent[];
|
footerComponents: CircuitTreeComponent[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CircuitTreeDownstreamBoardLoad {
|
||||||
|
distributionBoardId: string;
|
||||||
|
name: string;
|
||||||
|
boardNumber?: string;
|
||||||
|
supplyType?: DistributionBoardSupplyType;
|
||||||
|
activePowerKw: number;
|
||||||
|
apparentPowerKva: number;
|
||||||
|
cosPhi: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CircuitTreeFeedingReference {
|
||||||
|
distributionBoardId: string;
|
||||||
|
distributionBoardName: string;
|
||||||
|
distributionBoardNumber?: string;
|
||||||
|
circuitId?: string;
|
||||||
|
circuitEquipmentIdentifier?: string;
|
||||||
|
circuitDisplayName?: string;
|
||||||
|
circuitCategory?: CircuitGroupCategory;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProjectCircuitTreesResponse {
|
||||||
|
currentRevision: number;
|
||||||
|
trees: CircuitTreeResponse[];
|
||||||
|
}
|
||||||
|
|
||||||
import type { CircuitGroupCategory } from "../../shared/constants/circuit-group.js";
|
import type { CircuitGroupCategory } from "../../shared/constants/circuit-group.js";
|
||||||
|
import type {
|
||||||
|
DistributionBoardSupplyType,
|
||||||
|
} from "../../shared/constants/distribution-board.js";
|
||||||
import type {
|
import type {
|
||||||
DistributionBoardComponentPlacement,
|
DistributionBoardComponentPlacement,
|
||||||
DistributionBoardComponentRole,
|
DistributionBoardComponentRole,
|
||||||
|
|
|
||||||
|
|
@ -444,7 +444,10 @@ export interface CircuitTreeCircuitDto {
|
||||||
isReserve: boolean;
|
isReserve: boolean;
|
||||||
remark?: string;
|
remark?: string;
|
||||||
circuitTotalPower: number;
|
circuitTotalPower: number;
|
||||||
|
circuitApparentPower: number;
|
||||||
|
circuitCosPhi: number;
|
||||||
deviceRows: CircuitTreeDeviceRowDto[];
|
deviceRows: CircuitTreeDeviceRowDto[];
|
||||||
|
downstreamBoardLoads: CircuitTreeDownstreamBoardLoadDto[];
|
||||||
protectionDevice?: CircuitTreeProtectionDeviceDto;
|
protectionDevice?: CircuitTreeProtectionDeviceDto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -463,6 +466,11 @@ export interface CircuitTreeSectionDto {
|
||||||
|
|
||||||
export interface CircuitTreeResponseDto {
|
export interface CircuitTreeResponseDto {
|
||||||
circuitListId: string;
|
circuitListId: string;
|
||||||
|
distributionBoardId: string;
|
||||||
|
distributionBoardName: string;
|
||||||
|
distributionBoardNumber?: string;
|
||||||
|
distributionBoardSupplyType?: DistributionBoardSupplyType;
|
||||||
|
feedingReference?: CircuitTreeFeedingReferenceDto;
|
||||||
currentRevision: number;
|
currentRevision: number;
|
||||||
singlePhaseVoltageV: number;
|
singlePhaseVoltageV: number;
|
||||||
threePhaseVoltageV: number;
|
threePhaseVoltageV: number;
|
||||||
|
|
@ -474,6 +482,31 @@ export interface CircuitTreeResponseDto {
|
||||||
footerComponents: CircuitTreeComponentDto[];
|
footerComponents: CircuitTreeComponentDto[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CircuitTreeDownstreamBoardLoadDto {
|
||||||
|
distributionBoardId: string;
|
||||||
|
name: string;
|
||||||
|
boardNumber?: string;
|
||||||
|
supplyType?: DistributionBoardSupplyType;
|
||||||
|
activePowerKw: number;
|
||||||
|
apparentPowerKva: number;
|
||||||
|
cosPhi: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CircuitTreeFeedingReferenceDto {
|
||||||
|
distributionBoardId: string;
|
||||||
|
distributionBoardName: string;
|
||||||
|
distributionBoardNumber?: string;
|
||||||
|
circuitId?: string;
|
||||||
|
circuitEquipmentIdentifier?: string;
|
||||||
|
circuitDisplayName?: string;
|
||||||
|
circuitCategory?: CircuitGroupCategory;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProjectCircuitTreesResponseDto {
|
||||||
|
currentRevision: number;
|
||||||
|
trees: CircuitTreeResponseDto[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface ExternalCircuitListObjectDto {
|
export interface ExternalCircuitListObjectDto {
|
||||||
id: string;
|
id: string;
|
||||||
ifcGuid: string;
|
ifcGuid: string;
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ import type {
|
||||||
ProjectDto,
|
ProjectDto,
|
||||||
RoomDto,
|
RoomDto,
|
||||||
CircuitTreeResponseDto,
|
CircuitTreeResponseDto,
|
||||||
|
ProjectCircuitTreesResponseDto,
|
||||||
ExternalCsvConfigurationSnapshotDto,
|
ExternalCsvConfigurationSnapshotDto,
|
||||||
ExternalCsvPreviewDto,
|
ExternalCsvPreviewDto,
|
||||||
ExternalInitialImportPlanDto,
|
ExternalInitialImportPlanDto,
|
||||||
|
|
@ -529,6 +530,12 @@ export function getCircuitTree(projectId: string, circuitListId: string) {
|
||||||
return request<CircuitTreeResponseDto>(`/api/projects/${projectId}/circuit-lists/${circuitListId}/tree`);
|
return request<CircuitTreeResponseDto>(`/api/projects/${projectId}/circuit-lists/${circuitListId}/tree`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getProjectCircuitTrees(projectId: string) {
|
||||||
|
return request<ProjectCircuitTreesResponseDto>(
|
||||||
|
`/api/projects/${projectId}/circuit-trees`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function insertDistributionBoardComponentCommand(
|
export function insertDistributionBoardComponentCommand(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
expectedRevision: number,
|
expectedRevision: number,
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,11 @@
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from "express";
|
||||||
import {
|
import { calculateDistributionBoardLoads } from "../../domain/calculations/distribution-board-load-calculation.js";
|
||||||
applyDistributionBoardSimultaneityFactor,
|
import { calculateRowTotalPower } from "../../domain/calculations/circuit-power-calculation.js";
|
||||||
calculateCircuitTotalPower,
|
import type {
|
||||||
calculateDistributionBoardTotalPower,
|
CircuitTreeProtectionDevice,
|
||||||
calculateRowTotalPower,
|
CircuitTreeResponse,
|
||||||
calculateSectionTotalPower,
|
ProjectCircuitTreesResponse,
|
||||||
} from "../../domain/calculations/circuit-power-calculation.js";
|
} from "../../domain/models/circuit-tree.model.js";
|
||||||
import type { CircuitTreeResponse } from "../../domain/models/circuit-tree.model.js";
|
|
||||||
import type { CircuitTreeProtectionDevice } from "../../domain/models/circuit-tree.model.js";
|
|
||||||
import {
|
import {
|
||||||
circuitDeviceRowRepository,
|
circuitDeviceRowRepository,
|
||||||
circuitListRepository,
|
circuitListRepository,
|
||||||
|
|
@ -20,9 +18,7 @@ import {
|
||||||
} from "../composition/application-repositories.js";
|
} from "../composition/application-repositories.js";
|
||||||
|
|
||||||
export function isMissingCircuitTreeSchemaError(error: unknown): boolean {
|
export function isMissingCircuitTreeSchemaError(error: unknown): boolean {
|
||||||
if (!(error instanceof Error)) {
|
if (!(error instanceof Error)) return false;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
error.message.includes("no such table: circuit_sections") ||
|
error.message.includes("no such table: circuit_sections") ||
|
||||||
error.message.includes("no such table: circuits") ||
|
error.message.includes("no such table: circuits") ||
|
||||||
|
|
@ -37,70 +33,143 @@ export async function getCircuitTree(req: Request, res: Response) {
|
||||||
if (typeof projectId !== "string" || typeof circuitListId !== "string") {
|
if (typeof projectId !== "string" || typeof circuitListId !== "string") {
|
||||||
return res.status(400).json({ error: "Invalid parameters" });
|
return res.status(400).json({ error: "Invalid parameters" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const list = await circuitListRepository.findById(projectId, circuitListId);
|
const list = await circuitListRepository.findById(projectId, circuitListId);
|
||||||
if (!list) {
|
if (!list) return res.status(404).json({ error: "Circuit list not found" });
|
||||||
return res.status(404).json({ error: "Circuit list not found" });
|
|
||||||
|
try {
|
||||||
|
const result = await buildProjectCircuitTrees(projectId);
|
||||||
|
if (!result) return res.status(404).json({ error: "Project not found" });
|
||||||
|
const tree = result.trees.find((entry) => entry.circuitListId === circuitListId);
|
||||||
|
if (!tree) {
|
||||||
|
return res.status(404).json({ error: "Distribution board not found" });
|
||||||
}
|
}
|
||||||
|
return res.json(tree);
|
||||||
|
} catch (error) {
|
||||||
|
if (isMissingCircuitTreeSchemaError(error)) {
|
||||||
const project = await projectRepository.findById(projectId);
|
const project = await projectRepository.findById(projectId);
|
||||||
if (!project) {
|
const board = await distributionBoardRepository.findById(
|
||||||
return res.status(404).json({ error: "Project not found" });
|
|
||||||
}
|
|
||||||
const distributionBoard =
|
|
||||||
await distributionBoardRepository.findById(
|
|
||||||
projectId,
|
projectId,
|
||||||
list.distributionBoardId
|
list.distributionBoardId
|
||||||
);
|
);
|
||||||
if (!distributionBoard) {
|
if (!project || !board) {
|
||||||
return res.status(404).json({
|
return res.status(404).json({ error: "Project structure not found" });
|
||||||
error: "Distribution board not found",
|
}
|
||||||
});
|
return res.json(emptyCircuitTree(project, board, circuitListId));
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getProjectCircuitTrees(req: Request, res: Response) {
|
||||||
|
const { projectId } = req.params;
|
||||||
|
if (typeof projectId !== "string") {
|
||||||
|
return res.status(400).json({ error: "Invalid parameters" });
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const sections = await circuitSectionRepository.listByCircuitList(circuitListId);
|
const result = await buildProjectCircuitTrees(projectId);
|
||||||
const circuits = await circuitRepository.listByCircuitList(circuitListId);
|
if (!result) return res.status(404).json({ error: "Project not found" });
|
||||||
const rows = await circuitDeviceRowRepository.listByCircuitList(circuits.map((entry) => entry.id));
|
return res.json(result);
|
||||||
const components =
|
} catch (error) {
|
||||||
await distributionBoardComponentRepository.listByCircuitList(
|
if (isMissingCircuitTreeSchemaError(error)) {
|
||||||
circuitListId
|
const project = await projectRepository.findById(projectId);
|
||||||
);
|
if (!project) return res.status(404).json({ error: "Project not found" });
|
||||||
const [circuitProtections, componentProtections] =
|
return res.json({ currentRevision: project.currentRevision, trees: [] });
|
||||||
await Promise.all([
|
}
|
||||||
circuitProtectionDeviceRepository.listByCircuitIds(
|
throw error;
|
||||||
circuits.map((entry) => entry.id)
|
}
|
||||||
),
|
}
|
||||||
|
|
||||||
|
export async function buildProjectCircuitTrees(
|
||||||
|
projectId: string
|
||||||
|
): Promise<ProjectCircuitTreesResponse | null> {
|
||||||
|
const project = await projectRepository.findById(projectId);
|
||||||
|
if (!project) return null;
|
||||||
|
const [lists, boards] = await Promise.all([
|
||||||
|
circuitListRepository.listByProject(projectId),
|
||||||
|
distributionBoardRepository.listByProject(projectId),
|
||||||
|
]);
|
||||||
|
const listIds = lists.map((entry) => entry.id);
|
||||||
|
const [sections, circuits, components] = await Promise.all([
|
||||||
|
circuitSectionRepository.listByCircuitLists(listIds),
|
||||||
|
circuitRepository.listByCircuitLists(listIds),
|
||||||
|
distributionBoardComponentRepository.listByCircuitLists(listIds),
|
||||||
|
]);
|
||||||
|
const [rows, circuitProtections, componentProtections] = await Promise.all([
|
||||||
|
circuitDeviceRowRepository.listByCircuitList(circuits.map((entry) => entry.id)),
|
||||||
|
circuitProtectionDeviceRepository.listByCircuitIds(circuits.map((entry) => entry.id)),
|
||||||
distributionBoardComponentRepository.listProtectionByComponentIds(
|
distributionBoardComponentRepository.listProtectionByComponentIds(
|
||||||
components.map((entry) => entry.id)
|
components.map((entry) => entry.id)
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const boardById = new Map(boards.map((board) => [board.id, board]));
|
||||||
|
const listById = new Map(lists.map((list) => [list.id, list]));
|
||||||
const sectionById = new Map(sections.map((section) => [section.id, section]));
|
const sectionById = new Map(sections.map((section) => [section.id, section]));
|
||||||
const rowsByCircuitId = new Map<string, typeof rows>();
|
const circuitById = new Map(circuits.map((circuit) => [circuit.id, circuit]));
|
||||||
|
const rowsByCircuitId = groupBy(rows, (row) => row.circuitId);
|
||||||
|
const sectionsByListId = groupBy(sections, (section) => section.circuitListId);
|
||||||
|
const circuitsByListId = groupBy(circuits, (circuit) => circuit.circuitListId);
|
||||||
|
const componentsByListId = groupBy(components, (component) => component.circuitListId);
|
||||||
const circuitProtectionById = new Map(
|
const circuitProtectionById = new Map(
|
||||||
circuitProtections.map((entry) => [entry.circuitId, entry])
|
circuitProtections.map((entry) => [entry.circuitId, entry])
|
||||||
);
|
);
|
||||||
const componentProtectionById = new Map(
|
const componentProtectionById = new Map(
|
||||||
componentProtections.map((entry) => [entry.componentId, entry])
|
componentProtections.map((entry) => [entry.componentId, entry])
|
||||||
);
|
);
|
||||||
for (const row of rows) {
|
|
||||||
if (!rowsByCircuitId.has(row.circuitId)) {
|
|
||||||
rowsByCircuitId.set(row.circuitId, []);
|
|
||||||
}
|
|
||||||
rowsByCircuitId.get(row.circuitId)!.push(row);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const loads = calculateDistributionBoardLoads({
|
||||||
|
boards: boards.map((board) => ({
|
||||||
|
id: board.id,
|
||||||
|
name: board.name,
|
||||||
|
boardNumber: board.boardNumber,
|
||||||
|
simultaneityFactor: board.simultaneityFactor,
|
||||||
|
feedingDistributionBoardId: board.feedingDistributionBoardId,
|
||||||
|
feedingCircuitId: board.feedingCircuitId,
|
||||||
|
})),
|
||||||
|
circuits: circuits.map((circuit) => {
|
||||||
|
const list = listById.get(circuit.circuitListId);
|
||||||
|
if (!list) throw new Error("Circuit list is missing from project tree.");
|
||||||
|
return {
|
||||||
|
id: circuit.id,
|
||||||
|
distributionBoardId: list.distributionBoardId,
|
||||||
|
rows: (rowsByCircuitId.get(circuit.id) ?? []).map((row) => ({
|
||||||
|
activePowerKw: calculateRowTotalPower(
|
||||||
|
row.quantity,
|
||||||
|
row.powerPerUnit,
|
||||||
|
row.simultaneityFactor
|
||||||
|
),
|
||||||
|
cosPhi: row.cosPhi,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const trees = lists.map((list): CircuitTreeResponse => {
|
||||||
|
const board = boardById.get(list.distributionBoardId);
|
||||||
|
if (!board) throw new Error("Distribution board is missing from project tree.");
|
||||||
|
const boardLoad = loads.boards.get(board.id);
|
||||||
|
if (!boardLoad) throw new Error("Distribution-board load is missing.");
|
||||||
const tree: CircuitTreeResponse = {
|
const tree: CircuitTreeResponse = {
|
||||||
circuitListId,
|
circuitListId: list.id,
|
||||||
|
distributionBoardId: board.id,
|
||||||
|
distributionBoardName: board.name,
|
||||||
|
distributionBoardNumber: board.boardNumber ?? undefined,
|
||||||
|
distributionBoardSupplyType: board.supplyType ?? undefined,
|
||||||
|
feedingReference: resolveFeedingReference(
|
||||||
|
board,
|
||||||
|
boardById,
|
||||||
|
circuitById,
|
||||||
|
sectionById
|
||||||
|
),
|
||||||
currentRevision: project.currentRevision,
|
currentRevision: project.currentRevision,
|
||||||
singlePhaseVoltageV: project.singlePhaseVoltageV,
|
singlePhaseVoltageV: project.singlePhaseVoltageV,
|
||||||
threePhaseVoltageV: project.threePhaseVoltageV,
|
threePhaseVoltageV: project.threePhaseVoltageV,
|
||||||
distributionBoardSimultaneityFactor:
|
distributionBoardSimultaneityFactor: board.simultaneityFactor,
|
||||||
distributionBoard.simultaneityFactor,
|
distributionBoardTotalPower: boardLoad.activePowerBeforeSimultaneityKw,
|
||||||
distributionBoardTotalPower: 0,
|
distributionBoardTotalPowerWithSimultaneityFactor:
|
||||||
distributionBoardTotalPowerWithSimultaneityFactor: 0,
|
boardLoad.demandActivePowerKw,
|
||||||
headerComponents: [],
|
headerComponents: [],
|
||||||
sections: sections.map((section) => ({
|
sections: (sectionsByListId.get(list.id) ?? []).map((section) => ({
|
||||||
id: section.id,
|
id: section.id,
|
||||||
key: section.key,
|
key: section.key,
|
||||||
displayName: section.displayName,
|
displayName: section.displayName,
|
||||||
|
|
@ -115,9 +184,8 @@ export async function getCircuitTree(req: Request, res: Response) {
|
||||||
footerComponents: [],
|
footerComponents: [],
|
||||||
};
|
};
|
||||||
const sectionBlocks = new Map(tree.sections.map((section) => [section.id, section]));
|
const sectionBlocks = new Map(tree.sections.map((section) => [section.id, section]));
|
||||||
for (const component of components) {
|
for (const component of componentsByListId.get(list.id) ?? []) {
|
||||||
const protection =
|
const protection = componentProtectionById.get(component.id);
|
||||||
componentProtectionById.get(component.id);
|
|
||||||
const dto = {
|
const dto = {
|
||||||
id: component.id,
|
id: component.id,
|
||||||
circuitListId: component.circuitListId,
|
circuitListId: component.circuitListId,
|
||||||
|
|
@ -127,24 +195,18 @@ export async function getCircuitTree(req: Request, res: Response) {
|
||||||
role: component.role,
|
role: component.role,
|
||||||
placement: component.placement,
|
placement: component.placement,
|
||||||
sortOrder: component.sortOrder,
|
sortOrder: component.sortOrder,
|
||||||
protectionDevice: protection
|
protectionDevice: protection ? toProtectionDeviceDto(protection) : undefined,
|
||||||
? toProtectionDeviceDto(protection)
|
|
||||||
: undefined,
|
|
||||||
};
|
};
|
||||||
if (component.placement === "header") {
|
if (component.placement === "header") tree.headerComponents.push(dto);
|
||||||
tree.headerComponents.push(dto);
|
else if (component.placement === "footer") tree.footerComponents.push(dto);
|
||||||
} else if (component.placement === "footer") {
|
else if (component.sectionId !== null) {
|
||||||
tree.footerComponents.push(dto);
|
|
||||||
} else if (component.sectionId !== null) {
|
|
||||||
sectionBlocks.get(component.sectionId)?.components.push(dto);
|
sectionBlocks.get(component.sectionId)?.components.push(dto);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const circuit of circuits) {
|
for (const circuit of circuitsByListId.get(list.id) ?? []) {
|
||||||
const section = sectionById.get(circuit.sectionId);
|
const block = sectionBlocks.get(circuit.sectionId);
|
||||||
if (!section || section.circuitListId !== circuit.circuitListId) {
|
if (!block) continue;
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const deviceRows = (rowsByCircuitId.get(circuit.id) ?? []).map((row) => ({
|
const deviceRows = (rowsByCircuitId.get(circuit.id) ?? []).map((row) => ({
|
||||||
id: row.id,
|
id: row.id,
|
||||||
linkedProjectDeviceId: row.linkedProjectDeviceId ?? undefined,
|
linkedProjectDeviceId: row.linkedProjectDeviceId ?? undefined,
|
||||||
|
|
@ -166,12 +228,16 @@ export async function getCircuitTree(req: Request, res: Response) {
|
||||||
cosPhi: row.cosPhi ?? undefined,
|
cosPhi: row.cosPhi ?? undefined,
|
||||||
remark: row.remark ?? undefined,
|
remark: row.remark ?? undefined,
|
||||||
overriddenFields: row.overriddenFields ?? undefined,
|
overriddenFields: row.overriddenFields ?? undefined,
|
||||||
rowTotalPower: calculateRowTotalPower(row.quantity, row.powerPerUnit, row.simultaneityFactor),
|
rowTotalPower: calculateRowTotalPower(
|
||||||
|
row.quantity,
|
||||||
|
row.powerPerUnit,
|
||||||
|
row.simultaneityFactor
|
||||||
|
),
|
||||||
}));
|
}));
|
||||||
const circuitTotalPower = calculateCircuitTotalPower(deviceRows);
|
const circuitLoad = loads.circuits.get(circuit.id);
|
||||||
|
if (!circuitLoad) throw new Error("Circuit load is missing.");
|
||||||
const protection = circuitProtectionById.get(circuit.id);
|
const protection = circuitProtectionById.get(circuit.id);
|
||||||
|
block.circuits.push({
|
||||||
sectionBlocks.get(section.id)?.circuits.push({
|
|
||||||
id: circuit.id,
|
id: circuit.id,
|
||||||
circuitListId: circuit.circuitListId,
|
circuitListId: circuit.circuitListId,
|
||||||
sectionId: circuit.sectionId,
|
sectionId: circuit.sectionId,
|
||||||
|
|
@ -188,37 +254,81 @@ export async function getCircuitTree(req: Request, res: Response) {
|
||||||
status: circuit.status ?? undefined,
|
status: circuit.status ?? undefined,
|
||||||
isReserve: Boolean(circuit.isReserve),
|
isReserve: Boolean(circuit.isReserve),
|
||||||
remark: circuit.remark ?? undefined,
|
remark: circuit.remark ?? undefined,
|
||||||
circuitTotalPower,
|
circuitTotalPower: circuitLoad.activePowerKw,
|
||||||
|
circuitApparentPower: circuitLoad.apparentPowerKva,
|
||||||
|
circuitCosPhi: circuitLoad.cosPhi,
|
||||||
deviceRows,
|
deviceRows,
|
||||||
protectionDevice: protection
|
downstreamBoardLoads: circuitLoad.downstreamBoards
|
||||||
? toProtectionDeviceDto(protection)
|
.map((load) => ({
|
||||||
: undefined,
|
distributionBoardId: load.distributionBoardId,
|
||||||
|
name: load.name,
|
||||||
|
boardNumber: load.boardNumber ?? undefined,
|
||||||
|
supplyType: boardById.get(load.distributionBoardId)?.supplyType ?? undefined,
|
||||||
|
activePowerKw: load.activePowerKw,
|
||||||
|
apparentPowerKva: load.apparentPowerKva,
|
||||||
|
cosPhi: load.cosPhi,
|
||||||
|
}))
|
||||||
|
.sort((left, right) =>
|
||||||
|
(left.boardNumber ?? left.name).localeCompare(
|
||||||
|
right.boardNumber ?? right.name,
|
||||||
|
"de"
|
||||||
|
)
|
||||||
|
),
|
||||||
|
protectionDevice: protection ? toProtectionDeviceDto(protection) : undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const section of tree.sections) {
|
for (const section of tree.sections) {
|
||||||
section.sectionTotalPower = calculateSectionTotalPower(
|
section.sectionTotalPower = section.circuits.reduce(
|
||||||
section.circuits
|
(sum, circuit) => sum + circuit.circuitTotalPower,
|
||||||
|
0
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
tree.distributionBoardTotalPower =
|
return tree;
|
||||||
calculateDistributionBoardTotalPower(tree.sections);
|
});
|
||||||
tree.distributionBoardTotalPowerWithSimultaneityFactor =
|
|
||||||
applyDistributionBoardSimultaneityFactor(
|
|
||||||
tree.distributionBoardTotalPower,
|
|
||||||
tree.distributionBoardSimultaneityFactor
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.json(tree);
|
return { currentRevision: project.currentRevision, trees };
|
||||||
} catch (error) {
|
}
|
||||||
if (isMissingCircuitTreeSchemaError(error)) {
|
|
||||||
return res.json({
|
function resolveFeedingReference(
|
||||||
|
board: Awaited<ReturnType<typeof distributionBoardRepository.listByProject>>[number],
|
||||||
|
boardById: Map<string, Awaited<ReturnType<typeof distributionBoardRepository.listByProject>>[number]>,
|
||||||
|
circuitById: Map<string, Awaited<ReturnType<typeof circuitRepository.listByCircuitLists>>[number]>,
|
||||||
|
sectionById: Map<string, Awaited<ReturnType<typeof circuitSectionRepository.listByCircuitLists>>[number]>
|
||||||
|
) {
|
||||||
|
if (board.feedingDistributionBoardId === null) return undefined;
|
||||||
|
const feedingBoard = boardById.get(board.feedingDistributionBoardId);
|
||||||
|
if (!feedingBoard) throw new Error("Feeding distribution board is missing.");
|
||||||
|
const feedingCircuit = board.feedingCircuitId === null
|
||||||
|
? undefined
|
||||||
|
: circuitById.get(board.feedingCircuitId);
|
||||||
|
return {
|
||||||
|
distributionBoardId: feedingBoard.id,
|
||||||
|
distributionBoardName: feedingBoard.name,
|
||||||
|
distributionBoardNumber: feedingBoard.boardNumber ?? undefined,
|
||||||
|
circuitId: feedingCircuit?.id,
|
||||||
|
circuitEquipmentIdentifier: feedingCircuit?.equipmentIdentifier,
|
||||||
|
circuitDisplayName: feedingCircuit?.displayName ?? undefined,
|
||||||
|
circuitCategory: feedingCircuit
|
||||||
|
? sectionById.get(feedingCircuit.sectionId)?.category ?? undefined
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function emptyCircuitTree(
|
||||||
|
project: NonNullable<Awaited<ReturnType<typeof projectRepository.findById>>>,
|
||||||
|
board: NonNullable<Awaited<ReturnType<typeof distributionBoardRepository.findById>>>,
|
||||||
|
circuitListId: string
|
||||||
|
): CircuitTreeResponse & { warning: string } {
|
||||||
|
return {
|
||||||
circuitListId,
|
circuitListId,
|
||||||
|
distributionBoardId: board.id,
|
||||||
|
distributionBoardName: board.name,
|
||||||
|
distributionBoardNumber: board.boardNumber ?? undefined,
|
||||||
|
distributionBoardSupplyType: board.supplyType ?? undefined,
|
||||||
currentRevision: project.currentRevision,
|
currentRevision: project.currentRevision,
|
||||||
singlePhaseVoltageV: project.singlePhaseVoltageV,
|
singlePhaseVoltageV: project.singlePhaseVoltageV,
|
||||||
threePhaseVoltageV: project.threePhaseVoltageV,
|
threePhaseVoltageV: project.threePhaseVoltageV,
|
||||||
distributionBoardSimultaneityFactor:
|
distributionBoardSimultaneityFactor: board.simultaneityFactor,
|
||||||
distributionBoard.simultaneityFactor,
|
|
||||||
distributionBoardTotalPower: 0,
|
distributionBoardTotalPower: 0,
|
||||||
distributionBoardTotalPowerWithSimultaneityFactor: 0,
|
distributionBoardTotalPowerWithSimultaneityFactor: 0,
|
||||||
headerComponents: [],
|
headerComponents: [],
|
||||||
|
|
@ -226,10 +336,7 @@ export async function getCircuitTree(req: Request, res: Response) {
|
||||||
footerComponents: [],
|
footerComponents: [],
|
||||||
warning:
|
warning:
|
||||||
"Circuit-first tables are not available yet. Run database migrations (including 0008_circuit_first_model).",
|
"Circuit-first tables are not available yet. Run database migrations (including 0008_circuit_first_model).",
|
||||||
});
|
};
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function toProtectionDeviceDto(value: {
|
function toProtectionDeviceDto(value: {
|
||||||
|
|
@ -243,11 +350,20 @@ function toProtectionDeviceDto(value: {
|
||||||
return {
|
return {
|
||||||
type: value.type,
|
type: value.type,
|
||||||
ratedCurrentA: value.ratedCurrentA,
|
ratedCurrentA: value.ratedCurrentA,
|
||||||
fuseUtilizationCategory:
|
fuseUtilizationCategory: value.fuseUtilizationCategory ?? undefined,
|
||||||
value.fuseUtilizationCategory ?? undefined,
|
|
||||||
tripCharacteristic: value.tripCharacteristic ?? undefined,
|
tripCharacteristic: value.tripCharacteristic ?? undefined,
|
||||||
rcdType: value.rcdType ?? undefined,
|
rcdType: value.rcdType ?? undefined,
|
||||||
ratedResidualCurrentMa:
|
ratedResidualCurrentMa: value.ratedResidualCurrentMa ?? undefined,
|
||||||
value.ratedResidualCurrentMa ?? undefined,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function groupBy<T>(values: T[], keyOf: (value: T) => string) {
|
||||||
|
const groups = new Map<string, T[]>();
|
||||||
|
for (const value of values) {
|
||||||
|
const key = keyOf(value);
|
||||||
|
const group = groups.get(key) ?? [];
|
||||||
|
group.push(value);
|
||||||
|
groups.set(key, group);
|
||||||
|
}
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,10 @@ import {
|
||||||
listEstimateBalances,
|
listEstimateBalances,
|
||||||
updateEstimateBalance,
|
updateEstimateBalance,
|
||||||
} from "../controllers/estimate-balance.controller.js";
|
} from "../controllers/estimate-balance.controller.js";
|
||||||
import { getCircuitTree } from "../controllers/circuit-tree.controller.js";
|
import {
|
||||||
|
getCircuitTree,
|
||||||
|
getProjectCircuitTrees,
|
||||||
|
} from "../controllers/circuit-tree.controller.js";
|
||||||
import { getNextCircuitIdentifier } from "../controllers/circuit.controller.js";
|
import { getNextCircuitIdentifier } from "../controllers/circuit.controller.js";
|
||||||
import {
|
import {
|
||||||
getProjectHistory,
|
getProjectHistory,
|
||||||
|
|
@ -109,6 +112,7 @@ projectRouter.delete(
|
||||||
deleteDistributionBoard
|
deleteDistributionBoard
|
||||||
);
|
);
|
||||||
projectRouter.get("/:projectId/circuit-lists", listCircuitListsByProject);
|
projectRouter.get("/:projectId/circuit-lists", listCircuitListsByProject);
|
||||||
|
projectRouter.get("/:projectId/circuit-trees", getProjectCircuitTrees);
|
||||||
projectRouter.get("/:projectId/circuit-lists/:circuitListId/tree", getCircuitTree);
|
projectRouter.get("/:projectId/circuit-lists/:circuitListId/tree", getCircuitTree);
|
||||||
projectRouter.get(
|
projectRouter.get(
|
||||||
"/:projectId/circuit-sections/:sectionId/next-identifier",
|
"/:projectId/circuit-sections/:sectionId/next-identifier",
|
||||||
|
|
|
||||||
|
|
@ -38,13 +38,15 @@ describe("circuit tree controller", () => {
|
||||||
it("returns header, grouped and footer components with both protection layers", async () => {
|
it("returns header, grouped and footer components with both protection layers", async () => {
|
||||||
const originals = {
|
const originals = {
|
||||||
findList: circuitListRepository.findById,
|
findList: circuitListRepository.findById,
|
||||||
|
listLists: circuitListRepository.listByProject,
|
||||||
findProject: projectRepository.findById,
|
findProject: projectRepository.findById,
|
||||||
findBoard: distributionBoardRepository.findById,
|
findBoard: distributionBoardRepository.findById,
|
||||||
listSections: circuitSectionRepository.listByCircuitList,
|
listBoards: distributionBoardRepository.listByProject,
|
||||||
listCircuits: circuitRepository.listByCircuitList,
|
listSections: circuitSectionRepository.listByCircuitLists,
|
||||||
|
listCircuits: circuitRepository.listByCircuitLists,
|
||||||
listRows: circuitDeviceRowRepository.listByCircuitList,
|
listRows: circuitDeviceRowRepository.listByCircuitList,
|
||||||
listComponents:
|
listComponents:
|
||||||
distributionBoardComponentRepository.listByCircuitList,
|
distributionBoardComponentRepository.listByCircuitLists,
|
||||||
listComponentProtection:
|
listComponentProtection:
|
||||||
distributionBoardComponentRepository.listProtectionByComponentIds,
|
distributionBoardComponentRepository.listProtectionByComponentIds,
|
||||||
listCircuitProtection:
|
listCircuitProtection:
|
||||||
|
|
@ -57,6 +59,13 @@ describe("circuit tree controller", () => {
|
||||||
distributionBoardId: "board-1",
|
distributionBoardId: "board-1",
|
||||||
name: "UV-01",
|
name: "UV-01",
|
||||||
}) as never;
|
}) as never;
|
||||||
|
circuitListRepository.listByProject = async () =>
|
||||||
|
[{
|
||||||
|
id: "list-1",
|
||||||
|
projectId: "project-1",
|
||||||
|
distributionBoardId: "board-1",
|
||||||
|
name: "UV-01",
|
||||||
|
}] as never;
|
||||||
projectRepository.findById = async () =>
|
projectRepository.findById = async () =>
|
||||||
({
|
({
|
||||||
id: "project-1",
|
id: "project-1",
|
||||||
|
|
@ -66,8 +75,19 @@ describe("circuit tree controller", () => {
|
||||||
threePhaseVoltageV: 400,
|
threePhaseVoltageV: 400,
|
||||||
}) as never;
|
}) as never;
|
||||||
distributionBoardRepository.findById = async () =>
|
distributionBoardRepository.findById = async () =>
|
||||||
({ id: "board-1", simultaneityFactor: 0.8 }) as never;
|
({ id: "board-1", name: "UV-01", simultaneityFactor: 0.8 }) as never;
|
||||||
circuitSectionRepository.listByCircuitList = async () =>
|
distributionBoardRepository.listByProject = async () =>
|
||||||
|
[{
|
||||||
|
id: "board-1",
|
||||||
|
projectId: "project-1",
|
||||||
|
name: "UV-01",
|
||||||
|
boardNumber: "UV-01",
|
||||||
|
supplyType: "AV",
|
||||||
|
simultaneityFactor: 0.8,
|
||||||
|
feedingDistributionBoardId: null,
|
||||||
|
feedingCircuitId: null,
|
||||||
|
}] as never;
|
||||||
|
circuitSectionRepository.listByCircuitLists = async () =>
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
id: "group-1",
|
id: "group-1",
|
||||||
|
|
@ -80,7 +100,7 @@ describe("circuit tree controller", () => {
|
||||||
groupNumber: 1,
|
groupNumber: 1,
|
||||||
},
|
},
|
||||||
] as never;
|
] as never;
|
||||||
circuitRepository.listByCircuitList = async () =>
|
circuitRepository.listByCircuitLists = async () =>
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
id: "circuit-1",
|
id: "circuit-1",
|
||||||
|
|
@ -90,10 +110,19 @@ describe("circuit tree controller", () => {
|
||||||
displayName: "Licht",
|
displayName: "Licht",
|
||||||
sortOrder: 10,
|
sortOrder: 10,
|
||||||
isReserve: 0,
|
isReserve: 0,
|
||||||
|
cableType: null,
|
||||||
|
cableCrossSection: null,
|
||||||
|
cableLength: null,
|
||||||
|
rcdAssignment: null,
|
||||||
|
terminalDesignation: null,
|
||||||
|
voltage: 230,
|
||||||
|
controlRequirement: null,
|
||||||
|
status: null,
|
||||||
|
remark: null,
|
||||||
},
|
},
|
||||||
] as never;
|
] as never;
|
||||||
circuitDeviceRowRepository.listByCircuitList = async () => [];
|
circuitDeviceRowRepository.listByCircuitList = async () => [];
|
||||||
distributionBoardComponentRepository.listByCircuitList =
|
distributionBoardComponentRepository.listByCircuitLists =
|
||||||
async () =>
|
async () =>
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
|
|
@ -170,13 +199,15 @@ describe("circuit tree controller", () => {
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
circuitListRepository.findById = originals.findList;
|
circuitListRepository.findById = originals.findList;
|
||||||
|
circuitListRepository.listByProject = originals.listLists;
|
||||||
projectRepository.findById = originals.findProject;
|
projectRepository.findById = originals.findProject;
|
||||||
distributionBoardRepository.findById = originals.findBoard;
|
distributionBoardRepository.findById = originals.findBoard;
|
||||||
circuitSectionRepository.listByCircuitList =
|
distributionBoardRepository.listByProject = originals.listBoards;
|
||||||
|
circuitSectionRepository.listByCircuitLists =
|
||||||
originals.listSections;
|
originals.listSections;
|
||||||
circuitRepository.listByCircuitList = originals.listCircuits;
|
circuitRepository.listByCircuitLists = originals.listCircuits;
|
||||||
circuitDeviceRowRepository.listByCircuitList = originals.listRows;
|
circuitDeviceRowRepository.listByCircuitList = originals.listRows;
|
||||||
distributionBoardComponentRepository.listByCircuitList =
|
distributionBoardComponentRepository.listByCircuitLists =
|
||||||
originals.listComponents;
|
originals.listComponents;
|
||||||
distributionBoardComponentRepository.listProtectionByComponentIds =
|
distributionBoardComponentRepository.listProtectionByComponentIds =
|
||||||
originals.listComponentProtection;
|
originals.listComponentProtection;
|
||||||
|
|
|
||||||
89
tests/distribution-board-load-calculation.test.ts
Normal file
89
tests/distribution-board-load-calculation.test.ts
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { describe, it } from "node:test";
|
||||||
|
import { calculateDistributionBoardLoads } from "../src/domain/calculations/distribution-board-load-calculation.js";
|
||||||
|
|
||||||
|
const board = (
|
||||||
|
id: string,
|
||||||
|
simultaneityFactor: number,
|
||||||
|
feedingDistributionBoardId: string | null = null,
|
||||||
|
feedingCircuitId: string | null = null
|
||||||
|
) => ({
|
||||||
|
id,
|
||||||
|
name: id,
|
||||||
|
boardNumber: id.toUpperCase(),
|
||||||
|
simultaneityFactor,
|
||||||
|
feedingDistributionBoardId,
|
||||||
|
feedingCircuitId,
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("distribution-board load calculation", () => {
|
||||||
|
it("rolls recursive demand into the selected feeder circuit exactly once", () => {
|
||||||
|
const result = calculateDistributionBoardLoads({
|
||||||
|
boards: [
|
||||||
|
board("nshv", 0.8),
|
||||||
|
board("uv1", 0.5, "nshv", "nshv-out"),
|
||||||
|
board("uv2", 0.25, "uv1", "uv1-out"),
|
||||||
|
],
|
||||||
|
circuits: [
|
||||||
|
{ id: "nshv-out", distributionBoardId: "nshv", rows: [{ activePowerKw: 2, cosPhi: 1 }] },
|
||||||
|
{ id: "uv1-out", distributionBoardId: "uv1", rows: [{ activePowerKw: 4, cosPhi: 0.8 }] },
|
||||||
|
{ id: "uv2-load", distributionBoardId: "uv2", rows: [{ activePowerKw: 8, cosPhi: 0.8 }] },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(result.boards.get("uv2"), {
|
||||||
|
distributionBoardId: "uv2",
|
||||||
|
activePowerBeforeSimultaneityKw: 8,
|
||||||
|
apparentPowerBeforeSimultaneityKva: 10,
|
||||||
|
demandActivePowerKw: 2,
|
||||||
|
demandApparentPowerKva: 2.5,
|
||||||
|
cosPhi: 0.8,
|
||||||
|
});
|
||||||
|
assert.equal(result.circuits.get("uv1-out")?.activePowerKw, 6);
|
||||||
|
assert.equal(result.boards.get("uv1")?.demandActivePowerKw, 3);
|
||||||
|
assert.equal(result.circuits.get("nshv-out")?.activePowerKw, 5);
|
||||||
|
assert.equal(result.boards.get("nshv")?.demandActivePowerKw, 4);
|
||||||
|
assert.equal(result.circuits.get("nshv-out")?.downstreamBoards.length, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("combines apparent power for multiple child boards on one circuit", () => {
|
||||||
|
const result = calculateDistributionBoardLoads({
|
||||||
|
boards: [board("main", 1), board("a", 1, "main", "out"), board("b", 1, "main", "out")],
|
||||||
|
circuits: [
|
||||||
|
{ id: "out", distributionBoardId: "main", rows: [] },
|
||||||
|
{ id: "a-load", distributionBoardId: "a", rows: [{ activePowerKw: 2, cosPhi: 0.5 }] },
|
||||||
|
{ id: "b-load", distributionBoardId: "b", rows: [{ activePowerKw: 2, cosPhi: 1 }] },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const feeder = result.circuits.get("out")!;
|
||||||
|
assert.equal(feeder.activePowerKw, 4);
|
||||||
|
assert.equal(feeder.apparentPowerKva, 6);
|
||||||
|
assert.equal(feeder.cosPhi, 2 / 3);
|
||||||
|
assert.deepEqual(feeder.downstreamBoards.map((entry) => entry.distributionBoardId), ["a", "b"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps a board-only hierarchy relation out of load rollup", () => {
|
||||||
|
const result = calculateDistributionBoardLoads({
|
||||||
|
boards: [board("main", 1), board("child", 1, "main", null)],
|
||||||
|
circuits: [
|
||||||
|
{ id: "main-load", distributionBoardId: "main", rows: [{ activePowerKw: 1, cosPhi: 1 }] },
|
||||||
|
{ id: "child-load", distributionBoardId: "child", rows: [{ activePowerKw: 5, cosPhi: 1 }] },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
assert.equal(result.boards.get("main")?.demandActivePowerKw, 1);
|
||||||
|
assert.equal(result.boards.get("child")?.demandActivePowerKw, 5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects cycles defensively", () => {
|
||||||
|
assert.throws(
|
||||||
|
() => calculateDistributionBoardLoads({
|
||||||
|
boards: [board("a", 1, "b", "b-out"), board("b", 1, "a", "a-out")],
|
||||||
|
circuits: [
|
||||||
|
{ id: "a-out", distributionBoardId: "a", rows: [] },
|
||||||
|
{ id: "b-out", distributionBoardId: "b", rows: [] },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
/cycle/
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue