From 5cbbd3a84aaf56e9ef998448b85d2012bac20765 Mon Sep 17 00:00:00 2001 From: Julian Appel Date: Sun, 27 Sep 2026 17:01:23 +0200 Subject: [PATCH] Align cable sizing with DIN VDE 0298-4 --- AGENTS.md | 11 +- docs/cable-sizing-module.md | 218 +++++++----------- docs/circuit-list-editor-api.md | 7 +- docs/circuit-list-editor-known-limitations.md | 10 +- docs/current-architecture.md | 6 +- docs/spec/08-current-product-backlog.md | 5 +- .../domain/cable-sizing-calculation.ts | 146 ++++++------ .../components/cable-sizing-modal.tsx | 18 +- tests/cable-sizing-calculation.test.ts | 85 +++++-- 9 files changed, 262 insertions(+), 244 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index a8f97c3..e9db70a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -607,10 +607,13 @@ circuit list (see `docs/cable-sizing-module.md`). Rules: by clicking a grid cell; cable cells keep the normal spreadsheet behavior. - It has no database table and no migration; `/api/cable-sizing/*` is stateless. -- Reference data is IEC 60364-5-52:2009 Annex B. Change table values only with - a documented source in `docs/cable-sizing-module.md`; where sources differ, - use the lower value. Inputs outside the tables return no result instead of - assuming a factor of 1. +- Current-carrying capacities and correction factors use + DIN VDE 0298-4:2023-06 with Berichtigung 1:2023-10; the exact scope and the + separate basis of voltage-drop and protection checks are documented in + `docs/cable-sizing-module.md`. Inputs outside the supported tables return no + result instead of assuming a factor of 1. Direct burial, grouped method E, + aluminium below 25 mm² and neutral-current sizing above 33 % third harmonic + remain unsupported until their required inputs or normative basis exist. - Inputs are prefilled from the circuit (power, phase, voltage, weighted cos φ, length, protective device) and remain editable; manual cable entry without a calculation stays possible. diff --git a/docs/cable-sizing-module.md b/docs/cable-sizing-module.md index 374994e..cd8b08d 100644 --- a/docs/cable-sizing-module.md +++ b/docs/cable-sizing-module.md @@ -1,159 +1,109 @@ -# Leitungsberechnung (Cable-Sizing-Modul) +# Leitungsberechnung -Optionales, abgegrenztes Rechenmodul für die Querschnittsempfehlung eines -Stromkreises. Es wurde als eigenständiges Modul vereinbart, das an die -Stromkreisliste angedockt ist. Es **schreibt keine Projektdaten selbst**: Ein -Ergebnis wird erst durch ausdrückliches „Übernehmen“ über den bestehenden -Befehl `circuit.update` in den Stromkreis geschrieben. Revisionen und -Undo/Redo bleiben damit unverändert gültig. +Das optionale Modul gibt für einen einzelnen Stromkreis eine prüfbare +Querschnittsempfehlung aus. Es schreibt selbst keine Projektdaten. Erst die +ausdrückliche Aktion **Übernehmen** aktualisiert `cableCrossSection`, +`cableType` und `cableLength` über den vorhandenen Befehl `circuit.update`. +Revisionen sowie Undo/Redo bleiben dadurch erhalten. -Die vollständige, automatische Bemessung aller Stromkreise (siehe -`docs/spec/06-future-sizing-and-calculations.md`) ist weiterhin **nicht** -Teil dieses Moduls. +Eine automatische Bemessung aller Stromkreise ist weiterhin nicht Bestandteil +des Moduls. -## Aufbau und Andockpunkte +## Aufbau -``` -src/cable-sizing/domain/cable-sizing-calculation.ts reine Berechnung + Tabellen -src/cable-sizing/domain/cable-sizing-contracts.ts Zod-Schema der API +```text +src/cable-sizing/domain/cable-sizing-calculation.ts Berechnung und Tabellen +src/cable-sizing/domain/cable-sizing-contracts.ts API-Validierung src/server/controllers/cable-sizing.controller.ts zustandslose Endpunkte -src/server/routes/cable-sizing.routes.ts Router unter /api/cable-sizing +src/server/routes/cable-sizing.routes.ts Router src/frontend/components/cable-sizing-api.ts Fetch-Client -src/frontend/components/cable-sizing-defaults.ts Vorbelegung aus dem Stromkreis +src/frontend/components/cable-sizing-defaults.ts Vorbelegung src/frontend/components/cable-sizing-modal.tsx Dialog -tests/cable-sizing-calculation.test.ts Tabellen, Rechnung, Vorbelegung +tests/cable-sizing-calculation.test.ts Tabellen und Grenzfälle ``` -Das Domänenmodul importiert nichts aus `db/`, `server/` oder `frontend/`. -Außerhalb dieser Dateien gibt es genau zwei Andockpunkte: +Das Domänenmodul hängt weder von Datenbank, Server noch React ab. Die API ist +zustandslos. Im Stromkreis-Editor öffnet ein Rechner-Symbol den Dialog; die +Kabelzellen behalten ihr übliches Spreadsheet-Verhalten. -1. `src/server/index.ts`: `app.use("/api/cable-sizing", cableSizingRouter)`. -2. `src/frontend/components/circuit-tree-editor.tsx`: Zustand für den - geöffneten Dialog, ein Button **Leitungsberechnung** (Rechner-Icon) in der - Aktionsspalte jeder Stromkreiszeile, der Dialog selbst und - `handleApplyCableSizing`, das über `updateCircuitById` den regulären - `circuit.update`-Befehl ausführt. +## Eingaben -Der Aufruf liegt bewusst **nicht** auf einer Tabellenzelle: Klick, Doppelklick, -Enter, F2 und Tippen in den Kabelspalten verhalten sich wie in jeder anderen -Zelle (Spreadsheet-Regel in `AGENTS.md`). Es gibt keine eigene Datenbanktabelle -und keine Migration. +Aus dem Stromkreis werden Leistung, Phasen, Spannung, leistungsgewichtetes +cos φ, Leitungslänge und ein vorhandenes Überstromschutzorgan vorbelegt. Der +Planer wählt Verlegeart, Isolierstoff, Leitermaterial, Temperatur, Häufung, +zulässigen Spannungsfall und bei Drehstrom den Anteil der dritten Harmonischen. -## Eingaben und Vorbelegung +Eine Leitungslänge von `0 m` ist zulässig. Der Dialog weist sichtbar darauf +hin, dass dann kein Spannungsfall angesetzt wird. -| Eingabe | Vorbelegung | -|---|---| -| Leistung | `circuitTotalPower` des Stromkreises (mit Geräte-GZF), nicht editierbar | -| Phasen | 3~, wenn die Stromkreisgruppe 3-phasig ist oder eine Gerätezeile `three_phase` hat, sonst 1~ | -| Spannung | `circuit.voltage` (aus den Projekteinstellungen abgeleitet); bei manuell abweichender Phasenzahl die jeweilige Projektspannung | -| cos φ | leistungsgewichtet aus den Gerätezeilen: P / Σ(P_Zeile / cos φ_Zeile), fehlendes cos φ = 1; änderbar | -| Länge | `cableLength` des Stromkreises | -| Schutzorgan | Schutzgerät des Stromkreises: LS, FI/LS, AFDD = Leitungsschutzschalter; D0/NH = Sicherung gG; FI ohne Überstromauslöser wird nicht berücksichtigt | -| Verlegeart, Isolierstoff, Leitermaterial, Temperatur, Häufung, max. ΔU, Oberschwingungen | Planereingaben | +## Normative Grundlage und Grenzen -Kabeltyp und Querschnitt lassen sich im Dialog auch ohne Berechnung direkt -eintragen. Weicht der Eintrag von der Empfehlung oder dem -Praxis-Mindestquerschnitt ab, erscheint nur ein Hinweis. +Die Strombelastbarkeiten und Korrekturfaktoren wurden gegen +**DIN VDE 0298-4 (VDE 0298-4):2023-06** einschließlich +**Berichtigung 1:2023-10** geprüft: + +- Strombelastbarkeit: Tabellen 3 bis 6. +- Temperaturfaktoren: Tabellen 17 und 20. +- Häufung A1 bis C: Tabelle 22 nach der korrigierten Zuordnung. +- Häufung D: Tabelle 26 für sich berührende Rohre oder Kanäle im Erdreich. +- Oberschwingungen: Anhang B, insbesondere Tabelle B.1. + +Unterstützt werden A1, A2, B1, B2, C, D und E. D bezeichnet ausschließlich +mehradrige Kabel oder Mantelleitungen im Rohr beziehungsweise Kanal im +Erdreich. Direkte Erdverlegung wird nicht angeboten, weil DIN VDE 0298-4 dafür +auf die Kabel-Produktnorm DIN VDE 0276-603 verweist. + +Für Verlegeart E hängen Häufungsfaktoren von Trägerart, Abstand und Anzahl der +Kabelpritschen ab. Diese Angaben gehören noch nicht zum Eingabemodell. Daher +ist E nur für einen einzelnen Stromkreis berechenbar. Für D sind höchstens +sechs gehäufte Stromkreise durch die verwendete Tabellenzeile abgedeckt. + +Aluminium wird erst ab 25 mm² angeboten, weil die verwendeten Tabellen keine +kleineren Aluminiumquerschnitte enthalten. Bei mehr als 33 % dritter +Harmonischer ist der Neutralleiterstrom Bemessungsgrundlage. Solange dieser +nicht als Zahlenwert eingegeben und berechnet wird, liefert das Modul keine +Empfehlung. + +Die Berechnung des Spannungsfalls ist eine vereinfachte Ergänzung mit +Leitfähigkeit bei Betriebstemperatur; Reaktanz wird vernachlässigt. Ihre +Planungsgrenzen folgen nicht allein aus DIN VDE 0298-4. Auch die Koordination +mit Leitungsschutzschaltern und gG-Sicherungen gehört zur +Überstromschutzbemessung und ist getrennt von der Strombelastbarkeit zu +beurteilen. Der Dialog bezeichnet diese Ergebnisse deshalb nicht als vollständig +normative Leitungsbemessung. ## Rechenweg -1. Betriebsstrom I_B aus Leistung, Spannung, cos φ und Phasenzahl. -2. Erforderliche Belastbarkeit: ohne Schutzorgan I_B; mit Schutzorgan - max(I_B, I_n, I₂/1,45) nach IEC 60364-4-43 (I_n ≤ I_z und I₂ ≤ 1,45 · I_z). - I₂/I_n: Leitungsschutzschalter 1,45; Sicherung gG 2,1 (I_n ≤ 4 A), - 1,9 (4 A < I_n < 16 A), 1,6 (I_n ≥ 16 A). -3. Tabellenwert I_z je Querschnitt × Temperaturfaktor × Häufungsfaktor - (× 0,86 bei 15–33 % 3. Harmonischer im Drehstromkreis). -4. Spannungsfall ΔU = k · L · I_B · cos φ / (κ · A · U) mit k = 2 (1~) bzw. - √3 (3~), κ = 48 (Cu) bzw. 30 (Al) m/(Ω·mm²) bei Betriebstemperatur; - Reaktanz wird vernachlässigt. -5. Empfehlung = größerer Querschnitt aus Belastbarkeit und Spannungsfall; - 1-phasige Stromkreise werden auf den Praxis-Mindestquerschnitt 2,5 mm² - angehoben (Planungskonvention aus `docs/spec/06-…`, keine Rechenanforderung). -6. Zusätzlich: maximale Länge für den zulässigen Spannungsfall. +1. Betriebsstrom `I_B` aus Leistung, Spannung, cos φ und Phasenzahl. +2. Erforderliche Belastbarkeit aus `I_B` und gegebenenfalls dem vorhandenen + Schutzorgan. +3. Tabellenwert `I_z` multipliziert mit Temperatur-, Häufungs- und gegebenenfalls + Oberschwingungsfaktor. +4. Vereinfachter Spannungsfall für jeden verfügbaren Querschnitt. +5. Empfehlung als größerer Querschnitt aus Belastbarkeit und Spannungsfall. +6. Bei einphasigen Stromkreisen Anhebung auf den vereinbarten praktischen + Mindestquerschnitt von 2,5 mm². -Temperaturen und Häufungen **zwischen** Tabellenwerten werden auf den nächsten -ungünstigeren Tabellenwert gerundet (höhere Temperatur, mehr Stromkreise). -Werte **außerhalb** der Tabellen liefern kein Ergebnis und eine Meldung – es -wird nie stillschweigend mit Faktor 1 gerechnet. - -## Datenbasis und Prüfnachweis - -Grundlage ist **IEC 60364-5-52:2009, Anhang B**. Alle Werte wurden am -26.09.2026 zeilenweise mit öffentlich zugänglichen Wiedergaben abgeglichen: -TiSoft ElectricalDesign-Hilfe zu IEC 60364-5-52 -(`ti-soft.com/en/support/help/electricaldesign/standards/iec-60364-5-52/…`) -und ECalPro (`ecalpro.com/standards/iec-60364/…`). Wo die Quellen voneinander -abweichen, ist der kleinere, sichere Wert eingetragen. - -| Tabelle | Inhalt | Abgleich | Abweichungen | -|---|---|---|---| -| B.52.2 | PVC, 2 belastete Adern, Cu/Al, A1–D2 | TiSoft; Cu A1–C zusätzlich ECalPro | Cu A1 240 mm²: 321 / 320 → 320 | -| B.52.3 | VPE, 2 belastete Adern, Cu/Al, A1–D2 | TiSoft | – | -| B.52.4 | PVC, 3 belastete Adern, Cu/Al, A1–D2 | TiSoft | – | -| B.52.5 | VPE, 3 belastete Adern, Cu/Al, A1–D2 | TiSoft | – | -| B.52.10 / .11 | PVC, Verlegeart E, mehradrig, Cu / Al | TiSoft; Cu 2 Adern zusätzlich ECalPro | Cu 3 Adern 70 / 120 mm²: 198 / 278 bzw. 196 / 276 → 196 / 276 | -| B.52.12 / .13 | VPE, Verlegeart E, mehradrig, Cu / Al | TiSoft | – | -| B.52.14 | Temperatur Luft, PVC / VPE | TiSoft, ECalPro | – | -| B.52.15 | Temperatur Erde, PVC / VPE | TiSoft | – | -| B.52.17 | Häufung Luft, Zeile 1 (gebündelt) | TiSoft, ECalPro | – | -| B.52.18 | Häufung Erde direkt, Kabel berührend | TiSoft | – | -| B.52.19 A | Häufung mehradrige Kabel in berührenden Rohren im Erdreich | TiSoft | – | - -Anwendung der Tabellen: - -- Verlegearten A1, A2, B1, B2, C, E in Luft (Bezug 30 °C), D1 und D2 im - Erdreich (Bezug 20 °C, Bodenwärmewiderstand 2,5 K·m/W). Nach Tabelle B.52.1 - ist **D1 = Leitung im Rohr/Kabelschutzrohr im Erdreich** und **D2 = Kabel - direkt im Erdreich**. -- Häufung in Luft immer nach B.52.17 Zeile 1, auch für Verlegeart E; das ist - die ungünstigste Anordnung (einlagig auf Pritschen wären höhere Faktoren - zulässig). D1 nach B.52.19 A, D2 nach B.52.18 jeweils „berührend“. -- Aluminium nur mit den echten Aluminiumwerten (ab 2,5 mm², D2 ab 16 mm²); - Querschnitte ohne Tabellenwert werden nie empfohlen. -- Die Verlegearten F und G (Einzeladern mit Abstand) sind nicht enthalten. - -**Vor produktivem Einsatz:** Die nationalen Tabellen der DIN VDE 0298-4 -können in einzelnen Werten abweichen. Der Planer muss die Tabellen gegen den -Normtext prüfen. Die Prüfung ist in `tests/cable-sizing-calculation.test.ts` -über Stichwerte und eine Monotonieprüfung jeder Tabellenzeile abgesichert. +Temperaturen und Häufungen zwischen Tabellenwerten werden zum nächsten +ungünstigeren Tabellenwert gerundet. Außerhalb des belegten Tabellenbereichs +wird keine Empfehlung ausgegeben. Ein abweichender Querschnitt kann weiterhin +manuell eingetragen und bewusst übernommen werden. ## Nicht enthalten -- Kurzschlussfestigkeit, Abschaltbedingungen (Schleifenimpedanz) und - Selektivität -- Bodenwärmewiderstand ≠ 2,5 K·m/W, Häufung mit Abstand, Verlegearten F/G -- Bemessung des Neutralleiters bei > 33 % 3. Harmonischer (nur Hinweis) -- automatische Bemessung oder Prüfung aller Stromkreise +- direkte Erdverlegung +- detaillierte Anordnung mehrerer Kabel bei Verlegeart E +- Neutralleiterbemessung bei mehr als 33 % dritter Harmonischer +- Kurzschlussfestigkeit, Schleifenimpedanz, Abschaltbedingungen und Selektivität +- abweichender Bodenwärmewiderstand sowie Verlegearten F und G +- automatische Berechnung oder Änderung mehrerer Stromkreise ## API -`POST /api/cable-sizing/calculate` – zustandslos, liest und schreibt keine -Projektdaten. +`POST /api/cable-sizing/calculate` berechnet zustandslos. Die Verlegeart ist +`A1 | A2 | B1 | B2 | C | D | E`; `lengthM` darf `0` sein. Das Ergebnis enthält +`supported: false` und einen der Gründe `temperature`, `grouping` oder +`harmonics`, wenn keine belastbare Empfehlung möglich ist. -```jsonc -{ - "phase": 1, // 1 | 3 - "mode": "power", // "power" | "current" - "powerKw": 4.2, // bei mode "power" - "currentA": 18.3, // bei mode "current" - "cosPhi": 0.95, // 0,1 … 1 - "voltage": 230, - "lengthM": 23.5, - "layingMethod": "C", // A1 | A2 | B1 | B2 | C | D1 | D2 | E - "conductorMaterial": "copper", // copper | aluminum - "insulation": "pvc", // pvc | xlpe - "ambientTemperatureC": 30, // Luft; bei D1/D2 Bodentemperatur - "groupingCircuits": 1, // 1 … 100, Tabellen bis 20 - "maxVoltageDropPercent": 3, - "harmonicNeutralLoad": "none", // none | 15to33Percent | over33Percent - "existingProtection": { "ratedCurrentA": 16, "kind": "breaker" }, // optional; kind: breaker | fuse - "circuitCategory": "single_phase" // optional -} -``` - -Antwort: `{ "result": CableSizingResult, "alerts": CableSizingAlert[] }`. -`result.supported = false` mit `unsupportedReason` (`temperature` oder -`grouping`), wenn die Eingaben außerhalb der Tabellen liegen. -`GET /api/cable-sizing/laying-methods` und `GET /api/cable-sizing/insulation-materials` -liefern die Auswahllisten. +`GET /api/cable-sizing/laying-methods` und +`GET /api/cable-sizing/insulation-materials` liefern die Auswahllisten. diff --git a/docs/circuit-list-editor-api.md b/docs/circuit-list-editor-api.md index aea5130..e528f6e 100644 --- a/docs/circuit-list-editor-api.md +++ b/docs/circuit-list-editor-api.md @@ -433,14 +433,15 @@ read nor write project data; applying a result uses `circuit.update` through - `POST /cable-sizing/calculate` - request: phase, mode (`power` | `current`), powerKw or currentA, cosPhi, - voltage, lengthM, layingMethod (`A1` | `A2` | `B1` | `B2` | `C` | `D1` | - `D2` | `E`), conductorMaterial, insulation (`pvc` | `xlpe`), + voltage, lengthM, layingMethod (`A1` | `A2` | `B1` | `B2` | `C` | `D` | + `E`), conductorMaterial, insulation (`pvc` | `xlpe`), ambientTemperatureC, groupingCircuits, maxVoltageDropPercent, harmonicNeutralLoad, optional `existingProtection` (`{ ratedCurrentA, kind: "breaker" | "fuse" }`) and circuitCategory - returns `{ "result": { ... }, "alerts": [ ... ] }`; `result.supported` is `false` with `unsupportedReason` when temperature or grouping lie - outside the tables + outside the tables or the neutral current needed above 33 % third + harmonic is unavailable - unknown fields are rejected (`400`) - `GET /cable-sizing/laying-methods` - `GET /cable-sizing/insulation-materials` diff --git a/docs/circuit-list-editor-known-limitations.md b/docs/circuit-list-editor-known-limitations.md index 22a6297..78bb44e 100644 --- a/docs/circuit-list-editor-known-limitations.md +++ b/docs/circuit-list-editor-known-limitations.md @@ -20,11 +20,11 @@ - The global device library supports basic CRUD and copy operations, but has no versioning, permissions or controlled synchronization model. - There is no automatic sizing of all circuits. The optional cable sizing - module suggests a cross-section for one circuit on demand (IEC 60364-5-52 - Annex B tables; no short-circuit, disconnection-time or selectivity checks, - no methods F/G, soil resistivity fixed at 2.5 K·m/W) and does not validate - manually entered cable values. National tables (DIN VDE 0298-4) must be - checked by the planner; see `docs/cable-sizing-module.md`. + module suggests a cross-section for one circuit on demand using + DIN VDE 0298-4:2023-06 with Berichtigung 1:2023-10. It excludes direct + burial, grouped method E and neutral-current sizing above 33 % third + harmonic, and it does not check short-circuit withstand, disconnection time + or selectivity. See `docs/cable-sizing-module.md`. - Sorting is view-only until users explicitly apply sorted order. - Complete-circuit drag supports one circuit at a time between groups of the same category. Cross-category moves and multi-circuit moves across group diff --git a/docs/current-architecture.md b/docs/current-architecture.md index 2b4e38c..2887e8a 100644 --- a/docs/current-architecture.md +++ b/docs/current-architecture.md @@ -507,8 +507,10 @@ Kopieren in ein Projekt erzeugt ein eigenständiges Projektgerät. ## Leitungsberechnung Das optionale Modul `src/cable-sizing` berechnet auf Anforderung eine -Querschnittsempfehlung für einen Stromkreis (IEC 60364-5-52 Anhang B: -Belastbarkeit, Temperatur, Häufung, Spannungsfall, Schutzorgan). Die +Querschnittsempfehlung für einen Stromkreis. Strombelastbarkeit, Temperatur +und Häufung beruhen auf DIN VDE 0298-4:2023-06 einschließlich +Berichtigung 1:2023-10; Spannungsfall und Schutzorgan werden ergänzend und mit +den in der Moduldokumentation beschriebenen Grenzen geprüft. Die Domänenlogik ist rein, `POST /api/cable-sizing/calculate` ist zustandslos und es gibt keine eigene Tabelle. Der Editor öffnet den Dialog über den Button „Leitungsberechnung“ in der Aktionsspalte; „Übernehmen“ schreibt Kabeltyp, diff --git a/docs/spec/08-current-product-backlog.md b/docs/spec/08-current-product-backlog.md index 9663e33..e7821b4 100644 --- a/docs/spec/08-current-product-backlog.md +++ b/docs/spec/08-current-product-backlog.md @@ -47,8 +47,9 @@ requirements and intended sequencing, not proof of implementation. - [x] Phase F: documentation and full GUI verification. - [ ] Keep full electrical sizing and cable-dimensioning rules separate until the planner supplies the calculation requirements. The optional per-circuit - cable sizing module (`docs/cable-sizing-module.md`) exists; its tables must - be confirmed by the planner against DIN VDE 0298-4. + cable sizing module (`docs/cable-sizing-module.md`) exists; its supported + tables were confirmed against DIN VDE 0298-4:2023-06 with + Berichtigung 1:2023-10. Unsupported arrangements stay explicitly blocked. - Detailed specification: `docs/spec/09-distribution-board-components-and-protection-groups.md` diff --git a/src/cable-sizing/domain/cable-sizing-calculation.ts b/src/cable-sizing/domain/cable-sizing-calculation.ts index 3c881bd..07dd052 100644 --- a/src/cable-sizing/domain/cable-sizing-calculation.ts +++ b/src/cable-sizing/domain/cable-sizing-calculation.ts @@ -2,16 +2,14 @@ // 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 data: DIN VDE 0298-4 (VDE 0298-4):2023-06 together with +// Berichtigung 1:2023-10. The table-by-table scope and the separate sources +// required for voltage drop and protection coordination are documented in +// docs/cable-sizing-module.md. -// -- Reference installation methods (IEC 60364-5-52, Table B.52.1) ---------- +// -- Reference installation methods (DIN VDE 0298-4, Table 2) --------------- -export const LAYING_METHODS = ["A1", "A2", "B1", "B2", "C", "D1", "D2", "E"] as const; +export const LAYING_METHODS = ["A1", "A2", "B1", "B2", "C", "D", "E"] as const; export type LayingMethod = (typeof LAYING_METHODS)[number]; export const LAYING_METHOD_GROUP: Record = { @@ -21,8 +19,7 @@ export const LAYING_METHOD_GROUP: Record = { B2: "air", C: "air", E: "air", - D1: "ground", - D2: "ground", + D: "ground", }; export const LAYING_METHOD_LABELS: Record = { @@ -31,8 +28,7 @@ export const LAYING_METHOD_LABELS: Record = { 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", + D: "D – Mehradriges Kabel im Rohr oder Kanal im Erdreich", E: "E – Mehradrige Leitung frei in Luft", }; @@ -49,7 +45,7 @@ export const INSULATION_MATERIAL_LABELS: Record = { 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) --- +// -- Harmonic neutral loading (DIN VDE 0298-4, Annex B, three-phase only) --- export const HARMONIC_NEUTRAL_LOAD_OPTIONS = [ "none", @@ -61,7 +57,7 @@ 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", + over33Percent: "3. Harmonische > 33 % – ohne Neutralleiterstrom keine Empfehlung", }; export const HARMONIC_REDUCTION_FACTOR = 0.86; @@ -85,14 +81,13 @@ export const CURRENT_CAPACITY_A: 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. + // DIN VDE 0298-4, tables 3 and 4. A1: { - 2: [14.5, 19.5, 26, 34, 46, 61, 80, 99, 119, 151, 182, 210, 240, 273, 320], + 2: [15.5, 19.5, 26, 34, 46, 61, 80, 99, 119, 151, 182, 210, 240, 273, 321], 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], + 2: [15.5, 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: { @@ -107,22 +102,18 @@ export const CURRENT_CAPACITY_A: Record< 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: { + D: { 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²). + // DIN VDE 0298-4 aluminium values; the normalization below removes all + // entries below the first tabulated aluminium size of 25 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], @@ -143,14 +134,10 @@ export const CURRENT_CAPACITY_A: Record< 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: { + D: { 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], @@ -180,14 +167,10 @@ export const CURRENT_CAPACITY_A: Record< 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: { + D: { 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], @@ -215,14 +198,10 @@ export const CURRENT_CAPACITY_A: Record< 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: { + D: { 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], @@ -231,9 +210,25 @@ export const CURRENT_CAPACITY_A: Record< }, }; +// DIN VDE 0298-4 only supplies aluminium current-carrying capacities from +// 25 mm² onward. Smaller values from the former IEC data set must therefore +// never participate in a recommendation. +for (const insulation of INSULATION_MATERIALS) { + for (const method of LAYING_METHODS) { + for (const coreCount of [2, 3] as const) { + const row = CURRENT_CAPACITY_A[insulation].aluminum[method][coreCount] as (number | null)[]; + CROSS_SECTIONS_MM2.forEach((crossSection, index) => { + if (crossSection < 25) { + row[index] = null; + } + }); + } + } +} + // -- Correction factors ------------------------------------------------------ -/** B.52.14 (air, reference 30 °C) and B.52.15 (ground, reference 20 °C). */ +/** DIN VDE 0298-4 tables 17 (air) and 20 (ground). */ export const TEMPERATURE_FACTORS: Record< "air" | "ground", Record> @@ -260,26 +255,32 @@ export const REFERENCE_TEMPERATURE_C: Record<"air" | "ground", number> = { }; /** - * 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). + * Grouping reduction factors according to the corrected references in + * DIN VDE 0298-4 Berichtigung 1:2023-10: + * A1–C use table 22, row 1 (bundled/touching), D uses table 26 for touching + * conduits/ducts in earth. Method E needs details about support, spacing and + * number of trays from table 23, which the current input model does not carry; + * it is therefore supported only for one circuit. */ -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, +export const GROUPING_FACTORS: Record<"A1_TO_C" | "D" | "E", Record> = { + A1_TO_C: { + 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, 10: 0.48, 12: 0.45, 14: 0.43, 16: 0.41, 18: 0.39, 20: 0.38, }, - 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 }, + D: { 1: 1.0, 2: 0.85, 3: 0.75, 4: 0.7, 5: 0.65, 6: 0.6 }, + E: { 1: 1.0 }, }; -export const MAX_GROUPING_CIRCUITS = 20; - function groupingTableFor(method: LayingMethod) { - return method === "D1" ? GROUPING_FACTORS.D1 : method === "D2" ? GROUPING_FACTORS.D2 : GROUPING_FACTORS.air; + return method === "D" + ? GROUPING_FACTORS.D + : method === "E" + ? GROUPING_FACTORS.E + : GROUPING_FACTORS.A1_TO_C; +} + +export function maxGroupingCircuits(method: LayingMethod): number { + return Math.max(...Object.keys(groupingTableFor(method)).map(Number)); } /** @@ -373,7 +374,7 @@ export interface CableSizingInput { layingMethod: LayingMethod; conductorMaterial: ConductorMaterial; insulation: InsulationMaterial; - /** Ambient air temperature, or ground temperature for D1/D2. */ + /** Ambient air temperature, or ground temperature for D. */ ambientTemperatureC: number; /** Number of grouped circuits or multi-core cables, including this one. */ groupingCircuits: number; @@ -405,7 +406,7 @@ export interface ProtectionCoordinationResult { coordinated: boolean; } -export type UnsupportedReason = "temperature" | "grouping"; +export type UnsupportedReason = "temperature" | "grouping" | "harmonics"; export interface CableSizingResult { /** false: the inputs are outside the tabulated range – no numeric result. */ @@ -454,13 +455,23 @@ export function calculateCableSizing(input: CableSizingInput): CableSizingResult input.ambientTemperatureC ); const grouping = - input.groupingCircuits <= MAX_GROUPING_CIRCUITS + input.groupingCircuits <= maxGroupingCircuits(input.layingMethod) ? lookupConservative(groupingTableFor(input.layingMethod), Math.max(1, input.groupingCircuits)) : null; + const harmonicsSupported = !( + input.phase === 3 && input.harmonicNeutralLoad === "over33Percent" + ); + const unsupportedReason: UnsupportedReason | null = !temperature + ? "temperature" + : !grouping + ? "grouping" + : !harmonicsSupported + ? "harmonics" + : null; const empty: CableSizingResult = { supported: false, - unsupportedReason: !temperature ? "temperature" : "grouping", + unsupportedReason, operatingCurrentA: ib, designCurrentA, temperatureUsedC: temperature?.key ?? null, @@ -477,7 +488,7 @@ export function calculateCableSizing(input: CableSizingInput): CableSizingResult rows: [], protectionCoordination: null, }; - if (!temperature || !grouping) { + if (unsupportedReason || !temperature || !grouping) { return empty; } @@ -586,12 +597,17 @@ export function buildCableSizingAlerts(input: CableSizingInput, result: CableSiz const group = LAYING_METHOD_GROUP[input.layingMethod]; if (!result.supported) { + const groupingMaximum = maxGroupingCircuits(input.layingMethod); 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.`, + : result.unsupportedReason === "harmonics" + ? "Bei mehr als 33 % dritter Harmonischer muss nach dem Neutralleiterstrom bemessen werden. Ohne diesen Wert wird keine Querschnittsempfehlung ausgegeben." + : input.layingMethod === "E" + ? "Für Verlegeart E benötigt die Norm bei Häufung Angaben zu Trägerart, Abstand und Anzahl der Kabelpritschen. Ohne diese Angaben ist nur ein einzelner Stromkreis berechenbar." + : `Häufung mit ${input.groupingCircuits} Stromkreisen liegt für Verlegeart ${input.layingMethod} außerhalb der verwendeten Tabelle (max. ${groupingMaximum}) – keine Berechnung möglich.`, }); return alerts; } @@ -637,10 +653,10 @@ export function buildCableSizingAlerts(input: CableSizingInput, result: CableSiz text: "Reduktionsfaktor 0,86 wegen 15–33 % 3. Harmonischer im Neutralleiter angewendet.", }); } - if (input.phase === 3 && input.harmonicNeutralLoad === "over33Percent") { + if (input.lengthM === 0) { alerts.push({ - kind: "warn", - text: "3. Harmonische > 33 %: Der Neutralleiter ist nach dem Neutralleiterstrom zu bemessen – hier nicht berücksichtigt.", + kind: "info", + text: "Leitungslänge 0 m: Für diese Berechnung wird kein Spannungsfall angesetzt.", }); } if (group === "ground") { diff --git a/src/frontend/components/cable-sizing-modal.tsx b/src/frontend/components/cable-sizing-modal.tsx index deb01b2..b15de3b 100644 --- a/src/frontend/components/cable-sizing-modal.tsx +++ b/src/frontend/components/cable-sizing-modal.tsx @@ -11,9 +11,9 @@ import { LAYING_METHODS, LAYING_METHOD_GROUP, LAYING_METHOD_LABELS, - MAX_GROUPING_CIRCUITS, PRACTICAL_MINIMUM_CROSS_SECTION_MM2, REFERENCE_TEMPERATURE_C, + maxGroupingCircuits, temperatureOptions, type CableSizingAlert, type CableSizingInput, @@ -100,6 +100,7 @@ export function CableSizingModal({ ? threePhaseVoltageV : singlePhaseVoltageV; const group = LAYING_METHOD_GROUP[layingMethod]; + const groupingMaximum = maxGroupingCircuits(layingMethod); const availableTemperatures = temperatureOptions(layingMethod, insulation); const input: CableSizingInput = useMemo( @@ -208,7 +209,7 @@ export function CableSizingModal({ return ( + {lengthM === 0 ? ( +
+ Bei 0 m wird kein Spannungsfall angesetzt. +
+ ) : null}
{phase === 3 ? ( diff --git a/tests/cable-sizing-calculation.test.ts b/tests/cable-sizing-calculation.test.ts index 05e4f3d..39cfbb1 100644 --- a/tests/cable-sizing-calculation.test.ts +++ b/tests/cable-sizing-calculation.test.ts @@ -45,28 +45,34 @@ const capacity = ( CROSS_SECTIONS_MM2.indexOf(crossSection as (typeof CROSS_SECTIONS_MM2)[number]) ]; -describe("cable sizing reference tables (IEC 60364-5-52 Annex B)", () => { +describe("cable sizing reference tables (DIN VDE 0298-4:2023-06)", () => { it("contains the checked PVC copper values", () => { - // B.52.2: A1 two loaded conductors (the previously wrong row) + assert.equal(capacity("pvc", "copper", "A1", 2, 1.5), 15.5); + assert.equal(capacity("pvc", "copper", "A2", 2, 1.5), 15.5); assert.equal(capacity("pvc", "copper", "A1", 2, 2.5), 19.5); assert.equal(capacity("pvc", "copper", "A1", 2, 16), 61); assert.equal(capacity("pvc", "copper", "A1", 2, 120), 210); - // B.52.2: B2 from 50 mm² (previously copied from B1) + // DIN table 3: B2 from 50 mm² (previously copied from B1) assert.equal(capacity("pvc", "copper", "B2", 2, 50), 133); assert.equal(capacity("pvc", "copper", "B2", 2, 120), 232); - // B.52.4 and D1/D2 + // Tables 3 and 4, including reference method D in conduits/ducts. assert.equal(capacity("pvc", "copper", "C", 3, 6), 41); - assert.equal(capacity("pvc", "copper", "D1", 2, 2.5), 29); - assert.equal(capacity("pvc", "copper", "D2", 3, 16), 70); - // Conflicting sources resolved to the lower value - assert.equal(capacity("pvc", "copper", "A1", 2, 240), 320); + assert.equal(capacity("pvc", "copper", "D", 2, 2.5), 29); + assert.equal(capacity("pvc", "copper", "A1", 2, 240), 321); assert.equal(capacity("pvc", "copper", "E", 3, 70), 196); }); - it("uses the aluminium and XLPE tables instead of flat factors", () => { - assert.equal(capacity("pvc", "aluminum", "A1", 2, 1.5), null); - assert.equal(capacity("pvc", "aluminum", "C", 3, 16), 59); - assert.equal(capacity("pvc", "aluminum", "D2", 2, 10), null); + it("uses aluminium only from the first DIN-tabulated size of 25 mm²", () => { + for (const insulation of ["pvc", "xlpe"] as const) { + for (const method of ["A1", "A2", "B1", "B2", "C", "D", "E"] as const) { + for (const cores of [2, 3] as const) { + for (const crossSection of CROSS_SECTIONS_MM2.filter((value) => value < 25)) { + assert.equal(capacity(insulation, "aluminum", method, cores, crossSection), null); + } + } + } + } + assert.equal(capacity("pvc", "aluminum", "C", 3, 25), 73); assert.equal(capacity("xlpe", "copper", "C", 2, 2.5), 33); assert.equal(capacity("xlpe", "aluminum", "E", 3, 25), 97); }); @@ -113,10 +119,15 @@ describe("calculateCableSizing", () => { const cold = calculateCableSizing({ ...BASE_INPUT, ambientTemperatureC: 5 }); assert.equal(cold.temperatureUsedC, 10); assert.equal(cold.temperatureFactor, 1.22); - const grouped = calculateCableSizing({ ...BASE_INPUT, groupingCircuits: 10 }); + const exact = calculateCableSizing({ ...BASE_INPUT, groupingCircuits: 10 }); + assert.equal(exact.groupingUsed, 10); + assert.equal(exact.groupingFactor, 0.48); + assert.equal(calculateCableSizing({ ...BASE_INPUT, groupingCircuits: 14 }).groupingFactor, 0.43); + assert.equal(calculateCableSizing({ ...BASE_INPUT, groupingCircuits: 18 }).groupingFactor, 0.39); + const grouped = calculateCableSizing({ ...BASE_INPUT, groupingCircuits: 11 }); assert.equal(grouped.groupingUsed, 12); assert.equal(grouped.groupingFactor, 0.45); - const alerts = buildCableSizingAlerts({ ...BASE_INPUT, groupingCircuits: 10 }, grouped); + const alerts = buildCableSizingAlerts({ ...BASE_INPUT, groupingCircuits: 11 }, grouped); assert.ok(alerts.some((alert) => alert.text.includes("12 Stromkreise gerundet"))); }); @@ -129,13 +140,15 @@ describe("calculateCableSizing", () => { ); }); - it("uses the ground tables for D1 (ducts) and D2 (direct)", () => { - const d1 = calculateCableSizing({ ...BASE_INPUT, layingMethod: "D1", ambientTemperatureC: 20, groupingCircuits: 10 }); - assert.equal(d1.groupingFactor, 0.49); - const d2 = calculateCableSizing({ ...BASE_INPUT, layingMethod: "D2", ambientTemperatureC: 20, groupingCircuits: 10 }); - assert.equal(d2.groupingUsed, 12); - assert.equal(d2.groupingFactor, 0.36); - assert.equal(calculateCableSizing({ ...BASE_INPUT, layingMethod: "D2", ambientTemperatureC: 30 }).temperatureFactor, 0.89); + it("limits grouping to the corrected DIN tables for D and E", () => { + const d = calculateCableSizing({ ...BASE_INPUT, layingMethod: "D", ambientTemperatureC: 20, groupingCircuits: 6 }); + assert.equal(d.groupingFactor, 0.6); + assert.equal(calculateCableSizing({ ...BASE_INPUT, layingMethod: "D", ambientTemperatureC: 20, groupingCircuits: 7 }).supported, false); + const e = calculateCableSizing({ ...BASE_INPUT, layingMethod: "E", groupingCircuits: 2 }); + assert.equal(e.supported, false); + assert.equal(e.unsupportedReason, "grouping"); + assert.ok(buildCableSizingAlerts({ ...BASE_INPUT, layingMethod: "E", groupingCircuits: 2 }, e)[0]?.text.includes("Kabelpritsche")); + assert.equal(calculateCableSizing({ ...BASE_INPUT, layingMethod: "D", ambientTemperatureC: 30 }).temperatureFactor, 0.89); }); it("refuses inputs outside the tables instead of assuming a factor of 1", () => { @@ -156,9 +169,11 @@ describe("calculateCableSizing", () => { lengthM: 5, conductorMaterial: "aluminum", }); - assert.equal(result.recommendedCrossSectionMm2, 2.5); - assert.equal(result.rows[0].ratedCurrentA, null); - assert.equal(result.rows[0].recommended, false); + assert.equal(result.recommendedCrossSectionMm2, 25); + for (const row of result.rows.filter((row) => row.crossSectionMm2 < 25)) { + assert.equal(row.ratedCurrentA, null); + assert.equal(row.recommended, false); + } }); it("checks gG fuses with I2 = 1.6 In and circuit-breakers with In ≤ Iz", () => { @@ -186,6 +201,23 @@ describe("calculateCableSizing", () => { assert.equal(single.harmonicReductionApplied, false); }); + it("does not recommend a cross-section above 33 % third harmonic without neutral current", () => { + const input = { ...BASE_INPUT, phase: 3 as const, voltage: 400, harmonicNeutralLoad: "over33Percent" as const }; + const result = calculateCableSizing(input); + assert.equal(result.supported, false); + assert.equal(result.unsupportedReason, "harmonics"); + assert.equal(result.recommendedCrossSectionMm2, null); + assert.equal(buildCableSizingAlerts(input, result)[0]?.kind, "critical"); + }); + + it("allows zero length and states that no voltage drop is applied", () => { + const input = { ...BASE_INPUT, lengthM: 0 }; + const result = calculateCableSizing(input); + assert.equal(result.supported, true); + assert.equal(result.voltageDropAtRecommendedPercent, 0); + assert.ok(buildCableSizingAlerts(input, result).some((alert) => alert.text.includes("kein Spannungsfall"))); + }); + it("raises single-phase circuits to the 2.5 mm² practical minimum", () => { const result = calculateCableSizing({ ...BASE_INPUT, @@ -203,7 +235,7 @@ describe("calculateCableSizing", () => { it("offers only tabulated temperatures", () => { assert.deepEqual(temperatureOptions("C", "pvc").at(-1), 60); assert.deepEqual(temperatureOptions("C", "xlpe").at(-1), 80); - assert.deepEqual(temperatureOptions("D1", "pvc")[2], 20); + assert.deepEqual(temperatureOptions("D", "pvc")[2], 20); }); }); @@ -219,6 +251,9 @@ describe("cable sizing request schema", () => { false ); assert.equal(cableSizingRequestSchema.safeParse({ ...BASE_INPUT, layingMethod: "F" }).success, false); + assert.equal(cableSizingRequestSchema.safeParse({ ...BASE_INPUT, layingMethod: "D1" }).success, false); + assert.equal(cableSizingRequestSchema.safeParse({ ...BASE_INPUT, layingMethod: "D2" }).success, false); + assert.equal(cableSizingRequestSchema.safeParse({ ...BASE_INPUT, layingMethod: "D" }).success, true); assert.equal(cableSizingRequestSchema.safeParse({ ...BASE_INPUT, mode: "current" }).success, false); }); });