Image-to-Text
English
custom
ocr
document-ai
information-extraction
key-information-extraction
nameplate
nameplate-ocr
equipment-nameplate
data-center
data-centre
datacenter
mission-critical
mep
electrical
mechanical
equipment-schedule
schedule-verification
asset-register
commissioning
quality-assurance
ups
pdu
switchgear
generator
ats
transformer
busway
battery
crah
crac
chiller
cooling
server-rack
construction
aec
edge-ai
on-device
privacy-preserving
tesseract
browser
Eval Results (legacy)
File size: 20,506 Bytes
5e2d1eb 2843cff 5e2d1eb 2843cff 5e2d1eb 2843cff 5e2d1eb bc4e333 5e2d1eb 2843cff bc4e333 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 | // Nameplate parsing, schedule matching and the register CSV (pure functions).
// Run: npm test
import { test } from 'node:test';
import assert from 'node:assert/strict';
import {
parseNameplate, voltagesIn, parseEquipmentSchedule, scheduleVoltages, candidatesFor, checkAgainst, statusOf,
codeSimilarity, foldCode, duplicateSerials, registerSummary, registerCsv, typeOf, EQUIPMENT_TYPES,
SAMPLE_SCHEDULE, SAMPLE_PLATES,
} from '../nameplate-model.js';
const plate = lines => parseNameplate(lines.join('\n'));
const val = (r, k) => r.fields[k]?.value;
const schedule = parseEquipmentSchedule(SAMPLE_SCHEDULE).items;
test('a UPS plate reads into every field', () => {
const r = plate(SAMPLE_PLATES[0]);
assert.equal(r.type, 'ups');
assert.equal(val(r, 'manufacturer'), 'Northline Power Systems');
assert.equal(val(r, 'model'), 'NPX-750-480');
assert.equal(val(r, 'serial'), 'NL26A01937');
assert.equal(val(r, 'voltage'), '480Y/277 V');
assert.deepEqual(r.fields.voltage.values, [480, 277]);
assert.equal(val(r, 'phase'), 3);
assert.equal(val(r, 'hz'), 60);
assert.equal(val(r, 'kva'), 750);
assert.equal(val(r, 'kw'), 675);
assert.equal(val(r, 'amps'), 1002);
assert.equal(val(r, 'mfgDate'), '03/2026');
});
test('real-world plate wording: S/N, CAT. NO., kV, thousands separators, known makers', () => {
const gen = plate(['CATERPILLAR', 'GENERATOR SET', 'CAT. NO. 3516C-HD', 'S/N: ZBA00412', '13.8 KV 3 PHASE 60 HZ', '3,750 KVA 3000 EKW', 'FLA 1,569']);
assert.equal(gen.type, 'generator');
assert.equal(val(gen, 'manufacturer'), 'Caterpillar');
assert.equal(val(gen, 'model'), '3516C-HD');
assert.equal(val(gen, 'serial'), 'ZBA00412');
assert.deepEqual(gen.fields.voltage.values, [13800]);
assert.equal(val(gen, 'kva'), 3750);
assert.equal(val(gen, 'kw'), 3000);
assert.equal(val(gen, 'amps'), 1569);
const ch = plate(['AIR-COOLED CHILLER', 'MODEL: AC-1200', 'REFRIGERANT R-513A CHARGE 410 LBS', '460V 3PH 60HZ', 'MCA 612 MOCP 800']);
assert.equal(ch.type, 'chiller');
assert.equal(val(ch, 'refrigerant'), 'R-513A');
assert.equal(val(ch, 'mca'), 612);
assert.equal(val(ch, 'mocp'), 800);
assert.equal(val(ch, 'amps'), undefined, 'R-513A and a charge weight are not amps');
});
test('voltages: transformer pairs, labelled values, and numbers that are not voltages', () => {
assert.deepEqual(voltagesIn('PRI 13,800 V DELTA SEC 480Y/277 V').values, [480, 277, 13800]);
assert.deepEqual(voltagesIn('VOLTS: 208').values, [208]);
assert.deepEqual(voltagesIn('DWG 480/277 REV 2').values, [], 'a bare pair with no voltage word or unit');
assert.deepEqual(scheduleVoltages('480 / 415Y/240'), [480, 415, 240]);
assert.deepEqual(scheduleVoltages('13.8kV'), [13800]);
});
test('"New York" in an address is not the maker York; unknown makers come from a company line', () => {
const r = plate(['ACME SWITCHGEAR CORP', 'NEW YORK NY', 'SWITCHBOARD', 'MODEL SB-4000']);
assert.equal(val(r, 'manufacturer'), 'Acme Switchgear Corp');
assert.equal(r.type, 'switchgear');
// A transformer winding is "delta"; that is not the maker Delta.
const t = plate(['VOLTARC TRANSFORMER CORP', 'DRY TYPE TRANSFORMER', 'MODEL VT-2500-HV', 'PRIMARY 13800 V DELTA', 'SECONDARY 480Y/277 V']);
assert.equal(val(t, 'manufacturer'), 'Voltarc Transformer Corp');
assert.equal(t.type, 'transformer');
});
test('codes compare the way OCR misreads them', () => {
assert.equal(foldCode('NPX-75O-48O'), foldCode('NPX750480'));
assert.equal(codeSimilarity('NL26A0I937', 'NL26A01937'), 1);
assert.ok(codeSimilarity('9395P-750', '9395P-750-N-480') >= 0.9, 'option suffix on the plate');
assert.ok(codeSimilarity('NPX-750-415', 'NPX-750-480') < 0.85, 'a different voltage variant is not the same model');
});
test('schedule CSV: header row found below title rows, duplicate tags reported', () => {
const { items, problems } = parseEquipmentSchedule('Data Hall 1 equipment schedule\n\nMark,Equipment,Mfr,Model No.,Volts,kVA\nUPS-1A,UPS,Eaton,9395XR,480,750\nUPS-1A,UPS,Eaton,9395XR,480,750\n');
assert.equal(items.length, 1);
assert.equal(items[0].manufacturer, 'Eaton');
assert.equal(items[0].kva, 750);
assert.match(problems[0], /UPS-1A appears more than once/);
assert.match(parseEquipmentSchedule('a,b\n1,2').problems[0], /No header row/);
});
test('identical units: the first unscanned tag is offered first', () => {
const r = plate(SAMPLE_PLATES[0]);
const first = candidatesFor(r, schedule);
assert.deepEqual(first.slice(0, 4).map(c => c.item.tag), ['UPS-1A', 'UPS-1B', 'UPS-2A', 'UPS-2B']);
const next = candidatesFor(r, schedule, [{ tag: 'UPS-1A' }]);
assert.equal(next[0].item.tag, 'UPS-1B');
assert.equal(next.find(c => c.item.tag === 'UPS-1A').registered, true);
assert.ok(!first.some(c => c.item.tag.startsWith('GEN')), 'a generator is not a candidate for a UPS plate');
});
test('a serial on the schedule decides the match outright', () => {
const items = parseEquipmentSchedule('Tag,Model,Serial\nUPS-1A,NPX-750-480,NL26A00001\nUPS-1B,NPX-750-480,NL26A01937\n').items;
assert.equal(candidatesFor(plate(SAMPLE_PLATES[0]), items)[0].item.tag, 'UPS-1B');
});
test('voltage compatibility is a positive signal, but the equipment type still wins', () => {
// The 415 V UPS shares a voltage with the PDU rows (480/415Y/240), yet the
// matching UPS rows must still rank first.
const first = candidatesFor(plate(SAMPLE_PLATES[1]), schedule);
assert.ok(first[0].item.tag.startsWith('UPS'), `first was ${first[0].item.tag}`);
assert.ok(!first.slice(0, 4).some(c => c.item.tag.startsWith('PDU')), 'no PDU outranks the UPS rows');
});
test('out-of-range OCR noise is not accepted as a rating', () => {
const r = plate(['ACME', 'SWITCHBOARD', 'MODEL SB-1', '0 KVA', 'SCCR 0', '0 A', '0 OUTLETS']);
assert.equal(val(r, 'kva'), undefined, 'a zero rating is not a rating');
assert.equal(val(r, 'sccr'), undefined);
assert.equal(val(r, 'amps'), undefined);
assert.equal(val(r, 'outlets'), undefined);
const ok = plate(['ACME', 'SWITCHBOARD', 'MODEL SB-1', '480 V', '65 KAIC']);
assert.equal(val(ok, 'sccr'), 65, 'a plausible value is kept');
});
test('checks: the right unit verifies; the wrong-voltage unit is flagged on model and voltage', () => {
const ups1a = schedule.find(i => i.tag === 'UPS-1A');
const good = checkAgainst(plate(SAMPLE_PLATES[0]), ups1a);
assert.ok(good.every(c => c.status === 'ok'), JSON.stringify(good));
assert.equal(statusOf(good, 'UPS-1A'), 'verified');
const bad = checkAgainst(plate(SAMPLE_PLATES[1]), ups1a);
assert.deepEqual(bad.filter(c => c.status === 'mismatch').map(c => c.key), ['model', 'voltage']);
assert.equal(statusOf(bad, 'UPS-1A'), 'mismatch');
const unread = checkAgainst(parseNameplate('MODEL NPX-750-480'), ups1a);
assert.equal(statusOf(unread, 'UPS-1A'), 'partial');
assert.equal(statusOf([], ''), 'unmatched');
});
test('register: summary, duplicate serials, and a CSV that lists what is still unscanned', () => {
const a = plate(SAMPLE_PLATES[0]);
const ups1a = schedule.find(i => i.tag === 'UPS-1A');
const reg = [
{ id: 'a', tag: 'UPS-1A', type: 'ups', fields: a.fields, checks: checkAgainst(a, ups1a), status: 'verified', scannedAt: Date.UTC(2026, 9, 2, 14, 30) },
{ id: 'b', tag: '', type: 'ups', fields: a.fields, checks: [], status: 'unmatched', scannedAt: Date.UTC(2026, 9, 2, 14, 35) },
];
assert.deepEqual(duplicateSerials(reg), [['a', 'b']]);
const s = registerSummary(reg, schedule);
assert.equal(s.scannedOfSchedule, 1);
assert.equal(s.verified, 1);
assert.equal(s.unmatched, 1);
assert.equal(s.missing.length, schedule.length - 1);
const rows = registerCsv(reg, schedule).trim().split('\n');
assert.equal(rows.length, 1 + 2 + schedule.length - 1);
assert.match(rows[1], /^UPS-1A,Verified,UPS,Electrical Room 1A,Northline Power Systems,NPX-750-480,NL26A01937,480Y\/277 V,3,60,750,675,1002,,,,,,,,,,,,,,,,,,,,,,,,,,03\/2026,Serial also on another scanned plate,2026-10-02 14:30$/);
assert.match(rows[3], /^UPS-1B,Not scanned yet,/);
});
test('OCR misreads seen on real lettering are undone before parsing', () => {
const r = parseNameplate(['MODEL NO: NPX- 750- 480', '480¥/277 VAC 3 PH 60 Hw', '460 Vv', '750 XVA / 675 XW', 'FLA 38. 5'].join('\n'));
assert.equal(r.fields.model.value, 'NPX-750-480');
assert.deepEqual(r.fields.voltage.values, [480, 277, 460]);
assert.equal(r.fields.hz.value, 60);
assert.equal(r.fields.kva.value, 750);
assert.equal(r.fields.kw.value, 675);
assert.equal(r.fields.amps.value, 38.5);
});
test('MCA and MOCP are checked when the schedule lists them; near-zero-confidence lines are ignored', () => {
const ch = schedule.find(i => i.tag === 'CH-01');
assert.equal(ch.mca, 612);
const r = parseNameplate([{ text: 'MODEL AC-1200', confidence: 90 }, { text: '460 V 3 PH', confidence: 88 }, { text: 'MCA 612 MOCP 700', confidence: 85 },
{ text: 'REFRIGERANT R-513A', confidence: 90 }, { text: 'MODEL ZZ-9 480 V', confidence: 8 }]);
assert.equal(r.fields.model.value, 'AC-1200', 'the 8% line is texture, not a second model');
const checks = checkAgainst(r, ch);
assert.deepEqual(checks.filter(c => c.status === 'mismatch').map(c => c.key), ['mocp']);
});
test('a wide data centre equipment set is recognised, device wording beating its family', () => {
assert.ok(EQUIPMENT_TYPES.length >= 40, 'the taxonomy covers more than the classic dozen');
const cases = [
['FLYWHEEL UPS', 'flywheel'], ['UNINTERRUPTIBLE POWER SUPPLY', 'ups'], ['STATIC TRANSFER SWITCH', 'sts'],
['AUTOMATIC TRANSFER SWITCH', 'ats'], ['BATTERY CHARGER', 'rectifier'], ['POWER CONVERSION SYSTEM INVERTER', 'inverter'],
['BATTERY ENERGY STORAGE SYSTEM', 'bess'], ['PARALLELING SWITCHGEAR', 'paralleling'], ['DIESEL GENERATOR SET', 'generator'],
['FUEL OIL DAY TANK', 'fueltank'], ['SWITCHBOARD', 'switchgear'], ['MOTOR CONTROL CENTER', 'mcc'],
['POWER METER', 'metering'], ['DRY TYPE TRANSFORMER', 'transformer'], ['SURGE PROTECTIVE DEVICE', 'spd'],
['POWER FACTOR CORRECTION CAPACITOR', 'capacitor'], ['VARIABLE FREQUENCY DRIVE', 'vfd'], ['ISOLATED PHASE BUS', 'busway'],
['PANELBOARD', 'panelboard'], ['REMOTE POWER PANEL', 'pdu'], ['BATTERY CABINET', 'battery'], ['LOAD BANK', 'loadbank'],
['COOLING TOWER', 'coolingtower'], ['AIR-COOLED CHILLER', 'chiller'], ['DRY COOLER', 'condenser'],
['LIQUID COOLING CDU', 'cdu'], ['REAR DOOR HEAT EXCHANGER', 'cdu'], ['COMPUTER ROOM AIR HANDLER', 'crah'],
['AIR HANDLING UNIT', 'ahu'], ['HEAT PUMP', 'heatpump'], ['PLATE HEAT EXCHANGER', 'heatexchanger'],
['CHILLED WATER PUMP', 'pump'], ['CONDENSER WATER PUMP', 'pump'], ['STEAM HUMIDIFIER', 'humidifier'],
['WATER TREATMENT PLANT', 'watertreatment'], ['DESICCANT DRYER', 'dryer'], ['AIR COMPRESSOR', 'compressor'],
['FAN COIL UNIT', 'fcu'], ['ROOFTOP UNIT', 'rtu'], ['MAKEUP AIR UNIT', 'mua'], ['FM-200 FIRE SUPPRESSION', 'firesuppression'],
['VESDA ASPIRATING SMOKE DETECT', 'firealarm'], ['WATER LEAK DETECT', 'leakdetection'], ['BMS CONTROLLER', 'bms'],
['KVM SWITCH', 'kvm'], ['SERVER RACK ENCLOSURE', 'rack'], ['BLADE SERVER', 'server'], ['NETWORK SWITCH', 'network'],
['PROTECTIVE RELAY PANEL', 'relay'], ['TRANSFORMER MONITOR DGA', 'tmonitor'], ['NEUTRAL GROUNDING RESISTOR', 'ngr'],
['ISOLATION TRANSFORMER', 'transformer'], ['K-RATED TRANSFORMER', 'transformer'], ['WATER SOFTENER', 'watertreatment'],
['REVERSE OSMOSIS SYSTEM', 'watertreatment'], ['EXPANSION TANK', 'expansiontank'], ['AIR SEPARATOR', 'airseparator'],
['NITROGEN GENERATOR', 'nitrogen'], ['HOT WATER BOILER', 'boiler'], ['THERMAL STORAGE TANK', 'thermalstorage'],
['EVAPORATIVE COOLER', 'condenser'], ['STATIC BYPASS', 'sts'], ['RACK ATS', 'sts'],
];
for (const [text, want] of cases) assert.equal(typeOf(text), want, text);
});
test('cooling and electrical nameplates read the wider field set', () => {
const sg = plate(['IRONSIDE SWITCHGEAR CORP', 'SWITCHBOARD', 'MODEL IS-4000-MDP', 'SERIAL IS26C01188',
'480Y/277 V 3 PH 60 HZ', 'SCCR 65 KAIC', 'NEMA 3R', '0.8 PF']);
assert.equal(sg.type, 'switchgear');
assert.equal(val(sg, 'sccr'), 65);
assert.equal(val(sg, 'enclosure'), 'NEMA 3R');
assert.equal(val(sg, 'pf'), 0.8);
const ch = plate(['POLARIS THERMAL', 'WATER-COOLED CHILLER', 'MODEL PT-WC-900', '460 V 3 PH 60 HZ',
'REFRIGERANT R-1234ZE CHARGE 620 LBS', 'CAPACITY 500 TONS', 'AIRFLOW 24000 CFM', 'FLOW 2400 GPM']);
assert.equal(ch.type, 'chiller');
assert.equal(val(ch, 'refrigerant'), 'R-1234ZE');
assert.equal(val(ch, 'charge'), 620);
assert.equal(val(ch, 'tons'), 500);
assert.equal(val(ch, 'cfm'), 24000);
assert.equal(val(ch, 'gpm'), 2400);
const bat = plate(['ENERSYS', 'BATTERY CABINET', 'MODEL MX-500', '480 V 3 PH', 'CAPACITY 500 AH', 'IP20']);
assert.equal(bat.type, 'battery');
assert.equal(val(bat, 'batteryAh'), 500);
assert.equal(val(bat, 'enclosure'), 'IP 20');
});
test('battery chemistry, listings, speed and rigging weight are read', () => {
const bat = plate(['EXIDE', 'BATTERY STRING', 'MODEL VR-48', 'VRLA 48 CELLS', 'LISTED UL 1989', 'WEIGHT 1250 LBS']);
assert.equal(val(bat, 'batteryType'), 'VRLA');
assert.equal(val(bat, 'cells'), 48);
assert.equal(val(bat, 'standards'), 'UL 1989');
assert.equal(val(bat, 'weight'), 567, '1250 lb normalised to kg');
const pump = plate(['GRUNDFOS', 'END SUCTION PUMP', 'MODEL CR-64', 'RATED SPEED 1750 RPM', 'MOTOR IE3', 'WEIGHT 320 KG']);
assert.equal(pump.type, 'pump');
assert.equal(val(pump, 'rpm'), 1750);
assert.equal(val(pump, 'weight'), 320);
const li = plate(['SAFT', 'BATTERY CABINET', 'LITHIUM-ION', 'MODEL LI-500', 'IEC 62619']);
assert.equal(val(li, 'batteryType'), 'Lithium-ion');
assert.equal(val(li, 'standards'), 'IEC 62619');
});
test('battery, listing, speed and weight columns are imported and checked', () => {
const { items, problems } = parseEquipmentSchedule(
'Tag,Type,Model,Voltage,Battery type,Listings,Ah,Cells,RPM,Weight\n' +
'BAT-1,Battery cabinet,MX-500,480,VRLA,UL 1989,500,48,,\n' +
'PMP-1,Pump,CR-64,460,,,,,1750,320\n');
assert.deepEqual(problems, []);
const [bat, pump] = items;
assert.equal(bat.batteryType, 'VRLA');
assert.equal(bat.standards, 'UL 1989');
assert.equal(bat.cells, 48);
assert.equal(pump.rpm, 1750);
assert.equal(pump.weight, 320);
const good = checkAgainst(plate(['EXIDE', 'BATTERY CABINET', 'MODEL MX-500', '480 V', 'VRLA 500 AH 48 CELLS', 'UL 1989']), bat);
assert.deepEqual(good.filter(c => c.status !== 'ok'), []);
const bad = checkAgainst(plate(['EXIDE', 'BATTERY CABINET', 'MODEL MX-500', '480 V', 'AGM 400 AH 24 CELLS', 'UL 891']), bat);
assert.deepEqual(bad.filter(c => c.status === 'mismatch').map(c => c.key), ['batteryAh', 'cells', 'batteryType', 'standards']);
});
test('server-rack nameplates: rack U height, watts and airflow', () => {
const srv = plate(['DELL EMC', 'POWEREDGE R650 SERVER', 'MODEL NO: R650-2U', 'SERIAL NO: DL26R00442',
'480 V 3 PH 60 HZ', '800 W 12.5 A', 'AIRFLOW 120 CFM']);
assert.equal(srv.type, 'server');
assert.equal(val(srv, 'uHeight'), 2, 'from the model suffix R650-2U');
assert.equal(val(srv, 'powerW'), 800);
assert.equal(val(srv, 'amps'), 12.5);
assert.equal(val(srv, 'cfm'), 120);
// A kW line must not be read as watts.
const pdu = plate(['VERTIV', 'RACK PDU', 'MODEL MPX-30', '400 V 3 PH', '5.5 KW', '24 OUTLETS']);
assert.equal(val(pdu, 'powerW'), undefined);
assert.equal(val(pdu, 'kw'), 5.5);
const { items } = parseEquipmentSchedule('Tag,Type,Model,W,Rack U\nSRV-1,Server,R650,800,2\n');
assert.equal(items[0].powerW, 800);
assert.equal(items[0].uHeight, 2);
assert.deepEqual(checkAgainst(srv, items[0]).filter(c => c.status === 'mismatch').map(c => c.key), ['model']);
// A rack PDU: outlets, kW (not watts) and amps.
const rpdu = plate(['VERTIV', 'RACK PDU', 'MODEL NO: MPX-30-C13', 'SERIAL NO: VR26P00117',
'400 V 3 PH 60 HZ', '30 A 5.5 KW', '24 OUTLETS']);
assert.equal(rpdu.type, 'pdu');
assert.equal(val(rpdu, 'outlets'), 24);
assert.equal(val(rpdu, 'kw'), 5.5);
assert.equal(val(rpdu, 'powerW'), undefined, 'kW is not watts');
assert.equal(val(rpdu, 'amps'), 30);
const sw = plate(['CISCO', 'CATALYST NETWORK SWITCH', 'MODEL NO: C9300-48P', 'SERIAL NO: FOC26X0011',
'230 V 3 PH 50 HZ', '48-PORT POE 1100 W']);
assert.equal(sw.type, 'network');
assert.equal(val(sw, 'ports'), 48);
assert.equal(val(sw, 'powerW'), 1100);
});
test('schedule columns for the extra fields are read and checked', () => {
const { items, problems } = parseEquipmentSchedule('Tag,Type,Model,Voltage,PF,SCCR,Enclosure,Tons,CFM,GPM,Ah\n' +
'SWB-1,Switchboard,IS-4000,480Y/277,0.8,65,NEMA 3R,,,,,\n' +
'CH-1,Chiller,PT-900,460,,,,500,24000,2400,\n' +
'BAT-1,Battery cabinet,MX-500,480,,,,,,,500\n');
assert.deepEqual(problems, []);
const [swb, ch, bat] = items;
assert.equal(swb.pf, 0.8);
assert.equal(swb.sccr, 65);
assert.equal(swb.enclosure, 'NEMA 3R');
assert.equal(ch.tons, 500);
assert.equal(ch.cfm, 24000);
assert.equal(ch.gpm, 2400);
assert.equal(bat.batteryAh, 500);
const good = checkAgainst(plate(['ACME SWITCHBOARD', 'SWITCHBOARD', 'MODEL IS-4000', '480Y/277 V', 'PF 0.8', 'SCCR 65 KA', 'NEMA 3R']), swb);
assert.deepEqual(good.filter(c => c.status !== 'ok'), []);
const bad = checkAgainst(plate(['ACME SWITCHBOARD', 'SWITCHBOARD', 'MODEL IS-4000', '480Y/277 V', 'PF 0.9', 'SCCR 42 KA', 'NEMA 1']), swb);
assert.deepEqual(bad.filter(c => c.status === 'mismatch').map(c => c.key), ['pf', 'sccr', 'enclosure']);
});
test('server-rack plates read form factor, static load, outlet mix and port speed', () => {
// A blade chassis: its own form factor, and a static floor load.
const blade = plate(['NEXUS COMPUTE', 'BLADE SERVER CHASSIS', 'MODEL NO: BX-7000', 'SERIAL NO: NC26B00123',
'FORM FACTOR 8U', '480 V 3 PH 60 HZ', 'STATIC LOAD 1360 KG']);
assert.equal(blade.type, 'server');
assert.equal(val(blade, 'formFactor'), '8U');
assert.equal(val(blade, 'rackLoad'), 1360);
// A rack PDU: the outlet mix is canonicalised whichever way it is written.
const pdu = plate(['VERTIV', 'RACK PDU', 'MODEL NO: MPX-30-C13', 'SERIAL NO: VR26P00117',
'400 V 3 PH 60 HZ', '30 A 5.5 KW', '24 OUTLETS (12x C13, 4x C19)']);
assert.equal(pdu.type, 'pdu');
assert.equal(val(pdu, 'outletTypes'), 'C13x12 C19x4');
assert.equal(val(pdu, 'outlets'), 24);
// "C13" in a model code alone is not an outlet mix.
assert.equal(val(plate(['VERTIV', 'RACK PDU', 'MODEL NO: MPX-30-C13', '400 V 3 PH']), 'outletTypes'), undefined);
// A switch: the fastest link, not the first number.
const sw = plate(['CISCO', 'CATALYST NETWORK SWITCH', 'MODEL NO: C9300-48P', 'SERIAL NO: FOC26X0011',
'230 V 3 PH 50 HZ', '48-PORT POE 1100 W', 'UPLINK 10/25G']);
assert.equal(sw.type, 'network');
assert.equal(val(sw, 'ports'), 48);
assert.equal(val(sw, 'portSpeed'), 25);
assert.equal(val(sw, 'powerW'), 1100);
// "42RU" is a rack height, and a tower server is a form factor.
assert.equal(val(plate(['RITTAL', 'SERVER RACK ENCLOSURE', 'MODEL TS-42RU', '42RU 800x1200']), 'uHeight'), 42);
assert.equal(val(plate(['DELL', 'TOWER SERVER', 'MODEL T-360', 'Tower chassis']), 'formFactor'), 'tower');
});
test('new rack columns import from the schedule and are checked field by field', () => {
const { items, problems } = parseEquipmentSchedule(
'Tag,Type,Model,Form factor,Rack load,Outlet mix,Port speed,Ports\n' +
'SRV-1,Server,R650-2U,2U,1360,C13x24,25,48\n' +
'SW-1,Network switch,C9300,,,,25,48\n');
assert.deepEqual(problems, []);
const [srv, sw] = items;
assert.equal(srv.formFactor, '2U');
assert.equal(srv.rackLoad, 1360);
assert.equal(srv.outletTypes, 'C13x24');
assert.equal(srv.portSpeed, 25);
assert.equal(sw.portSpeed, 25);
assert.equal(sw.ports, 48);
const good = checkAgainst(plate(['ACME SERVER', 'POWEREDGE SERVER', 'MODEL R650-2U', '480 V 3 PH',
'FORM FACTOR 2U', 'STATIC LOAD 1360 KG', '25 OUTLETS (24x C13)', '48-PORT 25G']), srv);
assert.deepEqual(good.filter(c => c.status !== 'ok'), []);
const bad = checkAgainst(plate(['ACME SERVER', 'POWEREDGE SERVER', 'MODEL R650-2U', '480 V 3 PH',
'FORM FACTOR 4U', 'STATIC LOAD 900 KG', '25 OUTLETS (12x C13)', '48-PORT 10G']), srv);
assert.deepEqual(bad.filter(c => c.status === 'mismatch').map(c => c.key).sort(),
['formFactor', 'outletTypes', 'portSpeed', 'rackLoad'].sort());
});
|