Protection Device Architecture
This document describes the protection device, motor control device, and settings subsystem architecture.
Overview
The protection system uses a composition pattern where canvas devices (protection_devices, motor_control_devices) link to reusable catalog entries and type-specific settings tables through an intermediate device_components table.
┌─────────────────────────────────────────────────────────────────────────────┐
│ ORGANIZATION SCOPE │
│ ┌───────────────────────────────────────────────────────────────────────┐ │
│ │ device_catalog │ │
│ │ • Reusable device specifications (manufacturer, model, config) │ │
│ │ • System defaults (org_id = null) or org-specific entries │ │
│ │ • Categories: protective_relay, electronic_trip_unit, fuse, vfd... │ │
│ │ • NEW: breaker_lv category for LV breaker frames │ │
│ └───────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌───────────────┴───────────────┐ │
│ ▼ ▼ │
│ ┌─────────────────────────────┐ ┌─────────────────────────────────────┐ │
│ │ catalog_compatibility │ │ catalog_curve_data │ │
│ │ │ │ │ │
│ │ • M:N linking between │ │ • TCC curve points per catalog │ │
│ │ catalog entries │ │ • Multiple curves per entry │ │
│ │ • breaker ↔ trip unit │ │ • min_melt, total_clear, etc. │ │
│ │ compatibility │ │ │ │
│ └─────────────────────────────┘ └─────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
│
│ catalog_id (FK)
▼
┌────────────────────────────────────────────────────────────────────────────┐
│ PROJECT SCOPE │
│ │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ protection_devices │ │ motor_control_devs │ │
│ │ │ │ │ │
│ │ • Canvas component │ │ • Canvas component │ │
│ │ • Basic ratings │ │ • Basic ratings │ │
│ │ • Position (x,y) │ │ • Position (x,y) │ │
│ │ • catalog_id (FK) │ │ │ │
│ │ → breaker frame │ │ │ │
│ └──────────┬──────────┘ └──────────┬──────────┘ │
│ │ │ │
│ │ protection_device_id │ motor_control_device_id │
│ │ (mutually exclusive FKs) │ │
│ └───────────────┬───────────────┘ │
│ ▼ │
│ ┌───────────────────────────────┐ │
│ │ device_components │ │
│ │ │ │
│ │ • Links device → catalog │ │
│ │ • component_role (trip_unit, │ │
│ │ breaker, overload, fuse, │ │
│ │ relay, drive_unit, etc.) │ │
│ │ • Optional CT link │ │
│ │ • JSONB overflow settings │ │
│ └───────────────┬───────────────┘ │
│ │ │
│ │ device_component_id (FK) │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ TYPE-SPECIFIC SETTINGS TABLES │ │
│ │ │ │
│ │ trip_unit_settings │ relay_oc_settings │ fuse_settings │ │
│ │ hv_breaker_settings │ overload_relay_sett │ │ │
│ │ │ │
│ │ (1:1 relationship with device_components via unique FK) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ current_transformers │ │
│ │ │ │
│ │ • Project-scoped CTs that can be shared across devices │ │
│ │ • Linked to device_components via ct_id FK │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘Table Relationships
Core Tables
| Table | Scope | Purpose |
| - | - | - |
| device_catalog | Organization | Reusable device specs (manufacturer data, configuration JSONB) |
| catalog_compatibility | Organization | M:N linking between catalog entries (e.g., breaker frames ↔ trip units) |
| protection_devices | Project | Canvas protection devices (breakers, fuses, relays, switches) |
| motor_control_devices | Project | Canvas motor control devices (VFDs, soft starters, contactors) |
| device_components | Project | Links devices to catalog + settings, defines component role |
| current_transformers | Project | Sharable CTs referenced by device_components |
Settings Tables (1:1 with device_components)
| Table | For Roles | Key Settings |
| - | - | - |
| trip_unit_settings | trip_unit | LT/ST/INST/GF pickups, delays, I2t, ZSI |
| thermal_magnetic_settings | thermal_magnetic | Rated current, magnetic pickup mult, thermal dial |
| relay_oc_settings | relay | 51/50/51G/50G elements, CT config, curves |
| fuse_settings | fuse | Class, rating, let-through data |
| overload_relay_settings | overload, built_in_overload | FLA range, trip class, thermal memory |
| hv_breaker_settings | breaker (HV) | Opening time, arcing time, reclosing |
Curve Data Tables
| Table | Scope | Purpose |
| - | - | - |
| standard_curve_equations | System | IEEE/IEC standard curve coefficients |
| catalog_curve_data | Organization | Manufacturer TCC curve points per catalog entry |
| device_curve_overrides | Project | Instance-level curve modifications |
Device Categories
Protection Devices (protection_devices.device_type)
enum protection_device_type {
circuit_breaker_hv, // Medium/high voltage breakers
circuit_breaker_lv, // Low voltage breakers
fuse, // Fuses
disconnect_switch, // Disconnect switches
load_break_switch, // Load break switches
recloser, // Automatic reclosers
sectionalizer, // Sectionalizers
relay, // Protective relays
surge_arrester, // Surge arresters
overload_relay, // Standalone overload relays
}Motor Control Devices (motor_control_devices.device_type)
enum motor_control_device_type {
vfd, // Variable frequency drives
soft_starter, // Soft starters
contactor, // Contactors
motor_starter, // Combination motor starters
reduced_voltage_starter // Reduced voltage starters
}Device Catalog Categories (device_catalog.device_category)
Text field (not enum) for flexibility:
Protection:
protective_relay, electronic_trip_unit, fuse, breaker_lv, breaker_hv,
overload_relay, standard_curve
Motor Control:
vfd, soft_starter, contactor, motor_starterCatalog Compatibility
The catalog_compatibility table enables M:N relationships between catalog entries, primarily used for breaker frame ↔ trip unit compatibility:
CREATE TABLE catalog_compatibility (
id UUID PRIMARY KEY,
parent_catalog_id UUID REFERENCES device_catalog(id) ON DELETE CASCADE,
child_catalog_id UUID REFERENCES device_catalog(id) ON DELETE CASCADE,
relationship_type TEXT NOT NULL, -- "breaker_trip_unit", "relay_ct", etc.
notes TEXT,
UNIQUE(parent_catalog_id, child_catalog_id, relationship_type)
);Relationship Types:
| Type | Parent Category | Child Category | Description |
| - | - | - | - |
| breaker_trip_unit | breaker_lv | electronic_trip_unit | Which trip units are compatible with a breaker frame |
| relay_ct | protective_relay | - | CT compatibility (future) |
Data Flow for LV Breakers:
protection_devices (LV breaker on canvas)
├── catalog_id → "Siemens 3VA6 250A" (breaker_lv catalog entry)
│
└── device_components
└── catalog_id → "ETU350" (filtered by catalog_compatibility)
│
└── trip_unit_settings (constrained by ETU350 config)When user selects a breaker frame:
protection_devices.catalog_idstores the breaker frame reference- Trip unit dropdown queries
catalog_compatibilityto filter compatible entries - Settings forms derive constraints (frame ratings, delay bands) from catalog configuration
Component Roles
The device_components.component_role field identifies what role the component plays within its parent device:
| Role | Description | Settings Table |
| - | - | - |
| trip_unit | Electronic trip unit in LV breaker | trip_unit_settings |
| thermal_magnetic | Thermal-magnetic trip in LV breaker | thermal_magnetic_settings |
| breaker | Circuit breaker mechanism | hv_breaker_settings (if HV) |
| relay | Protective relay | relay_oc_settings |
| fuse | Fuse element | fuse_settings |
| overload | Thermal/electronic overload relay | overload_relay_settings |
| standard_curve | Standalone standard/equation TCC curve overlay; repeatable per protection device | None; edited through device_curve_overrides |
| built_in_overload | Built-in overload in VFD/soft starter | overload_relay_settings |
| drive_unit | VFD/soft starter main unit | JSONB (device_components.settings) |
| contactor | Contactor in motor starter | JSONB (device_components.settings) |
Data Flow Examples
Example 1: LV Circuit Breaker with Electronic Trip Unit
device_catalog (organization scope)
├── id: "siemens-3va6-250"
├── device_category: "breaker_lv"
├── name: "Siemens 3VA6 250A"
└── configuration: { frameSizes: [100, 160, 250], ... }
device_catalog (organization scope)
├── id: "siemens-etu350"
├── device_category: "electronic_trip_unit"
├── name: "ETU350"
└── configuration: {
│ type: "electronic_trip_unit",
│ frameRatings: [100, 160, 250],
│ functions: {
│ longTime: { delayBands: ["A", "B", "C"] },
│ shortTime: { delayBands: ["0.1", "0.2", "0.3"] }
│ }
│ }
catalog_compatibility
├── parent_catalog_id: "siemens-3va6-250"
├── child_catalog_id: "siemens-etu350"
└── relationship_type: "breaker_trip_unit"
protection_devices (project)
├── id: "cb-001"
├── device_type: "circuit_breaker_lv"
├── catalog_id: "siemens-3va6-250" (→ breaker frame)
├── rated_voltage: 0.48 (kV)
├── rated_current: 400 (A)
├── interrupting_capacity: 65 (kA)
├── continuous_duty_rating: 80 (80% standard, 100% for specially rated)
└── lv_breaker_type: "mccb" (MCCB, PCB, ICCB, or MCB - used when no catalog)
│
└── device_components
├── id: "comp-001"
├── protection_device_id: "cb-001"
├── catalog_id: "siemens-etu350" (→ trip unit, filtered by compatibility)
├── component_role: "trip_unit"
└── setting_type: "current"
│
└── trip_unit_settings
├── device_component_id: "comp-001"
├── frame_amps: 250 ← constrained by catalog config
├── sensor_amps: 400
├── lt_pickup_amps: 360
├── lt_delay_band: "B" ← constrained by catalog config
├── st_pickup_mult: 4.0
├── st_delay_band: "0.2" ← constrained by catalog config
├── inst_enabled: true
└── inst_pickup_mult: 8.0Example 2: LV Circuit Breaker with Thermal-Magnetic Trip
device_catalog (organization scope)
├── id: "square-d-qo"
├── device_category: "breaker_lv"
├── name: "Square D QO"
└── configuration: {
│ type: "thermal_magnetic",
│ tripMode: "integrated", // built-in trip; no compatibility tab
│ availableRatings: [15, 20, 30, 40, 50, 60, 70, 100],
│ magneticType: "adjustable",
│ magneticRange: { min: 5, max: 10, step: 1, unit: "xRating" },
│ thermalType: "fixed"
│ }
catalog_curve_data (organization scope - thermal curves per rating)
├── catalog_id: "square-d-qo"
├── curve_type: "thermal"
├── applies_to_rating: 20
├── curve_points: [[1.35, 3600], [1.5, 120], [2.0, 35], [3.0, 8], [6.0, 0.8]]
└── interpolation: "log_log"
protection_devices (project)
├── id: "cb-002"
├── device_type: "circuit_breaker_lv"
├── catalog_id: "square-d-qo"
├── rated_voltage: 0.12 (kV)
├── rated_current: 20 (A)
├── interrupting_capacity: 10 (kA)
├── continuous_duty_rating: 80 (80% standard, 100% for specially rated)
└── lv_breaker_type: null (derived from catalog when selected)
│
└── device_components
├── id: "comp-005"
├── protection_device_id: "cb-002"
├── catalog_id: "square-d-qo" (same as parent - integrated unit)
└── component_role: "thermal_magnetic"
│
└── thermal_magnetic_settings
├── device_component_id: "comp-005"
├── rated_current: 20 ← selects which catalog curve
├── magnetic_pickup_mult: 8.0 ← user selected within 5-10× range
└── thermal_dial_setting: null ← fixed thermal, no dialExample 3: VFD with Built-in Overload
motor_control_devices (project)
├── id: "vfd-001"
├── device_type: "vfd"
├── rated_voltage: 0.48 (kV)
├── rated_hp: 50
└── rated_kw: 37.3
│
├── device_components (drive_unit)
│ ├── id: "comp-002"
│ ├── motor_control_device_id: "vfd-001"
│ ├── catalog_id: "abb-acs580-073a" (→ device_catalog)
│ ├── component_role: "drive_unit"
│ └── settings: { ← JSONB overflow
│ "accel_time_sec": 10,
│ "decel_time_sec": 15,
│ "control_mode": "vector",
│ "bypass_enabled": false
│ }
│
└── device_components (built_in_overload)
├── id: "comp-003"
├── motor_control_device_id: "vfd-001"
├── catalog_id: null (built-in, not from catalog)
└── component_role: "built_in_overload"
│
└── overload_relay_settings
├── device_component_id: "comp-003"
├── overload_type: "electronic"
├── fla_range_min: 45
├── fla_range_max: 73
├── current_setting: 62
└── trip_class: "10"Example 4: Protective Relay with Shared CT
current_transformers (project)
├── id: "ct-001"
├── project_id: "project-xyz"
├── name: "MAIN-CT"
├── ct_ratio_primary: 400
├── ct_ratio_secondary: 5
└── accuracy_class: "C200"
protection_devices (project)
├── id: "relay-001"
├── device_type: "relay"
├── rated_voltage: 4.16 (kV)
└── rated_current: 400 (A)
│
└── device_components
├── id: "comp-004"
├── protection_device_id: "relay-001"
├── catalog_id: "sel-751" (→ device_catalog)
├── ct_id: "ct-001" (→ current_transformers)
└── component_role: "relay"
│
└── relay_oc_settings
├── device_component_id: "comp-004"
├── ct_primary: 400
├── ct_secondary: 5
├── phase_51_enabled: true
├── phase_51_pickup_amps_sec: 4.0
├── phase_51_pickup_amps_pri: 320
├── phase_51_curve_standard: "IEEE_C37_112"
├── phase_51_curve_type: "very_inverse"
├── phase_51_time_dial: 3.0
├── phase_50_enabled: true
└── phase_50_pickup_amps_pri: 3200Device Catalog Configuration
The device_catalog.configuration JSONB field stores device-specific specs. TypeScript interfaces in device-catalog-schema.ts:
Electronic Trip Unit
interface ElectronicTripUnitConfig {
type: "electronic_trip_unit";
series?: string;
tripUnit?: string;
frameRatings?: number[];
functions: {
longTime?: TripUnitFunction;
shortTime?: TripUnitFunction;
instantaneous?: TripUnitFunction;
groundFault?: TripUnitFunction;
};
}Protective Relay
interface ProtectiveRelayConfig {
type: "protective_relay";
protectionElements: Record<string, ProtectionElement>;
curveEquations?: Record<string, CurveCoefficients>;
ctRatios?: string[];
ptRatios?: string[];
}Fuse
interface FuseConfig {
type: "fuse";
fuseClass?: string; // "J", "RK1", "RK5", "L", "T", "CC"
voltage?: number;
currentRatings?: number[];
interruptingRating?: number;
currentLimiting?: boolean;
timeCurrent?: Record<string, FuseTimeCurrent>;
peakLetThrough?: Record<string, { prospective: number; letThrough: number }[]>;
}VFD
interface VFDConfig {
type: "vfd";
hpRange?: { min: number; max: number };
voltage?: number;
controlModes?: string[]; // "v/hz", "vector", "direct_torque"
builtInProtection?: {
overload?: boolean;
shortCircuit?: boolean;
groundFault?: boolean;
overVoltage?: boolean;
underVoltage?: boolean;
};
rampTimes?: { min: number; max: number; unit: string };
bypassMode?: boolean;
}LV Breaker Frame
interface BreakerLVConfig {
type: "breaker_lv";
breakerType?: "mccb" | "pcb" | "iccb" | "mcb"; // MCCB, PCB (Power CB), ICCB, MCB
series?: string; // e.g., "3VA6", "QO"
frameSizes?: number[]; // Available frame sizes: [100, 250, 400, ...]
ratedVoltage?: number;
interruptingRatings?: VoltageRating[]; // Voltage-dependent kA ratings
poles?: number[]; // [1, 2, 3, 4]
mountingTypes?: string[]; // ["Fixed", "Drawout", "Plug-in"]
continuousDutyRatings?: number[]; // [80] or [80, 100]
features?: {
adjustableTripUnit?: boolean;
groundFault?: boolean;
zsi?: boolean;
communication?: boolean;
};
}LV Breaker Types:
| Type | Full Name | Description |
| - | - | - |
| mccb | Molded Case Circuit Breaker | Most common, 15A-3000A, fixed/drawout |
| pcb | Low-Voltage Power Circuit Breaker | Drawout, 800A-6000A, for main/tie breakers |
| iccb | Insulated Case Circuit Breaker | Hybrid between MCCB and PCB |
| mcb | Miniature Circuit Breaker | Residential/light commercial, 1A-125A |
When a catalog entry has breakerType set, instances using that catalog inherit the type. When no catalog is selected, the instance-level lv_breaker_type column on protection_devices stores the type.
Thermal-Magnetic Breaker
interface ThermalMagneticConfig {
type: "thermal_magnetic";
tripMode?: "integrated" | "separate_trip_unit"; // Defaults to "integrated"
availableRatings?: number[]; // Handle sizes: [15, 20, 30, 40, ...]
continuousDutyRatings?: number[]; // [80] or [80, 100] - 80% standard, 100% specially rated
magneticType: "adjustable" | "fixed" | "toggle";
magneticRange?: { // If adjustable
min: number; // e.g., 5
max: number; // e.g., 10
step?: number;
unit: "xRating"; // Multiplier of rating
};
magneticFixedMult?: number; // If fixed (e.g., 10)
magneticToggleOptions?: string[]; // If toggle (e.g., ["Lo", "Hi"])
thermalType?: "fixed" | "adjustable";
thermalDialRange?: { // If adjustable (rare)
min: number; // e.g., 0.7
max: number; // e.g., 1.0
};
}tripMode distinguishes two mutually exclusive real-world scenarios:
"integrated"(default): Breaker has built-in thermal/magnetic trip (e.g., Square D QO). Magnetic/thermal settings belong on the breaker. No compatibility tab."separate_trip_unit": Breaker is a frame that accepts a separate TM trip unit part number (e.g., Siemens 3VA5 + TM230). Magnetic/thermal behavior is defined by the trip unit catalog entry. Compatibility tab shows only TM trip units.
Existing entries without tripMode default to "integrated" everywhere via ?? "integrated". No DB migration needed since the configuration column is schemaless JSON.
Catalog Configuration Visual Examples
This section provides detailed visual breakdowns of how different breaker types are represented in the catalog system.
Electronic Trip Unit vs Thermal-Magnetic: Key Differences
| Aspect | Electronic Trip Unit | TM Integrated | TM Separate Trip Unit |
| - | - | - | - |
| Catalog entries | 2 (frame + ETU) | 1 (integrated unit) | 2 (frame + TM trip unit) |
| Compatibility table | Yes (frame ↔ ETU) | No | Yes (frame ↔ TM trip unit) |
| Curve source | Calculated from settings | Manufacturer data in catalog_curve_data | Manufacturer data in catalog_curve_data |
| Adjustable zones | LT, ST, INST, GF | INST only (usually) | INST only (usually) |
| Settings table | trip_unit_settings | thermal_magnetic_settings | thermal_magnetic_settings |
| Magnetic/thermal config | On ETU catalog entry | On breaker catalog entry | On TM trip unit catalog entry |
AI Parsing for Electronic Trip Units
When AI parses ETU settings from PDFs:
- Model identification: AI uses the trip unit model name (e.g., "ETU350", "Ekip Touch"), NOT the breaker frame number (e.g., "3VA6210")
- Frame size handling: AI returns separate entries per frame size, all with the same
modelNumber - Automatic merging:
device-catalog/new/page.tsxmerges entries by(deviceType, manufacturer, modelNumber)into a single catalog entry with multipleframeRatings
Example: PDF with Siemens ETU350 settings for 100A and 250A frames:
- AI returns: 2 entries with
modelNumber: "ETU350",frameAmps: 100andframeAmps: 250 - Merge produces: 1 catalog entry with
frameRatings: [100, 250]and separateframeConfigsper size
The merge logic is extensible via MERGEABLE_DEVICE_TYPES in device-catalog/new/page.tsx.
Electronic Trip Unit Catalog Structure
Electronic trip units require two catalog entries linked via catalog_compatibility:
┌──────────────────────────────────────────────────────────────────────────────────────┐
│ DEVICE CATALOG (Organization Scope) │
│ │
│ device_catalog (breaker frame) device_catalog (trip unit) │
│ ┌──────────────────────────┐ ┌────────────────────────────────┐ │
│ │ id: "ab-140g-frame" │ │ id: "ab-140g-kth-lsi" │ │
│ │ category: breaker_lv │ │ category: electronic_trip_unit │ │
│ │ manufacturer: "Allen- │ │ manufacturer: "Allen-Bradley" │ │
│ │ Bradley" │ │ name: "140G-KTH LSI" │ │
│ │ name: "140G Frame" │ │ │ │
│ │ │ catalog │ configuration: { ──────────────────┐ │
│ │ configuration: { │ compatibility│ ...see below │ │ │
│ │ type: "breaker_lv", │◄────────────►│ } │ │ │
│ │ frameSizes: [300, 400] │ └────────────────────────────────┘ │ │
│ │ } │ │ │
│ └──────────────────────────┘ │ │
│ │ │
│ catalog_compatibility │ │
│ ┌────────────────────────────────┐ │ │
│ │ parent: "ab-140g-frame" │ │ │
│ │ child: "ab-140g-kth-lsi" │ │ │
│ │ type: "breaker_trip_unit" │ │ │
│ └────────────────────────────────┘ │ │
│ │ │
└───────────────────────────────────────────────────────────────────────────────────┘ │
│
┌───────────────────────────────────────────────────────────────────────────────────────┘
│
│ TRIP UNIT CONFIGURATION (example: Allen-Bradley 140G-KTH LSI)
│
│ Based on datasheet Table 17 - Trip Units, Electronic LSI:
│ ┌──────────────────────────────────────────────────────────────────────────────┐
│ │ Rated Current Iₙ [A] │ L (Long Time) │ S (Short Time) │ I │
│ │ │ I₁=0.4-1×Iₙ t₁=s │ I₂=1-10×Iₙ t₂=s │ I₃=1-10×Iₙ │
│ ├──────────────────────┼─────────────────────┼───────────────────┼────────────┤
│ │ 300 │ 120-300 3,6,9,18 │ 180-3000 0.05- │ 450-3600 │
│ │ 400 │ 160-400 │ 240-4000 0.5 │ 600-4800 │
│ └──────────────────────────────────────────────────────────────────────────────┘
│
│ configuration: {
│ type: "electronic_trip_unit",
│ protectionType: "LSI", ◄── Long, Short, Instantaneous (no GF)
│
│ frameRatings: [300, 400], ◄── Rated Current Iₙ [A]
│
│ functions: {
│ ┌─────────────────────────────────────────────────────────────────────────┐
│ │ longTime: { ◄── "L" column │
│ │ pickupRange: { min: 0.4, max: 1.0, unit: "xIn" }, ◄── I₁ = 0.4-1×Iₙ │
│ │ delayBands: ["3", "6", "9", "18"] ◄── t₁ = s (discrete values) │
│ │ } │
│ └─────────────────────────────────────────────────────────────────────────┘
│ ┌─────────────────────────────────────────────────────────────────────────┐
│ │ shortTime: { ◄── "S" column │
│ │ pickupRange: { min: 1, max: 10, unit: "xIn" }, ◄── I₂ = 1-10×Iₙ │
│ │ delayBands: ["0.05", "0.1", "0.25", "0.5"] ◄── t₂ = s │
│ │ } │
│ └─────────────────────────────────────────────────────────────────────────┘
│ ┌─────────────────────────────────────────────────────────────────────────┐
│ │ instantaneous: { ◄── "I" column │
│ │ pickupRange: { min: 1, max: 10, unit: "xIn" }, ◄── I₃ = 1-10×Iₙ │
│ │ delayBands: null ◄── No delay (instant) │
│ │ } │
│ └─────────────────────────────────────────────────────────────────────────┘
│
│ groundFault: null ◄── LSI = no GF (LSIG has it)
│ }
│ }
│
│ NO CURVE DATA NEEDED - curves are calculated from settings + standard equations
│
└──────────────────────────────────────────────────────────────────────────────────────Derived Values for UI: When user selects frameRating = 300A:
| Zone | Pickup Range | Delay Options |
| - | - | - |
| Long Time | 0.4 × 300 ... 1.0 × 300 = 120A - 300A | 3s, 6s, 9s, 18s |
| Short Time | 1 × 300 ... 10 × 300 = 300A - 3000A | 0.05s, 0.1s, 0.25s, 0.5s |
| Instantaneous | 1 × 300 ... 10 × 300 = 300A - 3000A | none (instant) |
Thermal-Magnetic Breaker Catalog Structure
Thermal-magnetic breakers have two modes controlled by tripMode:
"integrated"(default): Single catalog entry with built-in magnetic/thermal settings. No compatibility tab. Example: Square D QO."separate_trip_unit": Breaker frame catalog entry + compatible TM trip unit catalog entry (e.g., TM210/TM230/TM240) linked viacatalog_compatibility. Magnetic/thermal settings come from the trip unit; the breaker's own magnetic/thermal fields are ignored by the resolver.
Resolver output combines breaker config + trip-unit config + placed device rated current:
┌──────────────────────────────────────────────────────────────────────────────────────┐
│ DEVICE CATALOG (Organization Scope) │
│ │
│ device_catalog (breaker frame parent; compatibility to TM trip units) │
│ ┌─────────────────────────────────┐ │
│ │ id: "square-d-qo" │ │
│ │ category: breaker_lv │ │
│ │ manufacturer: "Square D" │ │
│ │ name: "QO Series" │ │
│ │ │ │
│ │ configuration: { │ │
│ │ type: "thermal_magnetic", │ │
│ │ tripMode: "integrated", │ ◄── or "separate_trip_unit" (default: int.) │
│ │ │ │
│ │ availableRatings: [ │ ◄── Which handle sizes exist │
│ │ 15, 20, 30, 40, 50, 60, │ │
│ │ 70, 100 │ │
│ │ ], │ │
│ │ │ │
│ │ magneticType: "adjustable", │ ◄── "adjustable" | "fixed" | "toggle" │
│ │ magneticRange: { │ │
│ │ min: 5, │ ◄── Instantaneous pickup range │
│ │ max: 10, │ (multiples of rating) │
│ │ step: 1, │ │
│ │ unit: "xRating" │ │
│ │ }, │ │
│ │ │ │
│ │ thermalType: "fixed", │ ◄── Most TM breakers = fixed thermal │
│ │ thermalDialRange: null │ (some industrial have adjustable dial) │
│ │ } │ │
│ └─────────────────────────────────┘ │
│ │ │
│ │ catalog_id (FK) │
│ ▼ │
│ catalog_curve_data (one row per rating - stores THERMAL curve only) │
│ ┌─────────────────────────────────┐ │
│ │ catalog_id: "square-d-qo" │ │
│ │ curve_type: "thermal" │ ◄── The fixed thermal trip curve │
│ │ applies_to_rating: 20 │ ◄── For 20A handle │
│ │ curve_points: [ │ │
│ │ [1.35, 3600], ← 135% = 1hr │ │
│ │ [1.50, 120], ← 150% = 2min │ ◄── Manufacturer TCC data points │
│ │ [2.00, 35], ← 200% = 35s │ [multiple of rating, seconds] │
│ │ [3.00, 8], ← 300% = 8s │ │
│ │ [4.00, 3], │ │
│ │ [6.00, 0.8] │ │
│ │ ], │ │
│ │ interpolation: "log_log" │ │
│ └─────────────────────────────────┘ │
│ ┌─────────────────────────────────┐ │
│ │ catalog_id: "square-d-qo" │ │
│ │ curve_type: "thermal" │ │
│ │ applies_to_rating: 30 │ ◄── Different curve for 30A handle │
│ │ curve_points: [...] │ │
│ └─────────────────────────────────┘ │
│ ┌─────────────────────────────────┐ │
│ │ ...one row per available rating │ │
│ └─────────────────────────────────┘ │
│ │
│ MAGNETIC INSTANTANEOUS is NOT in curve data - it's a vertical line calculated │
│ from selected multiplier/toggle (or manual override) in thermal_magnetic_settings │
│ │
└───────────────────────────────────────────────────────────────────────────────────────┘Instance Settings from Catalog Configuration
When a user places an electronic trip unit breaker on the canvas and opens settings, the form constraints come from the catalog configuration:
┌─────────────────────────────────────────────────────────────────────────┐
│ PROJECT SCOPE (Instance Settings) │
│ │
│ trip_unit_settings (for electronic trip unit) │
│ ┌────────────────────────────────────────────────────────────────────┐ │
│ │ device_component_id: "comp-xyz" │ │
│ │ │ │
│ │ frame_amps: 300 ◄── Dropdown: [300, 400] from config │ │
│ │ sensor_amps: 300 │ │
│ │ │ │
│ │ lt_pickup_amps: 240 ◄── Slider: 120-300A (0.4-1.0× frame) │ │
│ │ lt_delay_band: "6" ◄── Dropdown: ["3","6","9","18"] │ │
│ │ │ │
│ │ st_pickup_mult: 4.0 ◄── Slider: 1-10× from config │ │
│ │ st_delay_band: "0.1" ◄── Dropdown: ["0.05","0.1","0.25"...] │ │
│ │ │ │
│ │ inst_enabled: true │ │
│ │ inst_pickup_mult: 6.0 ◄── Slider: 1-10× from config │ │
│ │ │ │
│ │ gf_enabled: false ◄── Disabled (LSI config has no GF) │ │
│ └────────────────────────────────────────────────────────────────────┘ │
│ │
│ thermal_magnetic_settings (for thermal-magnetic breaker) │
│ ┌────────────────────────────────────────────────────────────────────┐ │
│ │ device_component_id: "comp-abc" │ │
│ │ │ │
│ │ rated_current: 20 ◄── Dropdown: [15,20,30...] from config│ │
│ │ (selects which catalog curve) │ │
│ │ │ │
│ │ magnetic_pickup_mult: 8.0 ◄── Slider: 5-10× from magneticRange │ │
│ │ │ │
│ │ thermal_dial_setting: null ◄── Hidden (thermalType: "fixed") │ │
│ └────────────────────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────────────┘Time-Current Curve System
┌─────────────────────────────────────────────────────────────────┐
│ CURVE DATA HIERARCHY │
├─────────────────────────────────────────────────────────────────┤
│ │
│ standard_curve_equations (system-wide) │
│ ├── IEEE C37.112 curves: t = (A / (M^P - 1) + B) * TD │
│ └── IEC 60255 curves: t = (K / (M^α - 1)) * TD │
│ │
│ ▼ │
│ │
│ catalog_curve_data (per catalog entry) │
│ ├── Manufacturer TCC points: [[mult, time], ...] │
│ ├── Curve types: minimum_melt, total_clear, long_time, etc. │
│ └── Interpolation: log_log, log_linear, linear │
│ │
│ ▼ │
│ │
│ device_curve_overrides (per device_component instance) │
│ ├── Override types: replace, shift_time, shift_current │
│ ├── Replacement curve points │
│ └── Multipliers for time/current shifts │
│ │
└─────────────────────────────────────────────────────────────────┘Standard Curve Equations
Pre-seeded IEEE and IEC standard curves:
| Standard | Curve Type | Coefficients |
| - | - | - |
| IEEE_C37_112 | moderately_inverse | A=0.0515, B=0.114, P=0.02 |
| IEEE_C37_112 | very_inverse | A=19.61, B=0.491, P=2.0 |
| IEEE_C37_112 | extremely_inverse | A=28.2, B=0.1217, P=2.0 |
| IEC_60255 | standard_inverse | K=0.14, α=0.02 |
| IEC_60255 | very_inverse | K=13.5, α=1.0 |
| IEC_60255 | extremely_inverse | K=80.0, α=2.0 |
Catalog Curve Data
Stores manufacturer-specific TCC curve points:
{
"curve_type": "minimum_melt",
"applies_to_rating": 100,
"curve_points": [
[1.5, 1000],
[2.0, 100],
[3.0, 20],
[6.0, 4],
[10.0, 1],
[20.0, 0.1]
],
"current_basis": "ampere_rating",
"interpolation": "log_log"
}Server Actions
Device Catalog
| Action | Purpose |
| - | - |
| getDeviceCatalogAction | Get catalog entry by ID |
| listDeviceCatalogAction | List entries (filter by category, org) |
| createDeviceCatalogAction | Create org-specific catalog entry |
| updateDeviceCatalogAction | Update catalog entry |
| deleteDeviceCatalogAction | Delete catalog entry |
Catalog Compatibility
| Action | Purpose |
| - | - |
| listCompatibleChildrenAction | List compatible child entries for a parent catalog entry |
| listCompatibleParentsAction | List compatible parent entries for a child catalog entry |
| addCompatibilityAction | Create a compatibility relationship |
| removeCompatibilityAction | Remove a compatibility relationship |
| batchAddCompatibilityAction | Add multiple compatibility relationships |
Catalog Curve Data
| Action | Purpose |
| - | - |
| listCatalogCurveDataAction | List TCC curve data for a catalog entry |
| createCatalogCurveDataAction | Create curve data entry |
| updateCatalogCurveDataAction | Update curve data entry |
| deleteCatalogCurveDataAction | Delete curve data entry |
| upsertCatalogCurveDataAction | Upsert curve data (replace all for catalog entry) |
Device Components
| Action | Purpose |
| - | - |
| listProtectionDeviceComponentsAction | List components for protection device |
| listMotorControlDeviceComponentsAction | List components for motor control device |
| getDeviceComponentAction | Get component by ID |
| getProtectionDeviceComponentByRoleAction | Get component by role |
| createProtectionDeviceComponentAction | Create component for protection device |
| createMotorControlDeviceComponentAction | Create component for motor control device |
| updateDeviceComponentAction | Update component |
| deleteDeviceComponentAction | Delete component |
Settings Tables
Each settings table has standard CRUD actions following the pattern:
get{Type}SettingsAction(deviceComponentId)
create{Type}SettingsAction(data)
update{Type}SettingsAction(id, updates)
delete{Type}SettingsAction(id)RLS (Row Level Security)
Tables with RLS enabled
| Table | Policy Basis |
| - | - |
| device_catalog | Organization membership (or system default) |
| protection_devices | Project access |
| motor_control_devices | Project access |
| current_transformers | Project access |
Tables without direct RLS (rely on FK cascade)
- device_components (access via protection_devices/motor_control_devices)
- trip_unit_settings
- relay_oc_settings
- fuse_settings
- overload_relay_settings
- hv_breaker_settings
- catalog_curve_data
- device_curve_overrides
Design Decisions
1. Composition over Dedicated Tables for VFD/Soft Starter Settings
Decision: No drive_settings table. VFDs/soft starters use motor_control_devices + child device_components for built-in protection.
Rationale:
- Built-in overload reuses
overload_relay_settingstable (no duplication) - Drive-specific settings (ramp times, bypass mode) go in
device_components.settingsJSONB - Matches real-world structure (VFD contains multiple protection functions)
- Enables TCC curves per protection element
2. Device Components as Junction Table
Decision: device_components links devices to catalog and settings with mutually exclusive parent FKs.
Rationale:
- Single table handles both protection and motor control devices
- CHECK constraint ensures exactly one parent
- Allows multiple components per device (e.g., trip unit + breaker mechanism)
- Flexible for future device types
3. JSONB Overflow Settings
Decision: device_components.settings JSONB for settings that don't warrant dedicated columns.
Rationale:
- Avoids table proliferation for rarely-used fields
- Extensible without migrations
- Drive-specific settings (ramp times, control modes) naturally fit JSONB
- Typed with TypeScript interfaces at runtime
4. Shared Current Transformers
Decision: Project-scoped current_transformers table linked via optional FK.
Rationale:
- Multiple relays may share the same physical CT
- Avoids duplicating CT data
- Supports CT-less devices (built-in sensing in LV breakers)
5. Thermal-Magnetic Breakers: Catalog Curves + Typed Settings
Decision: Thermal curve shape data stays in catalog_curve_data (organization scope). Instance-level thermal/magnetic selections and optional manual pickup overrides are stored in thermal_magnetic_settings (project scope).
Rationale:
- Thermal curve shape/error-band is manufacturer-defined and reusable across instances - fits
catalog_curve_data - Instance-level edits are setpoints/dials (thermal + magnetic), not curve geometry
- Optional manual amps overrides are persisted per placed device component
- Avoids duplicating curve geometry across every breaker instance
- Typed
thermal_magnetic_settingstable (vs JSONB) because thermal-magnetic breakers are common in LV distribution - Settings form derives valid option sets from catalog configuration and resolver output
Key Files
Schema
| File | Tables |
| - | - |
| device-catalog-schema.ts | device_catalog + TypeScript config interfaces |
| catalog-compatibility-schema.ts | catalog_compatibility (M:N linking) |
| device-components-schema.ts | device_components |
| protection-devices-schema.ts | protection_devices (includes catalog_id FK for breaker frame) |
| motor-control-devices-schema.ts | motor_control_devices |
| trip-unit-settings-schema.ts | trip_unit_settings |
| thermal-magnetic-settings-schema.ts | thermal_magnetic_settings |
| relay-oc-settings-schema.ts | relay_oc_settings |
| fuse-settings-schema.ts | fuse_settings |
| overload-relay-settings-schema.ts | overload_relay_settings |
| hv-breaker-settings-schema.ts | hv_breaker_settings |
| current-transformers-schema.ts | current_transformers |
| standard-curve-equations-schema.ts | standard_curve_equations |
| catalog-curve-data-schema.ts | catalog_curve_data |
| device-curve-overrides-schema.ts | device_curve_overrides |
Server Actions
| File | Purpose |
| - | - |
| device-catalog-actions.ts | Catalog CRUD |
| catalog-compatibility-actions.ts | Compatibility relationship management |
| catalog-curve-data-actions.ts | TCC curve data CRUD |
| device-components-actions.ts | Component linking |
| protection-devices-actions.ts | Protection device CRUD |
| motor-control-devices-actions.ts | Motor control device CRUD |
| trip-unit-settings-actions.ts | Trip unit settings |
| thermal-magnetic-settings-actions.ts | Thermal-magnetic settings |
| thermal-magnetic-resolver-actions.ts | Resolve TM valid options from breaker + trip-unit catalogs |
| thermal-magnetic-editor-state-actions.ts | Single-query TM editor state (resolved options + persisted overlay + orphan detection) |
| relay-oc-settings-actions.ts | Relay OC settings |
| fuse-settings-actions.ts | Fuse settings |
| overload-relay-settings-actions.ts | Overload relay settings |
| hv-breaker-settings-actions.ts | HV breaker settings |
UI Components (Device Catalog)
| File | Purpose |
| - | - |
| device-catalog/_components/device-catalog-sheet.tsx | Main catalog editor (Sheet with tabs) |
| device-catalog/_components/config-forms/*.tsx | Category-specific configuration forms |
| device-catalog/_components/compatibility-editor.tsx | Assign compatible trip units to breakers |
| device-catalog/_components/curve-editors/curve-points-editor.tsx | TCC curve data entry |
| device-catalog/_components/inputs/range-input.tsx | Numeric range input (min/max/step/unit) |
| device-catalog/_components/inputs/array-input.tsx | Numeric and string array inputs |
UI Components (Device Settings on Canvas)
| File | Purpose |
| - | - |
| sld-editor/.../device-settings/index.tsx | Device settings orchestrator |
| sld-editor/.../device-settings/breaker-frame-selector.tsx | Breaker frame dropdown (updates protection_devices.catalog_id) |
| sld-editor/.../device-settings/device-model-selector.tsx | Trip unit/relay/fuse dropdown (filters by compatibility) |
| sld-editor/.../device-settings/trip-unit-settings-form.tsx | ETU settings form + TM routing wrapper |
| sld-editor/.../device-settings/thermal-magnetic-settings-form.tsx | Editable TM settings form (discrete selectors + manual override + local preview) |
| sld-editor/.../device-settings/fuse-settings-form.tsx | Fuse settings (constrained by catalog config) |
| sld-editor/.../device-settings/relay-oc-settings-form.tsx | Relay OC settings (constrained by catalog config) |
Common Patterns
Settings Form Constraints
Settings forms derive their options from the catalog configuration. This ensures users can only select values appropriate for the selected device model.
Data Flow:
deviceComponent.catalog.configuration
│
▼
Settings Form (e.g., TripUnitSettingsForm)
│
├── frameRatings: config.frameRatings ?? DEFAULT_FRAME_RATINGS
├── ltDelayBands: config.functions?.longTime?.delayBands ?? DEFAULT_DELAY_BANDS
├── stDelayBands: config.functions?.shortTime?.delayBands ?? DEFAULT_DELAY_BANDS
└── gfDelayBands: config.functions?.groundFault?.delayBands ?? DEFAULT_DELAY_BANDSImplementation Pattern:
// In settings form component
const typedConfig = useMemo(() => {
if (!catalogConfig || catalogConfig.type !== "electronic_trip_unit") {
return {};
}
return catalogConfig as Partial<ElectronicTripUnitConfig>;
}, [catalogConfig]);
// Derive constrained options
const frameRatings = typedConfig.frameRatings ?? DEFAULT_FRAME_RATINGS;
const ltDelayBands = typedConfig.functions?.longTime?.delayBands ?? DEFAULT_DELAY_BANDS;
// Use in Select component
<Select value={formData.frameAmps}>
{frameRatings.map((rating) => (
<SelectItem key={rating} value={rating.toString()}>{rating}A</SelectItem>
))}
</Select>Fallback Behavior: When no catalog is selected or the catalog has no configuration, forms show generic defaults. This supports quick prototyping without requiring full catalog setup.
Creating a Protection Device with Settings
// 1. Create protection device
const device = await createProtectionDeviceAction({
projectId,
name: "CB-001",
deviceType: "circuit_breaker_lv",
ratedVoltage: 0.48,
ratedCurrent: 400,
positionX: 100,
positionY: 200,
});
// 2. Create device component linking to catalog
const component = await createProtectionDeviceComponentAction(device.data.id, {
catalogId: "siemens-3va6-etu350",
componentRole: "trip_unit",
settingType: "current",
});
// 3. Create trip unit settings
const settings = await createTripUnitSettingsAction({
deviceComponentId: component.data.id,
frameAmps: 400,
sensorAmps: 400,
ltPickupAmps: 360,
ltDelayBand: "B",
stPickupMult: 4.0,
instEnabled: true,
instPickupMult: 8.0,
});Fetching Device with Full Settings
// Get component with catalog and CT relations
const components = await listProtectionDeviceComponentsAction(deviceId);
// Returns: [{ id, componentRole, catalog: {...}, ct: {...}, ... }]
// Get settings by component ID
const tripUnitSettings = await getTripUnitSettingsAction(componentId);