243 lines
13 KiB
Markdown
243 lines
13 KiB
Markdown
# Cable Sizing Module (Proposal)
|
|
|
|
**Status: proposal, not yet reviewed or merged by the project owner.** This
|
|
document and the accompanying `src/cable-sizing/`, `src/db/schema/cable-sizing-calculations.ts`,
|
|
`src/server/{controllers,routes}/cable-sizing.*` and `src/frontend/components/cable-sizing-*`
|
|
files were written by a third party (see git history/authorship) against the
|
|
project's own stated direction in `docs/spec/06-future-sizing-and-calculations.md`
|
|
("The app should later support rule-based protection and cable sizing") and
|
|
`AGENTS.md` ("... and later electrical sizing logic"). Nothing here has been
|
|
pushed to the project's own repository; it exists as a local branch for
|
|
review, testing and discussion.
|
|
|
|
## Why a separate module instead of extending core domain code
|
|
|
|
`AGENTS.md` is explicit that critical multi-write commands, the revision/undo
|
|
system and the Circuit-First domain model are the supported architecture, and
|
|
that changes should be small and reviewable. A cable-sizing calculation is
|
|
**not** a project mutation - it doesn't need `expectedRevision`, doesn't
|
|
belong in the undo/redo stack, and shouldn't grow the `circuit.update`
|
|
command's `switch` statement or the DTOs it doesn't already have.
|
|
|
|
So this module is built exactly like the existing `src/external-model/`
|
|
adapter (the Revit/CSV import foundation): a one-way dependency boundary.
|
|
|
|
```
|
|
src/cable-sizing/domain/ <- pure functions and types, zero imports from
|
|
db/, server/ or frontend/
|
|
src/db/schema/ <- one new, fully additive table
|
|
src/db/repositories/ <- one plain repository (no revision semantics)
|
|
src/server/{controllers,routes}/cable-sizing.* <- one new, isolated route
|
|
group, mounted with a single app.use() line
|
|
src/frontend/components/cable-sizing-* <- one new modal + one small
|
|
API client, following the existing
|
|
FormModal / CircuitProtectionModal pattern
|
|
```
|
|
|
|
Nothing outside these files imports from them except the two required
|
|
one-line hooks described below. If this module is rejected or needs to move
|
|
out again, removing it is a matter of deleting these files, the two
|
|
one-line hooks, and running a `DROP TABLE` migration - it never touches
|
|
`circuits`, `project_revisions` or any command/history table.
|
|
|
|
## The two required hooks into existing code
|
|
|
|
1. `src/server/index.ts`: one import + one `app.use("/api/cable-sizing", cableSizingRouter)`
|
|
line, next to the existing `app.use("/api/projects", ...)` etc.
|
|
2. `src/frontend/components/circuit-tree-editor.tsx`: one new `useState` for
|
|
the open modal, one new trigger condition (mirroring the existing
|
|
`isProtectionTrigger` / `protectionEditorCircuit` pattern almost exactly),
|
|
and one new conditionally-rendered `<CableSizingModal />` alongside the
|
|
existing `<CircuitProtectionModal />`. See the diff for the exact lines.
|
|
|
|
No changes to `circuit-project-command.model.ts`, `project-command.service.ts`,
|
|
any Zod command schema, any migration for `circuits`/`project_revisions`, or
|
|
any existing test.
|
|
|
|
## Coupling to the grid UI (the one fragile point)
|
|
|
|
Everything described above - the calculation, the audit table, applying a
|
|
result via `circuit.update` - depends only on stable, explicitly documented
|
|
domain fields (`cableType`/`cableCrossSection`/`cableLength`/
|
|
`circuitTotalPower`/`voltage`/`protectionDevice.ratedCurrentA`), the same
|
|
ones `AGENTS.md` already treats as protected ("Protection and cable data
|
|
belong to the circuit"). None of that breaks if the grid UI changes.
|
|
|
|
The one place that *is* coupled to a UI implementation detail: the click
|
|
trigger in `circuit-tree-editor.tsx` matches grid **column keys**
|
|
(`cableSummary` and `cableCrossSection`, defined in
|
|
`circuit-grid-model.ts`) to decide where to show the calculator icon.
|
|
Column visibility and order in this app are a per-browser user preference,
|
|
not fixed - so this is checked against both known cable-related columns to
|
|
reduce (not eliminate) the chance a user's personal column layout hides the
|
|
trigger entirely. If a future column rename removes both keys, the failure
|
|
mode is purely cosmetic: the calculator icon/click stops appearing on that
|
|
cell, nothing crashes, no data is affected, and the rest of the app
|
|
(including manual cable-field editing) is completely unaffected - the
|
|
module's actual logic and persistence never depend on this column key.
|
|
|
|
## Feature overview
|
|
|
|
- **Calculate a recommendation**: laying method, insulation, conductor
|
|
material, ambient temperature, grouping, cos phi and max voltage drop -
|
|
same VDE 0298-4 reference-method model as the sibling Kabelliste tool
|
|
(see Verification status below).
|
|
- **Breaker-aware sizing**: if the circuit already has a protection device
|
|
(`circuit.protectionDevice`), the cross-section is selected against
|
|
`designCurrentA = max(operatingCurrentA, protectionDevice.ratedCurrentA)`,
|
|
not the load current alone - the standard `In <= Iz` rule (a breaker only
|
|
trips at its own rated current, so a cable sized for the actual load
|
|
alone could overheat under sustained current below that threshold). If no
|
|
cross-section can cover an oversized breaker, that is reported as the
|
|
limiting factor by name, not a generic capacity error.
|
|
- **Maximum length for the voltage-drop limit**: each cross-section option
|
|
also reports the longest single run that still meets the requested
|
|
`maxVoltageDropPercent` at the given load - the inverse of the
|
|
voltage-drop check, shown for the recommended cross-section.
|
|
- **Practical minimum for socket circuits**: `single_phase`-category
|
|
circuits are raised to at least 2.5 mm² if the calculation alone would
|
|
recommend less - see the dedicated note under Verification status. Never
|
|
lowers a calculation that already needs more.
|
|
- **Manual entry, always available**: the modal has a plain text Kabeltyp/
|
|
Querschnitt field. Running a calculation pre-fills it as a suggestion,
|
|
but the field is the single source of truth for what gets applied, and
|
|
stays freely editable - satisfying
|
|
`docs/spec/06-future-sizing-and-calculations.md`'s explicit requirement
|
|
that "users must remain able to manually override suggestions" without
|
|
requiring a calculation to have been run first. A non-blocking warning
|
|
appears if the typed value is not a standard cross-section, is smaller
|
|
than the last calculation, or is below the practical minimum above -
|
|
informational only, never forced, matching the same spec doc's "shown as
|
|
a warning or status indicator, not as an automatic forced change."
|
|
|
|
## How a suggestion or manual entry is applied
|
|
|
|
The module never writes to `circuits` directly. The modal's "Übernehmen"
|
|
button calls the **existing, unmodified** frontend helper with whatever is
|
|
currently in the Kabeltyp/Querschnitt fields, calculated or hand-typed:
|
|
|
|
```ts
|
|
updateCircuitById(projectId, expectedRevision, circuitId, {
|
|
cableCrossSection: "4 mm²",
|
|
cableType: "NYM-J 3x2.5",
|
|
cableLength: 30,
|
|
});
|
|
```
|
|
|
|
which is the same `circuit.update` command the grid already uses for manual
|
|
cell edits. This means:
|
|
|
|
- optimistic concurrency (`expectedRevision`) is respected automatically
|
|
- the change appears in the project's revision history and is undoable/
|
|
redoable exactly like a manual edit
|
|
- nothing is ever written automatically - calculating only fills the modal's
|
|
own fields, applying is always an explicit, separate click
|
|
|
|
## API contract
|
|
|
|
`POST /api/cable-sizing/calculate`
|
|
|
|
```jsonc
|
|
{
|
|
"phase": 1, // 1 | 3
|
|
"mode": "power", // "power" | "current"
|
|
"powerKw": 4.2, // circuit.circuitTotalPower, when mode="power"
|
|
"cosPhi": 1,
|
|
"voltage": 230, // circuit.voltage (already project-derived, read-only)
|
|
"lengthM": 23.5, // circuit.cableLength
|
|
"layingMethod": "C", // DIN VDE 0298-4 reference method, A1|A2|B1|B2|C|D1|D2|E|F|G
|
|
"conductorMaterial": "copper", // "copper" | "aluminum"
|
|
"insulation": "pvc", // "pvc" | "xlpe"
|
|
"ambientTemperatureC": 30,
|
|
"groupingCircuits": 1,
|
|
"maxVoltageDropPercent": 3,
|
|
"harmonicNeutralLoad": "none", // "none" | "15to33Percent" | "over33Percent", three-phase only
|
|
"existingProtectionRatedCurrentA": 16, // optional, from circuit.protectionDevice
|
|
"circuitCategory": "single_phase", // optional, from the circuit's section.category -
|
|
// enables the practical-minimum convention below
|
|
"context": { "projectId": "...", "circuitId": "...", "equipmentIdentifier": "-1F1.1" }
|
|
}
|
|
```
|
|
|
|
Response: `{ calculationId, result: CableSizingResult, alerts: CableSizingAlert[] }`
|
|
(see `src/cable-sizing/domain/cable-sizing-calculation.ts` for the exact
|
|
shape). `GET /api/cable-sizing/laying-methods` and `.../insulation-materials`
|
|
expose the pick-list metadata (including which combinations are verified) so
|
|
a future non-modal UI could build its own form without hard-coding the enum.
|
|
|
|
## Verification status - important
|
|
|
|
The reference current-carrying-capacity tables are ported from a sibling
|
|
project's Kabelliste module, which was itself only verified against ~8
|
|
public sources for **six** of the ten DIN VDE 0298-4 reference laying
|
|
methods (A1, B2, C, E, D1, D2) and **PVC insulation only**. The other four
|
|
methods (A2, B1, F, G) and XLPE/VPE insulation could not be verified without
|
|
contradiction across sources during that earlier work, so
|
|
`calculateCableSizing` deliberately returns `dataVerified: false` and no
|
|
numeric recommendation for those combinations, rather than a guessed value.
|
|
The UI surfaces this as a plain critical alert. Anyone with access to the
|
|
actual norm text can extend `LAYING_METHOD_VERIFIED` and
|
|
`CURRENT_CAPACITY_A` in `src/cable-sizing/domain/cable-sizing-calculation.ts`
|
|
once the missing tables are confirmed.
|
|
|
|
The protection-coordination check (`existingProtectionRatedCurrentA` vs. the
|
|
recommended cross-section's corrected capacity) is a **simplified** `In <=
|
|
Iz` check only - it is not a full IEC 60364-4-43 overload (`I2 <= 1.45 x
|
|
Iz`) or short-circuit withstand check.
|
|
|
|
`PRACTICAL_MINIMUM_CROSS_SECTION_MM2` (2.5 mm² for `single_phase`) is
|
|
**not** a verified norm value at all - it is a named planning convention,
|
|
sourced directly from this project's own
|
|
`docs/spec/06-future-sizing-and-calculations.md` ("Standard Single-Phase
|
|
Circuits ... usually use ... cable cross-section: 2.5 mm²"), applied as a
|
|
floor on top of the calculated recommendation. It intentionally only
|
|
covers the one category and one convention that document already states;
|
|
it is not a general substitute for norm-compliant calculation.
|
|
|
|
## Relationship to the project's own future-sizing spec
|
|
|
|
`docs/spec/06-future-sizing-and-calculations.md` separately describes simple
|
|
category-based defaults (e.g. lighting circuits -> 10 A / 1.5 mm²) as
|
|
"common planning defaults, not a replacement for full norm-compliant
|
|
calculation". This module is the latter: an on-demand, norm-referenced
|
|
calculation for one circuit at a time, not a bulk default-filling tool. The
|
|
two are complementary and could later be wired together (e.g. the category
|
|
defaults pre-fill this module's laying-method/insulation fields), but that
|
|
integration is out of scope here.
|
|
|
|
The spec's `isPublicBuilding` halogen-free rule and the `Control Requirement`
|
|
(DALI/KNX/core-count) field are **not** implemented by this module - both
|
|
concern cable *type* selection, not cross-section sizing, and the latter
|
|
isn't in the current schema yet. They're natural follow-ups once this module
|
|
is reviewed.
|
|
|
|
## Maintenance plan
|
|
|
|
The `src/cable-sizing/domain/` folder has zero imports from the rest of the
|
|
app by design (see the dependency-direction note above), so it can be
|
|
extracted into an independently versioned/published package later (e.g. a
|
|
private npm package or a git subtree) without touching anything outside the
|
|
five files/folders listed above - "separately maintained but part of the
|
|
app for now", per the intent of this proposal.
|
|
|
|
## Local testing
|
|
|
|
Deployed and manually exercised end-to-end on an internal test host at
|
|
`http://192.168.0.133:3220` (own docker deployment, own subnet/ports to
|
|
avoid clashing with ~50 other containers on that host - see the two
|
|
commits marked "Local-only" in this branch, which are not part of this
|
|
proposal and should not be carried over if it is ever proposed upstream).
|
|
Exercised against seeded real project/circuit data created through the
|
|
actual command API (not direct DB writes), including a deliberately long
|
|
(85 m) circuit to trigger the voltage-drop-critical path.
|
|
|
|
`npm test`, `npm run build:api`, `npm run build:web` and
|
|
`npm run typecheck:scripts` all pass with this module included at every
|
|
commit in this branch.
|
|
|
|
This branch (`feature/cable-sizing-module`) is pushed to a mirror of this
|
|
repository under this homelab's own Forgejo instance, not to
|
|
`git.jappel.io` - no write access to the upstream repository was available
|
|
or used. See the branch's commit history for the incremental history of
|
|
this module, including fixes made during manual testing (trigger column,
|
|
icon rendering, breaker-aware sizing).
|