Add protection domain foundation

This commit is contained in:
2026-07-30 19:03:36 +02:00
parent a72d2c76e0
commit a88c4b2086
13 changed files with 909 additions and 2 deletions
@@ -0,0 +1,40 @@
import type {
BreakerTripCharacteristic,
FuseProtectionDeviceType,
FuseUtilizationCategory,
RatedResidualCurrentMa,
RcdType,
} from "../../shared/constants/protection-device.js";
export interface FuseProtectionDeviceConfiguration {
type: FuseProtectionDeviceType;
ratedCurrentA: number;
fuseUtilizationCategory: FuseUtilizationCategory;
}
export interface BreakerProtectionDeviceConfiguration {
type: "LS" | "AFDD";
ratedCurrentA: number;
tripCharacteristic: BreakerTripCharacteristic;
}
export interface ResidualCurrentDeviceConfiguration {
type: "FI";
ratedCurrentA: number;
rcdType: RcdType;
ratedResidualCurrentMa: RatedResidualCurrentMa;
}
export interface CombinedResidualCurrentBreakerConfiguration {
type: "FI_LS";
ratedCurrentA: number;
tripCharacteristic: BreakerTripCharacteristic;
rcdType: RcdType;
ratedResidualCurrentMa: RatedResidualCurrentMa;
}
export type ProtectionDeviceConfiguration =
| FuseProtectionDeviceConfiguration
| BreakerProtectionDeviceConfiguration
| ResidualCurrentDeviceConfiguration
| CombinedResidualCurrentBreakerConfiguration;
@@ -0,0 +1,165 @@
import {
circuitGroupCategoryNumbers,
type CircuitGroupCategory,
} from "../../shared/constants/circuit-group.js";
export type GroupedEquipmentIdentifierKind =
| "group_upstream_protection"
| "group_rcd"
| "circuit";
export interface ParsedGroupedEquipmentIdentifier {
category: CircuitGroupCategory;
groupNumber: number;
kind: GroupedEquipmentIdentifierKind;
circuitNumber: number | null;
}
const categoryByNumber: Record<string, CircuitGroupCategory> = {
"1": "lighting",
"2": "single_phase",
"3": "three_phase",
};
function requirePositiveInteger(value: number, fieldName: string): void {
if (!Number.isInteger(value) || value < 1) {
throw new RangeError(`${fieldName} must be a positive integer`);
}
}
function getGroupStem(
category: CircuitGroupCategory,
functionLetter: "F" | "Q",
groupNumber: number
): string {
requirePositiveInteger(groupNumber, "groupNumber");
return `-${circuitGroupCategoryNumbers[category]}${functionLetter}${groupNumber}`;
}
export function formatGroupUpstreamProtectionIdentifier(
category: CircuitGroupCategory,
groupNumber: number
): string {
return `${getGroupStem(category, "F", groupNumber)}.0`;
}
export function formatGroupRcdIdentifier(
category: CircuitGroupCategory,
groupNumber: number
): string {
return `${getGroupStem(category, "Q", groupNumber)}.0`;
}
export function formatGroupedCircuitIdentifier(
category: CircuitGroupCategory,
groupNumber: number,
circuitNumber: number
): string {
requirePositiveInteger(circuitNumber, "circuitNumber");
return `${getGroupStem(category, "F", groupNumber)}.${circuitNumber}`;
}
export function parseGroupedEquipmentIdentifier(
equipmentIdentifier: string
): ParsedGroupedEquipmentIdentifier | null {
const match = /^-(1|2|3)(F|Q)([1-9]\d*)\.(0|[1-9]\d*)$/.exec(
equipmentIdentifier
);
if (!match) {
return null;
}
const [, categoryNumber, functionLetter, groupNumberValue, suffixValue] =
match;
const category = categoryByNumber[categoryNumber];
const groupNumber = Number(groupNumberValue);
const suffix = Number(suffixValue);
if (functionLetter === "Q") {
return suffix === 0
? {
category,
groupNumber,
kind: "group_rcd",
circuitNumber: null,
}
: null;
}
if (suffix === 0) {
return {
category,
groupNumber,
kind: "group_upstream_protection",
circuitNumber: null,
};
}
return {
category,
groupNumber,
kind: "circuit",
circuitNumber: suffix,
};
}
export function getNextGroupedCircuitNumber(
category: CircuitGroupCategory,
groupNumber: number,
equipmentIdentifiers: Iterable<string>
): number {
requirePositiveInteger(groupNumber, "groupNumber");
let highestCircuitNumber = 0;
for (const equipmentIdentifier of equipmentIdentifiers) {
const parsed = parseGroupedEquipmentIdentifier(equipmentIdentifier);
if (
parsed?.kind === "circuit" &&
parsed.category === category &&
parsed.groupNumber === groupNumber &&
parsed.circuitNumber !== null
) {
highestCircuitNumber = Math.max(
highestCircuitNumber,
parsed.circuitNumber
);
}
}
return highestCircuitNumber + 1;
}
export function getNextGroupedCircuitIdentifier(
category: CircuitGroupCategory,
groupNumber: number,
equipmentIdentifiers: Iterable<string>
): string {
return formatGroupedCircuitIdentifier(
category,
groupNumber,
getNextGroupedCircuitNumber(
category,
groupNumber,
equipmentIdentifiers
)
);
}
export function getNextCircuitGroupNumber(
category: CircuitGroupCategory,
groups: Iterable<{
category: CircuitGroupCategory;
groupNumber: number;
}>
): number {
let highestGroupNumber = 0;
for (const group of groups) {
if (group.category === category) {
requirePositiveInteger(group.groupNumber, "groupNumber");
highestGroupNumber = Math.max(highestGroupNumber, group.groupNumber);
}
}
return highestGroupNumber + 1;
}
@@ -0,0 +1,41 @@
import type { ProtectionDeviceConfiguration } from "../models/protection-device.model.js";
import type { CircuitGroupCategory } from "../../shared/constants/circuit-group.js";
const defaultCircuitProtectionByCategory = {
lighting: {
type: "LS",
ratedCurrentA: 10,
tripCharacteristic: "B",
},
single_phase: {
type: "FI_LS",
ratedCurrentA: 16,
tripCharacteristic: "B",
rcdType: "A",
ratedResidualCurrentMa: 30,
},
three_phase: {
type: "FI_LS",
ratedCurrentA: 16,
tripCharacteristic: "B",
rcdType: "A",
ratedResidualCurrentMa: 30,
},
} as const satisfies Record<CircuitGroupCategory, ProtectionDeviceConfiguration>;
const defaultGroupRcd = {
type: "FI",
ratedCurrentA: 40,
rcdType: "A",
ratedResidualCurrentMa: 30,
} as const satisfies ProtectionDeviceConfiguration;
export function createDefaultCircuitProtection(
category: CircuitGroupCategory
): ProtectionDeviceConfiguration {
return { ...defaultCircuitProtectionByCategory[category] };
}
export function createDefaultGroupRcd(): ProtectionDeviceConfiguration {
return { ...defaultGroupRcd };
}