Add native local device integrations
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import * as plugins from '../../ts/plugins.js';
|
||||
import { ApcupsdClient } from '../../ts/integrations/apcupsd/index.js';
|
||||
|
||||
const statusText = `UPSNAME : TCP UPS
|
||||
STATUS : ONBATT LOWBATT
|
||||
BCHARGE : 17.0 Percent
|
||||
STATFLAG : 0x05000000
|
||||
`;
|
||||
|
||||
tap.test('requests APCUPSd NIS status over local TCP', async () => {
|
||||
const server = plugins.net.createServer((socketArg) => {
|
||||
socketArg.once('data', (chunkArg) => {
|
||||
const buffer = Buffer.isBuffer(chunkArg) ? chunkArg : Buffer.from(chunkArg);
|
||||
const length = buffer.readUInt16BE(0);
|
||||
const command = buffer.subarray(2, 2 + length).toString('utf8');
|
||||
expect(command).toEqual('status');
|
||||
socketArg.write(frame(statusText));
|
||||
socketArg.write(Buffer.alloc(2));
|
||||
});
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve));
|
||||
try {
|
||||
const address = server.address();
|
||||
const port = typeof address === 'object' && address ? address.port : 0;
|
||||
const snapshot = await new ApcupsdClient({ host: '127.0.0.1', port, timeoutMs: 1000 }).getSnapshot();
|
||||
expect(snapshot.online).toBeTrue();
|
||||
expect(snapshot.ups.name).toEqual('TCP UPS');
|
||||
expect(snapshot.ups.lineOnline).toBeFalse();
|
||||
expect(snapshot.battery.chargePercent).toEqual(17);
|
||||
} finally {
|
||||
await new Promise<void>((resolve, reject) => server.close((errorArg) => errorArg ? reject(errorArg) : resolve()));
|
||||
}
|
||||
});
|
||||
|
||||
const frame = (valueArg: string): Buffer => {
|
||||
const payload = Buffer.from(valueArg, 'utf8');
|
||||
const result = Buffer.alloc(payload.length + 2);
|
||||
result.writeUInt16BE(payload.length, 0);
|
||||
payload.copy(result, 2);
|
||||
return result;
|
||||
};
|
||||
|
||||
export default tap.start();
|
||||
@@ -0,0 +1,25 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { createApcupsdDiscoveryDescriptor } from '../../ts/integrations/apcupsd/index.js';
|
||||
|
||||
tap.test('matches and validates manual APCUPSd entries', async () => {
|
||||
const descriptor = createApcupsdDiscoveryDescriptor();
|
||||
const matcher = descriptor.getMatchers()[0];
|
||||
const result = await matcher.matches({ host: '192.168.1.60', name: 'Rack APC UPS' }, {});
|
||||
expect(result.matched).toBeTrue();
|
||||
expect(result.candidate?.integrationDomain).toEqual('apcupsd');
|
||||
expect(result.candidate?.port).toEqual(3551);
|
||||
|
||||
const validator = descriptor.getValidators()[0];
|
||||
const validation = await validator.validate(result.candidate!, {});
|
||||
expect(validation.matched).toBeTrue();
|
||||
expect(validation.normalizedDeviceId).toEqual('192.168.1.60:3551');
|
||||
});
|
||||
|
||||
tap.test('rejects manual entries without APCUPSd hints', async () => {
|
||||
const descriptor = createApcupsdDiscoveryDescriptor();
|
||||
const matcher = descriptor.getMatchers()[0];
|
||||
const result = await matcher.matches({ name: 'Generic device' }, {});
|
||||
expect(result.matched).toBeFalse();
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
@@ -0,0 +1,61 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { ApcupsdClient, ApcupsdMapper, type IApcupsdSnapshot } from '../../ts/integrations/apcupsd/index.js';
|
||||
|
||||
const rawStatus = `APC : 001,043,1036
|
||||
DATE : 2026-01-01 00:00:00 +0000
|
||||
HOSTNAME : nas
|
||||
VERSION : 3.14.14
|
||||
UPSNAME : Office UPS
|
||||
MODEL : Back-UPS ES 700
|
||||
STATUS : ONLINE
|
||||
LINEV : 230.0 Volts
|
||||
LOADPCT : 21.0 Percent
|
||||
BCHARGE : 98.0 Percent
|
||||
TIMELEFT : 42.5 Minutes
|
||||
BATTV : 13.6 Volts
|
||||
LINEFREQ : 50.0 Hz
|
||||
OUTPUTV : 230.1 Volts
|
||||
NOMPOWER : 405 Watts
|
||||
SERIALNO : AS1234567890
|
||||
STATFLAG : 0x05000008 Status Flag
|
||||
END APC : 2026-01-01 00:00:00 +0000
|
||||
`;
|
||||
|
||||
tap.test('parses APCUPSd status output into a safe snapshot', async () => {
|
||||
const snapshot = await new ApcupsdClient({ rawStatus }).getSnapshot();
|
||||
expect(snapshot.ups.name).toEqual('Office UPS');
|
||||
expect(snapshot.ups.serialNumber).toEqual('AS1234567890');
|
||||
expect(snapshot.ups.lineOnline).toBeTrue();
|
||||
expect(snapshot.battery.chargePercent).toEqual(98);
|
||||
expect(snapshot.battery.timeLeftMinutes).toEqual(42.5);
|
||||
expect(snapshot.power.lineVoltage).toEqual(230);
|
||||
expect(snapshot.power.loadPercent).toEqual(21);
|
||||
});
|
||||
|
||||
tap.test('maps APCUPSd snapshot to canonical devices and entities', async () => {
|
||||
const snapshot = await new ApcupsdClient({ rawStatus }).getSnapshot();
|
||||
const devices = ApcupsdMapper.toDevices(snapshot);
|
||||
const entities = ApcupsdMapper.toEntities(snapshot);
|
||||
|
||||
expect(devices.some((deviceArg) => deviceArg.id === 'apcupsd.ups.as1234567890')).toBeTrue();
|
||||
expect(entities.find((entityArg) => entityArg.id === 'binary_sensor.office_ups_online_status')?.state).toEqual('on');
|
||||
expect(entities.find((entityArg) => entityArg.id === 'sensor.office_ups_battery_charge')?.state).toEqual(98);
|
||||
expect(entities.find((entityArg) => entityArg.id === 'sensor.office_ups_input_voltage')?.attributes?.unit).toEqual('V');
|
||||
expect(entities.find((entityArg) => entityArg.id === 'sensor.office_ups_nominal_output_power')?.state).toEqual(405);
|
||||
});
|
||||
|
||||
tap.test('maps offline snapshots without inventing unavailable sensor values', async () => {
|
||||
const snapshot: IApcupsdSnapshot = {
|
||||
ups: { id: 'offline-ups', name: 'Offline UPS' },
|
||||
battery: {},
|
||||
power: {},
|
||||
status: {},
|
||||
online: false,
|
||||
updatedAt: '2026-01-01T00:00:00.000Z',
|
||||
};
|
||||
const entities = ApcupsdMapper.toEntities(snapshot);
|
||||
expect(entities.find((entityArg) => entityArg.id === 'sensor.offline_ups_status')?.available).toBeFalse();
|
||||
expect(entities.some((entityArg) => entityArg.id === 'sensor.offline_ups_battery_charge')).toBeFalse();
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
Reference in New Issue
Block a user