import { isValidCircuitProtectionRecord } from "../../shared/validation/protection-device.schemas.js"; import type { BreakerTripCharacteristic, CircuitProtectionDeviceType, FuseUtilizationCategory, RcdType, } from "../../shared/constants/protection-device.js"; import type { SerializedProjectCommand } from "./project-command.model.js"; export const circuitProtectionUpdateCommandType = "circuit-protection.update" as const; export const circuitProtectionUpdateCommandSchemaVersion = 1 as const; // type "NONE" records an explicit "no protection device" decision; all // values are then null. A missing snapshot (null) means "not yet planned". export interface CircuitProtectionSnapshot { circuitId: string; type: CircuitProtectionDeviceType; ratedCurrentA: number | null; fuseUtilizationCategory: FuseUtilizationCategory | null; tripCharacteristic: BreakerTripCharacteristic | null; rcdType: RcdType | null; ratedResidualCurrentMa: number | null; } interface CircuitProtectionUpdatePayload { circuitId: string; expected: CircuitProtectionSnapshot | null; target: CircuitProtectionSnapshot | null; } export interface CircuitProtectionUpdateProjectCommand extends SerializedProjectCommand { schemaVersion: typeof circuitProtectionUpdateCommandSchemaVersion; type: typeof circuitProtectionUpdateCommandType; } export function createCircuitProtectionUpdateProjectCommand( circuitId: string, expected: CircuitProtectionSnapshot | null, target: CircuitProtectionSnapshot | null ): CircuitProtectionUpdateProjectCommand { const command: CircuitProtectionUpdateProjectCommand = { schemaVersion: circuitProtectionUpdateCommandSchemaVersion, type: circuitProtectionUpdateCommandType, payload: { circuitId, expected, target }, }; assertCircuitProtectionUpdateProjectCommand(command); return command; } export function assertCircuitProtectionUpdateProjectCommand( command: SerializedProjectCommand ): asserts command is CircuitProtectionUpdateProjectCommand { if ( command.schemaVersion !== circuitProtectionUpdateCommandSchemaVersion || command.type !== circuitProtectionUpdateCommandType || !isPlainObject(command.payload) || Object.keys(command.payload).length !== 3 ) { throw new Error("Unsupported circuit-protection update command."); } const { circuitId, expected, target } = command.payload; if (typeof circuitId !== "string" || !circuitId.trim()) { throw new Error("Circuit protection requires a circuit id."); } if (expected === null && target === null) { throw new Error("Circuit protection update must change state."); } if (expected !== null) { assertCircuitProtectionSnapshot(expected, circuitId); } if (target !== null) { assertCircuitProtectionSnapshot(target, circuitId); } if (circuitProtectionSnapshotsEqual(expected, target)) { throw new Error("Circuit protection update must change state."); } } function circuitProtectionSnapshotsEqual( left: CircuitProtectionSnapshot | null, right: CircuitProtectionSnapshot | null ): boolean { if (left === null || right === null) { return left === right; } return ( left.circuitId === right.circuitId && left.type === right.type && left.ratedCurrentA === right.ratedCurrentA && left.fuseUtilizationCategory === right.fuseUtilizationCategory && left.tripCharacteristic === right.tripCharacteristic && left.rcdType === right.rcdType && left.ratedResidualCurrentMa === right.ratedResidualCurrentMa ); } export function assertCircuitProtectionSnapshot( value: unknown, circuitId: string ): asserts value is CircuitProtectionSnapshot { if ( !isPlainObject(value) || Object.keys(value).length !== 7 || value.circuitId !== circuitId ) { throw new Error("Circuit-protection snapshot is invalid."); } if (!isValidCircuitProtectionRecord(value)) { throw new Error("Circuit-protection configuration is invalid."); } } function isPlainObject(value: unknown): value is Record { return value !== null && typeof value === "object" && !Array.isArray(value); }