diff --git a/tests/unit/handlers/coolingSystem.handlers.test.js b/tests/unit/handlers/coolingSystem.handlers.test.js index 1691645..cfda0f5 100644 --- a/tests/unit/handlers/coolingSystem.handlers.test.js +++ b/tests/unit/handlers/coolingSystem.handlers.test.js @@ -403,7 +403,8 @@ test('buildMinersCircuit1View - builds view from enriched equipment', (t) => { t.ok(view.lines, 'should have lines') t.is(view.lines.length, 2, 'should have 2 lines') t.ok(view.pumps, 'should have pumps') - t.is(view.pumps.length, 3, 'should have 3 miner loop pumps') + t.is(view.pumps.length, 2, 'should have 2 miner loop pumps (makeup pump excluded)') + t.absent(view.pumps.find(p => p.id === 'B-7515'), 'makeup pump not in pumps array') // Check enriched data with units t.ok(view.pumps[0].speed.unit, 'pump speed should have unit') t.ok(view.pumps[0].current.unit, 'pump current should have unit') @@ -688,6 +689,50 @@ test('buildMinersCircuit1View - control_valves null when config has none', (t) = t.pass() }) +test('buildMinersCircuit1View - makeup system from global config', (t) => { + const equipment = createMockEquipment() + const config = createMockConfig() + config.cooling_system.makeup = { + tank: 'TQ-7501', + level_sensor: 'LIT-7503', + level_control_valve: 'LCV-7501', + on_off_valves: ['LCV-7501', 'LCV-7502'], + pump: 'B-7515', + defaults: { + tank_volume: { value: 0.5, unit: 'm³' }, + pump_head: { value: 40, unit: 'm.c.a' }, + pump_flow: { value: 2, unit: 'm³/h' } + } + } + const view = buildMinersCircuit1View(equipment, config) + + t.ok(view.makeup, 'should have makeup system') + t.is(view.makeup.tank.id, 'TQ-7501', 'tank id from config') + t.is(view.makeup.tank.description, 'Make-Up Tank (0.5 m³)', 'tank description carries volume') + t.alike(view.makeup.tank.volume, { value: 0.5, unit: 'm³' }, 'tank volume from defaults') + t.is(view.makeup.tank.level.value, 76, 'tank level from level sensor') + t.is(view.makeup.pump.id, 'B-7515', 'makeup pump id from config') + t.is(view.makeup.pump.description, 'Make-Up Pump', 'pump description') + t.is(view.makeup.pump.status, 'Standby', 'pump status from equipment') + t.alike(view.makeup.pump.rated_head, { value: 40, unit: 'm.c.a' }, 'pump rated head from defaults') + t.alike(view.makeup.pump.rated_flow, { value: 2, unit: 'm³/h' }, 'pump rated flow from defaults') + t.is(view.makeup.level_control_valve.id, 'LCV-7501', 'level control valve id') + t.is(view.makeup.on_off_valves.length, 2, 'on/off valves from config') + t.absent(view.pumps.find(p => p.id === 'B-7515'), 'makeup pump excluded from pumps array') + t.pass() +}) + +test('buildMinersCircuit1View - makeup tank description without volume', (t) => { + const equipment = createMockEquipment() + const config = createMockConfig() + config.cooling_system.makeup = { tank: 'TQ-7501', level_sensor: 'LIT-7503' } + const view = buildMinersCircuit1View(equipment, config) + + t.is(view.makeup.tank.description, 'Make-Up Tank', 'description without volume suffix') + t.is(view.makeup.pump, null, 'no pump when not configured') + t.pass() +}) + test('buildMinersCircuit1View - pumps include label and has_interlock', (t) => { const equipment = createMockEquipment() const config = createMockConfig() diff --git a/workers/lib/constants.js b/workers/lib/constants.js index 6551bd0..af136cc 100644 --- a/workers/lib/constants.js +++ b/workers/lib/constants.js @@ -461,8 +461,10 @@ const COOLING_SYSTEM_PROJECTIONS = { 'last.snap.stats.dcs_specific.equipment.temperatures': 1, 'last.snap.stats.dcs_specific.equipment.pressures': 1, 'last.snap.stats.dcs_specific.equipment.flows': 1, + 'last.snap.stats.dcs_specific.equipment.levels': 1, 'last.snap.stats.dcs_specific.equipment.heat_exchangers': 1, 'last.snap.stats.dcs_specific.equipment.valves': 1, + 'last.snap.stats.dcs_specific.equipment.tanks': 1, 'last.snap.config.cooling_system': 1 }, circuit2: { diff --git a/workers/lib/server/handlers/cooling.system.handlers.js b/workers/lib/server/handlers/cooling.system.handlers.js index ca2d51c..68b742d 100644 --- a/workers/lib/server/handlers/cooling.system.handlers.js +++ b/workers/lib/server/handlers/cooling.system.handlers.js @@ -77,6 +77,61 @@ function buildVibrationSwitch (vibrationSwitches, switchTag) { return { tag: switchTag, state: sw?.state ?? null } } +function buildMakeupSystem (equipment, config, circuitMakeupConfig) { + const { pumps, levels, valves, tanks } = equipment + const makeupConfig = { ...(config?.cooling_system?.makeup || {}), ...(circuitMakeupConfig || {}) } + + const makeupTankId = makeupConfig.tank || tanks?.[0]?.equipment + const makeupLevelValve = valves?.find(v => v.equipment === makeupConfig.level_control_valve) + const makeupOnOffValves = makeupConfig.on_off_valves || [] + const makeupPumpId = makeupConfig.pump || null + const makeupPump = makeupPumpId ? (pumps || []).find(p => p.equipment === makeupPumpId) : null + const tankVolume = makeupConfig.defaults?.tank_volume || null + + return { + tank: { + id: makeupTankId, + name: makeupTankId, + description: tankVolume?.value != null + ? `Make-Up Tank (${tankVolume.value} ${tankVolume.unit})` + : 'Make-Up Tank', + volume: tankVolume, + level: getSensorReading(levels, makeupConfig.level_sensor), + level_sensor: makeupConfig.level_sensor + }, + pump: makeupPumpId + ? { + id: makeupPumpId, + name: makeupPumpId, + description: 'Make-Up Pump', + status: makeupPump?.status ?? null, + is_running: makeupPump?.fbk_run_out || false, + speed: makeupPump?.speed ?? null, + current: makeupPump?.current ?? null, + rated_head: makeupConfig.defaults?.pump_head || null, + rated_flow: makeupConfig.defaults?.pump_flow || null + } + : null, + level_control_valve: makeupConfig.level_control_valve + ? { + id: makeupConfig.level_control_valve, + type: makeupLevelValve?.type || null, + description: makeupLevelValve?.description || null, + position: makeupLevelValve?.position + } + : null, + on_off_valves: makeupOnOffValves.map(vid => { + const valve = valves?.find(v => v.equipment === vid) + return { + id: vid, + type: valve?.type || null, + position: valve?.position, + is_open: valve?.is_open ?? valve?.position?.value > 50 + } + }) + } +} + function buildGroupDifferentialPressure (lineConfig, pressures) { const groupSensors = lineConfig.group_pressure_sensors || {} // Absolute group number (line1 -> 1-8, line2 -> 9-16), parsed from the @@ -289,7 +344,11 @@ function buildMinersCircuit1View (equipment, config) { } } - const formattedPumps = filterPumpsByCircuit(pumps, 'MINER_LOOP').map(formatPump) + const makeupSystem = buildMakeupSystem(equipment, config, coolingConfig.makeup) + + const formattedPumps = filterPumpsByCircuit(pumps, 'MINER_LOOP') + .filter(p => p.equipment !== makeupSystem.pump?.id) + .map(formatPump) return { title: coolingConfig.name || viewConfig.title, @@ -311,6 +370,7 @@ function buildMinersCircuit1View (equipment, config) { pumps_config: coolingConfig.defaults?.pumps_config || null, lines, control_valves: Object.keys(controlValves).length > 0 ? controlValves : null, + makeup: makeupSystem, pumps: formattedPumps } } @@ -323,10 +383,8 @@ function buildMinersCircuit2View (equipment, config) { const coolingTowers = (equipment.cooling_towers || []).filter(ct => ct.circuit === 'COOLING_TOWER') const vibrationSwitches = equipment.vibration_switches const valves = equipment.valves - const tanks = equipment.tanks const towerConfig = config?.cooling_system?.cooling_tower_loop || {} const minerLoopConfig = config?.cooling_system?.miner_loop || {} - const makeupGlobalConfig = config?.cooling_system?.makeup || {} const viewConfig = config?.cooling_system?.view_metadata?.miners?.circuit2 || {} // Build HX → groups mapping from miner_loop line configs @@ -422,53 +480,11 @@ function buildMinersCircuit2View (equipment, config) { })) // Makeup water system - const makeupConfig = towerConfig.makeup || {} - const makeupTankId = makeupConfig.tank || tanks?.[0]?.equipment - const makeupLevelValve = valves?.find(v => v.equipment === makeupConfig.level_control_valve) - const makeupOnOffValves = makeupConfig.on_off_valves || [] - const makeupPumpId = makeupGlobalConfig.pump || null - const makeupPump = makeupPumpId ? (pumps || []).find(p => p.equipment === makeupPumpId) : null - - const makeupSystem = { - tank: { - id: makeupTankId, - name: makeupTankId, - volume: makeupGlobalConfig.defaults?.tank_volume || null, - level: getSensorReading(levels, makeupConfig.level_sensor), - level_sensor: makeupConfig.level_sensor - }, - pump: makeupPumpId - ? { - id: makeupPumpId, - name: makeupPumpId, - status: makeupPump?.status ?? null, - is_running: makeupPump?.fbk_run_out || false, - speed: makeupPump?.speed ?? null, - current: makeupPump?.current ?? null, - rated_head: makeupGlobalConfig.defaults?.pump_head || null, - rated_flow: makeupGlobalConfig.defaults?.pump_flow || null - } - : null, - level_control_valve: makeupConfig.level_control_valve - ? { - id: makeupConfig.level_control_valve, - type: makeupLevelValve?.type || null, - description: makeupLevelValve?.description || null, - position: makeupLevelValve?.position - } - : null, - on_off_valves: makeupOnOffValves.map(vid => { - const valve = valves?.find(v => v.equipment === vid) - return { - id: vid, - type: valve?.type || null, - position: valve?.position, - is_open: valve?.position?.value > 50 - } - }) - } + const makeupSystem = buildMakeupSystem(equipment, config, towerConfig.makeup) - const towerPumps = filterPumpsByCircuit(pumps, 'COOLING_TOWER').map(formatPump) + const towerPumps = filterPumpsByCircuit(pumps, 'COOLING_TOWER') + .filter(p => p.equipment !== makeupSystem.pump?.id) + .map(formatPump) return { title: towerConfig.name || viewConfig.title, @@ -557,6 +573,7 @@ function buildMinersLayoutView (equipment, config, stats, rackPowerByRack) { pumps_config: circuit1.pumps_config, lines: circuit1.lines, control_valves: circuit1.control_valves, + makeup: circuit1.makeup, pumps: circuit1.pumps }, circuit2: {