Align cable sizing with DIN VDE 0298-4

This commit is contained in:
Julian Appel 2026-09-27 17:01:23 +02:00
parent 598003554a
commit 5cbbd3a84a
9 changed files with 262 additions and 244 deletions

View file

@ -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.

View file

@ -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.

View file

@ -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`

View file

@ -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

View file

@ -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,

View file

@ -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`

View file

@ -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<LayingMethod, "air" | "ground"> = {
@ -21,8 +19,7 @@ export const LAYING_METHOD_GROUP: Record<LayingMethod, "air" | "ground"> = {
B2: "air",
C: "air",
E: "air",
D1: "ground",
D2: "ground",
D: "ground",
};
export const LAYING_METHOD_LABELS: Record<LayingMethod, string> = {
@ -31,8 +28,7 @@ export const LAYING_METHOD_LABELS: Record<LayingMethod, string> = {
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<InsulationMaterial, string> = {
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<HarmonicNeutralLoad, string> = {
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<InsulationMaterial, Record<number, number>>
@ -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<number, number>> = {
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<number, number>> = {
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") {

View file

@ -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 (
<FormModal
description="Querschnittsempfehlung nach IEC 60364-5-52 Anhang B (Belastbarkeit, Temperatur, Häufung, Spannungsfall, Schutzorgan) oder direkte Eingabe. Die Empfehlung vor der Übernahme fachlich prüfen; nationale Tabellen (DIN VDE 0298-4) können abweichen."
description="Querschnittsempfehlung mit Strombelastbarkeit, Temperatur- und Häufungsfaktoren nach DIN VDE 0298-4:2023-06 einschließlich Berichtigung 1:2023-10. Spannungsfall und Schutzorgan werden ergänzend geprüft. Die Empfehlung vor der Übernahme fachlich prüfen."
isSaving={isSaving}
onClose={onClose}
onSubmit={handleSubmit}
@ -234,6 +235,11 @@ export function CableSizingModal({
type="number"
value={lengthM}
/>
{lengthM === 0 ? (
<div className="form-text text-warning">
Bei 0 m wird kein Spannungsfall angesetzt.
</div>
) : null}
</div>
<div className="col-12 col-md-4">
<label className="form-label" htmlFor="cable-sizing-phase">
@ -279,6 +285,7 @@ export function CableSizingModal({
onChange={(event) => {
const method = event.target.value as LayingMethod;
setLayingMethod(method);
setGroupingCircuits((current) => Math.min(current, maxGroupingCircuits(method)));
applyReferenceTemperature(method, insulation);
resetResult();
}}
@ -362,7 +369,7 @@ export function CableSizingModal({
<input
className="form-control"
id="cable-sizing-grouping"
max={MAX_GROUPING_CIRCUITS}
max={groupingMaximum}
min={1}
onChange={(event) => {
setGroupingCircuits(Math.max(1, Math.round(Number(event.target.value) || 1)));
@ -373,7 +380,10 @@ export function CableSizingModal({
value={groupingCircuits}
/>
<div className="form-text">
Anzahl gemeinsam verlegter Stromkreise, einschließlich dieses.
Anzahl gemeinsam verlegter Stromkreise, einschließlich dieses
{layingMethod === "E"
? "; ohne Angaben zur Kabelpritsche ist nur 1 zulässig."
: `; für diese Verlegeart sind bis ${groupingMaximum} tabelliert.`}
</div>
</div>
{phase === 3 ? (

View file

@ -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);
});
});