// Pure cable sizing domain logic. No imports from db/, server/ or frontend/; // the module is an optional calculator attached to the circuit list and never // writes project data itself (see docs/cable-sizing-module.md). // // Reference data: IEC 60364-5-52:2009, Annex B (tables B.52.2 to B.52.5, // B.52.10 to B.52.19). Every value was cross-checked against publicly // available reproductions of these tables; where sources disagreed the lower, // i.e. safer, value is used. The table-by-table verification record is in // docs/cable-sizing-module.md. National tables (DIN VDE 0298-4) may differ in // individual values and must be checked by the planner against the norm text. // -- Reference installation methods (IEC 60364-5-52, Table B.52.1) ---------- export const LAYING_METHODS = ["A1", "A2", "B1", "B2", "C", "D1", "D2", "E"] as const; export type LayingMethod = (typeof LAYING_METHODS)[number]; export const LAYING_METHOD_GROUP: Record = { A1: "air", A2: "air", B1: "air", B2: "air", C: "air", E: "air", D1: "ground", D2: "ground", }; export const LAYING_METHOD_LABELS: Record = { A1: "A1 – Einzeladern im Rohr in wärmegedämmter Wand", A2: "A2 – Mehradrige Leitung im Rohr in wärmegedämmter Wand", B1: "B1 – Einzeladern im Rohr auf oder in der Wand", B2: "B2 – Mehradrige Leitung im Rohr auf oder in der Wand", C: "C – Mehradrige Leitung direkt auf oder in der Wand", D1: "D1 – Leitung im Rohr oder Kabelschutzrohr im Erdreich", D2: "D2 – Kabel direkt im Erdreich", E: "E – Mehradrige Leitung frei in Luft", }; // -- Insulation / conductor material ---------------------------------------- export const INSULATION_MATERIALS = ["pvc", "xlpe"] as const; export type InsulationMaterial = (typeof INSULATION_MATERIALS)[number]; export const INSULATION_MATERIAL_LABELS: Record = { pvc: "PVC (70 °C), z. B. NYM, NYY", xlpe: "VPE/XLPE (90 °C), z. B. N2XH, NHXH", }; export const CONDUCTOR_MATERIALS = ["copper", "aluminum"] as const; export type ConductorMaterial = (typeof CONDUCTOR_MATERIALS)[number]; // -- Harmonic neutral loading (IEC 60364-5-52, Annex E, three-phase only) --- export const HARMONIC_NEUTRAL_LOAD_OPTIONS = [ "none", "15to33Percent", "over33Percent", ] as const; export type HarmonicNeutralLoad = (typeof HARMONIC_NEUTRAL_LOAD_OPTIONS)[number]; export const HARMONIC_NEUTRAL_LOAD_LABELS: Record = { none: "Keine nennenswerte 3. Harmonische (< 15 %) – Standardfall", "15to33Percent": "3. Harmonische 15–33 % – Reduktionsfaktor 0,86", over33Percent: "3. Harmonische > 33 % – Neutralleiter wie Außenleiter dimensionieren", }; export const HARMONIC_REDUCTION_FACTOR = 0.86; // -- Current-carrying capacity tables ---------------------------------------- export const CROSS_SECTIONS_MM2 = [ 1.5, 2.5, 4, 6, 10, 16, 25, 35, 50, 70, 95, 120, 150, 185, 240, ] as const; type CoreCount = 2 | 3; type CapacityRow = readonly (number | null)[]; type CapacityTable = Record>; const n = null; /** Iz in A at 30 °C air / 20 °C ground, soil resistivity 2.5 K·m/W. */ export const CURRENT_CAPACITY_A: Record< InsulationMaterial, Record > = { pvc: { copper: { // B.52.2 / B.52.4; E: B.52.10 multi-core. A1 2L 240 mm²: sources 320/321 → 320. // E 3L 70/120 mm²: sources 196/198 and 276/278 → 196 and 276. A1: { 2: [14.5, 19.5, 26, 34, 46, 61, 80, 99, 119, 151, 182, 210, 240, 273, 320], 3: [13.5, 18, 24, 31, 42, 56, 73, 89, 108, 136, 164, 188, 216, 245, 286], }, A2: { 2: [14, 18.5, 25, 32, 43, 57, 75, 92, 110, 139, 167, 192, 219, 248, 291], 3: [13, 17.5, 23, 29, 39, 52, 68, 83, 99, 125, 150, 172, 196, 223, 261], }, B1: { 2: [17.5, 24, 32, 41, 57, 76, 101, 125, 151, 192, 232, 269, 300, 341, 400], 3: [15.5, 21, 28, 36, 50, 68, 89, 110, 134, 171, 207, 239, 262, 296, 346], }, B2: { 2: [16.5, 23, 30, 38, 52, 69, 90, 111, 133, 168, 201, 232, 258, 294, 344], 3: [15, 20, 27, 34, 46, 62, 80, 99, 118, 149, 179, 206, 225, 255, 297], }, C: { 2: [19.5, 27, 36, 46, 63, 85, 112, 138, 168, 213, 258, 299, 344, 392, 461], 3: [17.5, 24, 32, 41, 57, 76, 96, 119, 144, 184, 223, 259, 299, 341, 403], }, D1: { 2: [22, 29, 37, 46, 60, 78, 99, 119, 140, 173, 204, 231, 261, 292, 336], 3: [18, 24, 30, 38, 50, 64, 82, 98, 116, 143, 169, 192, 217, 243, 280], }, D2: { 2: [22, 28, 38, 48, 64, 83, 110, 132, 156, 192, 230, 261, 293, 331, 382], 3: [19, 24, 33, 41, 54, 70, 92, 110, 130, 162, 193, 220, 246, 278, 320], }, E: { 2: [22, 30, 40, 51, 70, 94, 119, 148, 180, 232, 282, 328, 379, 434, 514], 3: [18.5, 25, 34, 43, 60, 80, 101, 126, 153, 196, 238, 276, 319, 364, 430], }, }, aluminum: { // B.52.2 / B.52.4 aluminium; E: B.52.11. No values below 2.5 mm² // (D2 below 16 mm²). A1: { 2: [n, 15, 20, 26, 36, 48, 63, 77, 93, 118, 142, 164, 189, 215, 252], 3: [n, 14, 18.5, 24, 32, 43, 57, 70, 84, 107, 129, 149, 170, 194, 227], }, A2: { 2: [n, 14.5, 19.5, 25, 33, 44, 58, 71, 86, 108, 130, 150, 172, 195, 229], 3: [n, 13.5, 17.5, 23, 31, 41, 53, 65, 78, 98, 118, 135, 155, 176, 207], }, B1: { 2: [n, 18.5, 25, 32, 44, 60, 79, 97, 118, 150, 181, 210, 234, 266, 312], 3: [n, 16.5, 22, 28, 39, 53, 70, 86, 104, 133, 161, 186, 204, 230, 269], }, B2: { 2: [n, 17.5, 24, 30, 41, 54, 71, 86, 104, 131, 157, 181, 201, 230, 269], 3: [n, 15.5, 21, 27, 36, 48, 62, 77, 92, 116, 139, 160, 176, 199, 232], }, C: { 2: [n, 21, 28, 36, 49, 66, 83, 103, 125, 160, 195, 226, 261, 298, 352], 3: [n, 18.5, 25, 32, 44, 59, 73, 90, 110, 140, 170, 197, 227, 259, 305], }, D1: { 2: [n, 22, 29, 36, 47, 61, 77, 93, 109, 135, 159, 180, 204, 228, 262], 3: [n, 18.5, 24, 30, 39, 50, 64, 77, 91, 112, 132, 150, 169, 190, 218], }, D2: { 2: [n, n, n, n, n, 63, 82, 98, 117, 145, 173, 200, 224, 255, 298], 3: [n, n, n, n, n, 53, 69, 83, 99, 122, 148, 169, 189, 214, 250], }, E: { 2: [n, 23, 31, 39, 54, 73, 89, 111, 135, 173, 210, 244, 282, 322, 380], 3: [n, 19.5, 26, 33, 46, 61, 78, 96, 117, 150, 183, 212, 245, 280, 330], }, }, }, xlpe: { copper: { // B.52.3 / B.52.5; E: B.52.12 multi-core. A1: { 2: [19, 26, 35, 45, 61, 81, 106, 131, 158, 200, 241, 278, 318, 362, 424], 3: [17, 23, 31, 40, 54, 73, 95, 117, 141, 179, 216, 249, 285, 324, 380], }, A2: { 2: [18.5, 25, 33, 42, 57, 76, 99, 121, 145, 183, 220, 253, 290, 329, 386], 3: [16.5, 22, 30, 38, 51, 68, 89, 109, 130, 164, 197, 227, 259, 295, 346], }, B1: { 2: [23, 31, 42, 54, 75, 100, 133, 164, 198, 253, 306, 354, 393, 449, 528], 3: [20, 28, 37, 48, 66, 88, 117, 144, 175, 222, 269, 312, 342, 384, 450], }, B2: { 2: [22, 30, 40, 51, 69, 91, 119, 146, 175, 221, 265, 305, 334, 384, 459], 3: [19.5, 26, 35, 44, 60, 80, 105, 128, 154, 194, 233, 268, 300, 340, 398], }, C: { 2: [24, 33, 45, 58, 80, 107, 138, 171, 209, 269, 328, 382, 441, 506, 599], 3: [22, 30, 40, 52, 71, 96, 119, 147, 179, 229, 278, 322, 371, 424, 500], }, D1: { 2: [25, 33, 43, 53, 71, 91, 116, 139, 164, 203, 239, 271, 306, 343, 395], 3: [21, 28, 36, 44, 58, 75, 96, 115, 135, 167, 197, 223, 251, 281, 324], }, D2: { 2: [27, 35, 46, 58, 77, 100, 129, 155, 183, 225, 270, 306, 343, 387, 448], 3: [23, 30, 39, 49, 65, 84, 107, 129, 153, 188, 226, 257, 287, 324, 375], }, E: { 2: [26, 36, 49, 63, 86, 115, 149, 185, 225, 289, 352, 410, 473, 542, 641], 3: [23, 32, 42, 54, 75, 100, 127, 158, 192, 246, 298, 346, 399, 456, 538], }, }, aluminum: { // B.52.3 / B.52.5 aluminium; E: B.52.13. A1: { 2: [n, 20, 27, 35, 48, 64, 84, 103, 125, 158, 191, 220, 253, 288, 338], 3: [n, 19, 25, 32, 44, 58, 76, 94, 113, 142, 171, 197, 226, 256, 300], }, A2: { 2: [n, 19.5, 26, 33, 45, 60, 78, 96, 115, 145, 175, 201, 230, 262, 307], 3: [n, 18, 24, 31, 41, 55, 71, 87, 104, 131, 157, 180, 206, 233, 273], }, B1: { 2: [n, 25, 33, 43, 59, 79, 105, 130, 157, 200, 242, 281, 307, 351, 412], 3: [n, 22, 29, 38, 52, 71, 93, 116, 140, 179, 217, 251, 267, 300, 351], }, B2: { 2: [n, 23, 31, 40, 54, 72, 94, 115, 138, 175, 210, 242, 261, 300, 358], 3: [n, 21, 28, 35, 48, 64, 84, 103, 124, 156, 188, 216, 240, 272, 318], }, C: { 2: [n, 26, 35, 45, 62, 84, 101, 126, 154, 198, 241, 280, 324, 371, 439], 3: [n, 24, 32, 41, 57, 76, 90, 112, 136, 174, 211, 245, 283, 323, 382], }, D1: { 2: [n, 26, 33, 42, 55, 71, 90, 108, 128, 158, 186, 211, 238, 267, 307], 3: [n, 22, 28, 35, 46, 59, 75, 90, 106, 130, 154, 174, 197, 220, 253], }, D2: { 2: [n, n, n, n, n, 76, 98, 117, 139, 170, 204, 233, 261, 296, 343], 3: [n, n, n, n, n, 64, 82, 98, 117, 144, 172, 197, 220, 250, 290], }, E: { 2: [n, 28, 38, 49, 67, 91, 108, 135, 164, 211, 257, 300, 346, 397, 470], 3: [n, 24, 32, 42, 58, 77, 97, 120, 146, 187, 227, 263, 304, 347, 409], }, }, }, }; // -- Correction factors ------------------------------------------------------ /** B.52.14 (air, reference 30 °C) and B.52.15 (ground, reference 20 °C). */ export const TEMPERATURE_FACTORS: Record< "air" | "ground", Record> > = { air: { pvc: { 10: 1.22, 15: 1.17, 20: 1.12, 25: 1.06, 30: 1.0, 35: 0.94, 40: 0.87, 45: 0.79, 50: 0.71, 55: 0.61, 60: 0.5 }, xlpe: { 10: 1.15, 15: 1.12, 20: 1.08, 25: 1.04, 30: 1.0, 35: 0.96, 40: 0.91, 45: 0.87, 50: 0.82, 55: 0.76, 60: 0.71, 65: 0.65, 70: 0.58, 75: 0.5, 80: 0.41, }, }, ground: { pvc: { 10: 1.1, 15: 1.05, 20: 1.0, 25: 0.95, 30: 0.89, 35: 0.84, 40: 0.77, 45: 0.71, 50: 0.63, 55: 0.55, 60: 0.45 }, xlpe: { 10: 1.07, 15: 1.04, 20: 1.0, 25: 0.96, 30: 0.93, 35: 0.89, 40: 0.85, 45: 0.8, 50: 0.76, 55: 0.71, 60: 0.65, 65: 0.6, 70: 0.53, 75: 0.46, 80: 0.38, }, }, }; export const REFERENCE_TEMPERATURE_C: Record<"air" | "ground", number> = { air: 30, ground: 20, }; /** * Grouping reduction factors for touching circuits/cables: * air (A1–C, E): B.52.17 item 1 "bunched in air, on a surface, embedded or * enclosed" – the most restrictive arrangement, also used for E (single * layers on trays would allow higher factors); * D1: B.52.19 A (multi-core cables in touching ducts); * D2: B.52.18 (cables direct in ground, touching). */ export const GROUPING_FACTORS: Record<"air" | "D1" | "D2", Record> = { air: { 1: 1.0, 2: 0.8, 3: 0.7, 4: 0.65, 5: 0.6, 6: 0.57, 7: 0.54, 8: 0.52, 9: 0.5, 12: 0.45, 16: 0.41, 20: 0.38 }, D1: { 1: 1.0, 2: 0.85, 3: 0.75, 4: 0.7, 5: 0.65, 6: 0.6, 7: 0.57, 8: 0.54, 9: 0.52, 10: 0.49, 11: 0.47, 12: 0.45, 13: 0.44, 14: 0.42, 15: 0.41, 16: 0.39, 17: 0.38, 18: 0.37, 19: 0.35, 20: 0.34, }, D2: { 1: 1.0, 2: 0.75, 3: 0.65, 4: 0.6, 5: 0.55, 6: 0.5, 7: 0.45, 8: 0.43, 9: 0.41, 12: 0.36, 16: 0.32, 20: 0.29 }, }; export const MAX_GROUPING_CIRCUITS = 20; function groupingTableFor(method: LayingMethod) { return method === "D1" ? GROUPING_FACTORS.D1 : method === "D2" ? GROUPING_FACTORS.D2 : GROUPING_FACTORS.air; } /** * Looks up a tabulated factor. Values between tabulated keys use the next * higher key (the lower, safer factor); values above the table are not * supported (null) and values below the smallest key use the smallest key * (whose factor is lower than the true one, i.e. safe). */ function lookupConservative(table: Record, value: number): { key: number; factor: number } | null { const keys = Object.keys(table).map(Number).sort((a, b) => a - b); const key = keys.find((candidate) => candidate >= value); if (key === undefined) { return null; } return { key, factor: table[key] }; } /** Tabulated temperatures offered by the UI for a laying method and insulation. */ export function temperatureOptions(method: LayingMethod, insulation: InsulationMaterial): number[] { return Object.keys(TEMPERATURE_FACTORS[LAYING_METHOD_GROUP[method]][insulation]) .map(Number) .sort((a, b) => a - b); } // -- Conductor resistivity for the voltage drop ------------------------------- /** Conductivity in m/(Ω·mm²) at the maximum operating temperature (conservative). */ export const KAPPA: Record = { copper: 48, aluminum: 30 }; // -- Practical minimum cross-sections by circuit category -------------------- // A planning convention from docs/spec/06-future-sizing-and-calculations.md // ("Standard Single-Phase Circuits ... cable cross-section: 2.5 mm²"), not a // calculation result. It only ever raises a recommendation. export type CircuitCategory = "lighting" | "single_phase" | "three_phase"; export const PRACTICAL_MINIMUM_CROSS_SECTION_MM2: Partial> = { single_phase: 2.5, }; // -- Protective device -------------------------------------------------------- export const PROTECTION_KINDS = ["breaker", "fuse"] as const; export type ProtectionKind = (typeof PROTECTION_KINDS)[number]; export interface ExistingProtection { ratedCurrentA: number; /** breaker: circuit-breaker (I2 ≤ 1.45 In); fuse: gG fuse (I2 up to 2.1 In). */ kind: ProtectionKind; } /** * Conventional tripping/fusing current I2 as multiple of In. * Circuit-breakers (IEC 60898-1): 1.45. gG fuses (IEC 60269-2): * In ≤ 4 A: 2.1, 4 A < In < 16 A: 1.9, In ≥ 16 A: 1.6. */ export function conventionalTrippingFactor(protection: ExistingProtection): number { if (protection.kind === "breaker") { return 1.45; } if (protection.ratedCurrentA <= 4) { return 2.1; } return protection.ratedCurrentA < 16 ? 1.9 : 1.6; } /** * Minimum Iz required by overload protection (IEC 60364-4-43, 433.1): * In ≤ Iz and I2 ≤ 1.45 · Iz. */ export function minimumIzForProtection(protection: ExistingProtection): number { return Math.max( protection.ratedCurrentA, (conventionalTrippingFactor(protection) * protection.ratedCurrentA) / 1.45 ); } // -- Input / result ----------------------------------------------------------- export interface CableSizingInput { phase: 1 | 3; mode: "power" | "current"; /** kW, only used when mode === "power" */ powerKw?: number; /** A, only used when mode === "current" */ currentA?: number; cosPhi: number; /** Line voltage in V (project single-/three-phase voltage). */ voltage: number; /** Single-run cable length in m. */ lengthM: number; layingMethod: LayingMethod; conductorMaterial: ConductorMaterial; insulation: InsulationMaterial; /** Ambient air temperature, or ground temperature for D1/D2. */ ambientTemperatureC: number; /** Number of grouped circuits or multi-core cables, including this one. */ groupingCircuits: number; maxVoltageDropPercent: number; harmonicNeutralLoad: HarmonicNeutralLoad; existingProtection?: ExistingProtection; circuitCategory?: CircuitCategory; } export interface CrossSectionRow { crossSectionMm2: number; /** Tabulated Iz; null if the table has no value for this size. */ ratedCurrentA: number | null; correctedCurrentA: number | null; currentSufficient: boolean | null; voltageDropPercent: number | null; voltageDropSufficient: boolean | null; /** Longest single run meeting maxVoltageDropPercent at this load. */ maxLengthForVoltageDropM: number | null; recommended: boolean; } export interface ProtectionCoordinationResult { ratedCurrentA: number; kind: ProtectionKind; /** I2 / In used for the check. */ conventionalTrippingFactor: number; /** In ≤ Iz and I2 ≤ 1.45 · Iz for the recommended cross-section. */ coordinated: boolean; } export type UnsupportedReason = "temperature" | "grouping"; export interface CableSizingResult { /** false: the inputs are outside the tabulated range – no numeric result. */ supported: boolean; unsupportedReason: UnsupportedReason | null; operatingCurrentA: number; /** * Current the cross-section must carry after correction: * max(Ib, In, I2/1.45) with an existing protective device, otherwise Ib. */ designCurrentA: number; /** Tabulated temperature/grouping actually used (rounded to the safe side). */ temperatureUsedC: number | null; groupingUsed: number | null; temperatureFactor: number | null; groupingFactor: number | null; crossSectionByCapacityMm2: number | null; crossSectionByVoltageDropMm2: number | null; recommendedCrossSectionMm2: number | null; voltageDropAtRecommendedPercent: number | null; combinedDerationFactor: number; harmonicReductionApplied: boolean; practicalMinimumApplied: boolean; rows: CrossSectionRow[]; protectionCoordination: ProtectionCoordinationResult | null; } function operatingCurrentA(input: CableSizingInput): number { if (input.mode === "current") { return input.currentA ?? 0; } const powerW = (input.powerKw ?? 0) * 1000; return input.phase === 1 ? powerW / (input.voltage * input.cosPhi) : powerW / (Math.sqrt(3) * input.voltage * input.cosPhi); } export function calculateCableSizing(input: CableSizingInput): CableSizingResult { const ib = operatingCurrentA(input); const designCurrentA = input.existingProtection ? Math.max(ib, minimumIzForProtection(input.existingProtection)) : ib; const group = LAYING_METHOD_GROUP[input.layingMethod]; const temperature = lookupConservative( TEMPERATURE_FACTORS[group][input.insulation], input.ambientTemperatureC ); const grouping = input.groupingCircuits <= MAX_GROUPING_CIRCUITS ? lookupConservative(groupingTableFor(input.layingMethod), Math.max(1, input.groupingCircuits)) : null; const empty: CableSizingResult = { supported: false, unsupportedReason: !temperature ? "temperature" : "grouping", operatingCurrentA: ib, designCurrentA, temperatureUsedC: temperature?.key ?? null, groupingUsed: grouping?.key ?? null, temperatureFactor: temperature?.factor ?? null, groupingFactor: grouping?.factor ?? null, crossSectionByCapacityMm2: null, crossSectionByVoltageDropMm2: null, recommendedCrossSectionMm2: null, voltageDropAtRecommendedPercent: null, combinedDerationFactor: 1, harmonicReductionApplied: false, practicalMinimumApplied: false, rows: [], protectionCoordination: null, }; if (!temperature || !grouping) { return empty; } const coreCount: CoreCount = input.phase === 1 ? 2 : 3; const harmonicReductionApplied = input.phase === 3 && input.harmonicNeutralLoad === "15to33Percent"; const combinedDerationFactor = temperature.factor * grouping.factor * (harmonicReductionApplied ? HARMONIC_REDUCTION_FACTOR : 1); const voltageDropCoefficient = input.phase === 1 ? 2 : Math.sqrt(3); const kappa = KAPPA[input.conductorMaterial]; const ratedCurrents = CURRENT_CAPACITY_A[input.insulation][input.conductorMaterial][input.layingMethod][coreCount]; const correctedCurrents = ratedCurrents.map((value) => value == null ? null : value * combinedDerationFactor ); const voltageDrops = CROSS_SECTIONS_MM2.map( (crossSection) => ((voltageDropCoefficient * input.lengthM * ib * input.cosPhi) / (kappa * crossSection) / input.voltage) * 100 ); const indexByCapacity = correctedCurrents.findIndex( (value) => value != null && value >= designCurrentA ); const indexByVoltageDrop = voltageDrops.findIndex( (value, index) => ratedCurrents[index] != null && value <= input.maxVoltageDropPercent ); let indexRecommended = indexByCapacity >= 0 && indexByVoltageDrop >= 0 ? Math.max(indexByCapacity, indexByVoltageDrop) : -1; const practicalMinimumMm2 = input.circuitCategory ? PRACTICAL_MINIMUM_CROSS_SECTION_MM2[input.circuitCategory] : undefined; let practicalMinimumApplied = false; if (indexRecommended >= 0 && practicalMinimumMm2 != null && CROSS_SECTIONS_MM2[indexRecommended] < practicalMinimumMm2) { const minimumIndex = CROSS_SECTIONS_MM2.indexOf(practicalMinimumMm2 as (typeof CROSS_SECTIONS_MM2)[number]); if (minimumIndex >= 0 && ratedCurrents[minimumIndex] != null) { indexRecommended = minimumIndex; practicalMinimumApplied = true; } } const voltageDropCurrentBasis = ib * input.cosPhi; const rows: CrossSectionRow[] = CROSS_SECTIONS_MM2.map((crossSection, index) => { const rated = ratedCurrents[index]; const corrected = correctedCurrents[index]; return { crossSectionMm2: crossSection, ratedCurrentA: rated, correctedCurrentA: corrected, currentSufficient: corrected == null ? null : corrected >= designCurrentA, voltageDropPercent: rated == null ? null : voltageDrops[index], voltageDropSufficient: rated == null ? null : voltageDrops[index] <= input.maxVoltageDropPercent, maxLengthForVoltageDropM: rated == null || voltageDropCurrentBasis <= 0 ? null : (input.maxVoltageDropPercent * kappa * crossSection * input.voltage) / (100 * voltageDropCoefficient * voltageDropCurrentBasis), recommended: index === indexRecommended, }; }); let protectionCoordination: ProtectionCoordinationResult | null = null; if (input.existingProtection && indexRecommended >= 0) { const iz = rows[indexRecommended].correctedCurrentA; const factor = conventionalTrippingFactor(input.existingProtection); const inA = input.existingProtection.ratedCurrentA; protectionCoordination = { ratedCurrentA: inA, kind: input.existingProtection.kind, conventionalTrippingFactor: factor, coordinated: iz != null && inA <= iz && factor * inA <= 1.45 * iz, }; } return { ...empty, supported: true, unsupportedReason: null, crossSectionByCapacityMm2: indexByCapacity >= 0 ? CROSS_SECTIONS_MM2[indexByCapacity] : null, crossSectionByVoltageDropMm2: indexByVoltageDrop >= 0 ? CROSS_SECTIONS_MM2[indexByVoltageDrop] : null, recommendedCrossSectionMm2: indexRecommended >= 0 ? CROSS_SECTIONS_MM2[indexRecommended] : null, voltageDropAtRecommendedPercent: indexRecommended >= 0 ? voltageDrops[indexRecommended] : null, combinedDerationFactor, harmonicReductionApplied, practicalMinimumApplied, rows, protectionCoordination, }; } // -- Alerts ------------------------------------------------------------------- export interface CableSizingAlert { kind: "critical" | "warn" | "info" | "ok"; text: string; } const numberFormat = (value: number, digits = 1) => value.toLocaleString("de-DE", { minimumFractionDigits: digits, maximumFractionDigits: digits }); const crossSectionFormat = (value: number) => `${value.toLocaleString("de-DE", { maximumFractionDigits: 1 })} mm²`; export function buildCableSizingAlerts(input: CableSizingInput, result: CableSizingResult): CableSizingAlert[] { const alerts: CableSizingAlert[] = []; const group = LAYING_METHOD_GROUP[input.layingMethod]; if (!result.supported) { alerts.push({ kind: "critical", text: result.unsupportedReason === "temperature" ? `${group === "air" ? "Umgebungstemperatur" : "Bodentemperatur"} ${input.ambientTemperatureC} °C liegt außerhalb der Tabelle für ${input.insulation === "pvc" ? "PVC" : "VPE"} – keine Berechnung möglich.` : `Häufung mit ${input.groupingCircuits} Stromkreisen liegt außerhalb der Tabelle (max. ${MAX_GROUPING_CIRCUITS}) – keine Berechnung möglich.`, }); return alerts; } if (result.temperatureUsedC !== input.ambientTemperatureC) { alerts.push({ kind: "info", text: `Temperatur auf den nächsten Tabellenwert ${result.temperatureUsedC} °C gerundet (sichere Seite).`, }); } if (result.groupingUsed !== input.groupingCircuits) { alerts.push({ kind: "info", text: `Häufung auf den nächsten Tabellenwert ${result.groupingUsed} Stromkreise gerundet (sichere Seite).`, }); } const byCapacity = result.crossSectionByCapacityMm2; const byDrop = result.crossSectionByVoltageDropMm2; if (byCapacity == null) { const dueToProtection = input.existingProtection != null && result.designCurrentA > result.operatingCurrentA; alerts.push({ kind: "critical", text: dueToProtection ? `Das vorhandene Schutzorgan (${input.existingProtection!.ratedCurrentA} A) verlangt Iz ≥ ${numberFormat(result.designCurrentA)} A – das erreicht bei Verlegeart ${input.layingMethod} kein Querschnitt bis 240 mm². Verlegeart oder Schutzorgan prüfen.` : `Der Betriebsstrom ${numberFormat(result.operatingCurrentA)} A übersteigt bei Verlegeart ${input.layingMethod} die Belastbarkeit aller Querschnitte bis 240 mm².`, }); } else if (byDrop == null) { alerts.push({ kind: "critical", text: `Der Spannungsfall von ${numberFormat(input.maxVoltageDropPercent)} % wird bei ${input.lengthM} m mit keinem Querschnitt bis 240 mm² eingehalten.`, }); } else if (byDrop > byCapacity) { alerts.push({ kind: "warn", text: `Der Spannungsfall ist maßgeblich: ${crossSectionFormat(byCapacity)} reichen thermisch, für ΔU ≤ ${numberFormat(input.maxVoltageDropPercent)} % sind ${crossSectionFormat(byDrop)} nötig.`, }); } if (result.harmonicReductionApplied) { alerts.push({ kind: "info", text: "Reduktionsfaktor 0,86 wegen 15–33 % 3. Harmonischer im Neutralleiter angewendet.", }); } if (input.phase === 3 && input.harmonicNeutralLoad === "over33Percent") { alerts.push({ kind: "warn", text: "3. Harmonische > 33 %: Der Neutralleiter ist nach dem Neutralleiterstrom zu bemessen – hier nicht berücksichtigt.", }); } if (group === "ground") { alerts.push({ kind: "info", text: "Erdverlegung: Bodenwärmewiderstand 2,5 K·m/W angenommen (Tabellenreferenz).", }); } if (result.practicalMinimumApplied) { alerts.push({ kind: "info", text: `Auf ${crossSectionFormat(result.recommendedCrossSectionMm2!)} angehoben (Praxis-Mindestquerschnitt für 1-phasige Stromkreise, keine Rechenanforderung).`, }); } if (result.protectionCoordination) { const coordination = result.protectionCoordination; const kindLabel = coordination.kind === "breaker" ? "Leitungsschutzschalter" : "Sicherung gG"; alerts.push( coordination.coordinated ? { kind: "info", text: `Schutzorgan ${kindLabel} ${coordination.ratedCurrentA} A berücksichtigt: In ≤ Iz und I₂ = ${numberFormat(coordination.conventionalTrippingFactor, 2)} · In ≤ 1,45 · Iz. Kurzschlussschutz und Abschaltbedingungen sind nicht Teil dieser Berechnung.`, } : { kind: "warn", text: `Schutzorgan ${kindLabel} ${coordination.ratedCurrentA} A ist mit dem empfohlenen Querschnitt nicht koordiniert (In ≤ Iz, I₂ ≤ 1,45 · Iz).`, } ); } const recommendedRow = result.rows.find((row) => row.recommended); if (recommendedRow && byCapacity != null && byDrop != null) { if (!alerts.some((alert) => alert.kind === "warn" || alert.kind === "critical")) { alerts.push({ kind: "ok", text: `${crossSectionFormat(recommendedRow.crossSectionMm2)} ${input.conductorMaterial === "aluminum" ? "Al" : "Cu"} erfüllt bei ${input.lengthM} m Belastbarkeit (${numberFormat(result.designCurrentA)} A) und Spannungsfall (ΔU ≤ ${numberFormat(input.maxVoltageDropPercent)} %).`, }); } if (recommendedRow.maxLengthForVoltageDropM != null) { alerts.push({ kind: "info", text: `Maximale Länge bei ${crossSectionFormat(recommendedRow.crossSectionMm2)} für ΔU ≤ ${numberFormat(input.maxVoltageDropPercent)} %: ${recommendedRow.maxLengthForVoltageDropM.toFixed(0)} m.`, }); } } return alerts; }