Add native local device integrations
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { createAirgradientDiscoveryDescriptor } from '../../ts/integrations/airgradient/index.js';
|
||||
|
||||
tap.test('matches AirGradient zeroconf records', async () => {
|
||||
const descriptor = createAirgradientDiscoveryDescriptor();
|
||||
const mdnsMatcher = descriptor.getMatchers()[0];
|
||||
const result = await mdnsMatcher.matches({
|
||||
type: '_airgradient._tcp.local.',
|
||||
name: 'Living Room._airgradient._tcp.local.',
|
||||
host: 'airgradient-123456.local',
|
||||
port: 80,
|
||||
txt: {
|
||||
serialno: 'abcdef123456',
|
||||
model: 'I-9PSL',
|
||||
fw_ver: '3.1.2',
|
||||
},
|
||||
}, {});
|
||||
|
||||
expect(result.matched).toBeTrue();
|
||||
expect(result.confidence).toEqual('certain');
|
||||
expect(result.normalizedDeviceId).toEqual('abcdef123456');
|
||||
expect(result.candidate?.integrationDomain).toEqual('airgradient');
|
||||
expect(result.candidate?.metadata?.firmwareSupported).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('matches manual AirGradient entries and validates supported firmware', async () => {
|
||||
const descriptor = createAirgradientDiscoveryDescriptor();
|
||||
const manualMatcher = descriptor.getMatchers()[1];
|
||||
const result = await manualMatcher.matches({
|
||||
host: '192.168.1.75',
|
||||
name: 'Living Room AirGradient',
|
||||
serialNumber: 'abcdef123456',
|
||||
firmwareVersion: '3.1.2',
|
||||
}, {});
|
||||
|
||||
expect(result.matched).toBeTrue();
|
||||
expect(result.candidate?.port).toEqual(80);
|
||||
|
||||
const validator = descriptor.getValidators()[0];
|
||||
const validated = await validator.validate(result.candidate!, {});
|
||||
expect(validated.matched).toBeTrue();
|
||||
expect(validated.confidence).toEqual('certain');
|
||||
});
|
||||
|
||||
tap.test('rejects unsupported AirGradient firmware versions when known', async () => {
|
||||
const validator = createAirgradientDiscoveryDescriptor().getValidators()[0];
|
||||
const result = await validator.validate({
|
||||
source: 'mdns',
|
||||
integrationDomain: 'airgradient',
|
||||
host: 'airgradient-old.local',
|
||||
serialNumber: 'old123',
|
||||
metadata: {
|
||||
discoveryProtocol: 'zeroconf',
|
||||
firmwareVersion: '3.0.9',
|
||||
},
|
||||
}, {});
|
||||
|
||||
expect(result.matched).toBeFalse();
|
||||
expect(result.reason).toContain('below the supported minimum');
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
@@ -0,0 +1,116 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { AirgradientClient, AirgradientMapper, type IAirgradientSnapshot } from '../../ts/integrations/airgradient/index.js';
|
||||
|
||||
const snapshot: IAirgradientSnapshot = {
|
||||
connected: true,
|
||||
updatedAt: '2026-01-01T00:00:00.000Z',
|
||||
devices: [
|
||||
{
|
||||
host: '192.168.1.75',
|
||||
name: 'Living Room AirGradient',
|
||||
serialNumber: 'abcdef123456',
|
||||
model: 'I-9PSL',
|
||||
modelName: 'AirGradient ONE',
|
||||
firmwareVersion: '3.1.2',
|
||||
latestFirmwareVersion: '3.2.0',
|
||||
online: true,
|
||||
measures: {
|
||||
signalStrength: -49,
|
||||
serialNumber: 'abcdef123456',
|
||||
bootTime: 4,
|
||||
firmwareVersion: '3.1.2',
|
||||
model: 'I-9PSL',
|
||||
pm01: 2,
|
||||
pm02: 6.5,
|
||||
rawPm02: 7,
|
||||
pm10: 9,
|
||||
ambientTemperature: 21.4,
|
||||
relativeHumidity: 45,
|
||||
rco2: 620,
|
||||
totalVolatileOrganicComponentIndex: 3,
|
||||
nitrogenIndex: 1,
|
||||
pm003Count: 123,
|
||||
},
|
||||
config: {
|
||||
country: 'US',
|
||||
pmStandard: 'us-aqi',
|
||||
ledBarMode: 'co2',
|
||||
co2AutomaticBaselineCalibrationDays: 30,
|
||||
temperatureUnit: 'c',
|
||||
configurationControl: 'local',
|
||||
postDataToAirGradient: true,
|
||||
ledBarBrightness: 80,
|
||||
displayBrightness: 60,
|
||||
noxLearningOffset: 60,
|
||||
tvocLearningOffset: 120,
|
||||
},
|
||||
},
|
||||
],
|
||||
events: [],
|
||||
};
|
||||
|
||||
tap.test('maps AirGradient measures and config to devices and entities', async () => {
|
||||
const devices = AirgradientMapper.toDevices(snapshot);
|
||||
const entities = AirgradientMapper.toEntities(snapshot);
|
||||
|
||||
expect(devices[0].id).toEqual('airgradient.device.abcdef123456');
|
||||
expect(devices[0].manufacturer).toEqual('AirGradient');
|
||||
expect(devices[0].features.some((featureArg) => featureArg.id === 'co2')).toBeTrue();
|
||||
expect(entities.find((entityArg) => entityArg.id === 'sensor.living_room_airgradient_pm2_5')?.state).toEqual(6.5);
|
||||
expect(entities.find((entityArg) => entityArg.id === 'sensor.living_room_airgradient_co2')?.state).toEqual(620);
|
||||
expect(entities.find((entityArg) => entityArg.uniqueId.endsWith('_display_pm_standard') && entityArg.platform === 'select')?.state).toEqual('us_aqi');
|
||||
expect(entities.find((entityArg) => entityArg.uniqueId.endsWith('_display_brightness') && entityArg.platform === 'number')?.state).toEqual(60);
|
||||
expect(entities.find((entityArg) => entityArg.uniqueId.endsWith('_update'))?.attributes?.latestVersion).toEqual('3.2.0');
|
||||
});
|
||||
|
||||
tap.test('maps AirGradient services to safe config payloads', async () => {
|
||||
const entities = AirgradientMapper.toEntities(snapshot);
|
||||
const pmStandardEntity = entities.find((entityArg) => entityArg.uniqueId.endsWith('_display_pm_standard') && entityArg.platform === 'select')!;
|
||||
const brightnessEntity = entities.find((entityArg) => entityArg.uniqueId.endsWith('_display_brightness') && entityArg.platform === 'number')!;
|
||||
const postDataEntity = entities.find((entityArg) => entityArg.uniqueId.endsWith('_post_data_to_airgradient'))!;
|
||||
const ledTestEntity = entities.find((entityArg) => entityArg.uniqueId.endsWith('_led_bar_test'))!;
|
||||
|
||||
const pmCommand = AirgradientMapper.commandForService(snapshot, {
|
||||
domain: 'select',
|
||||
service: 'select_option',
|
||||
target: { entityId: pmStandardEntity.id },
|
||||
data: { option: 'ugm3' },
|
||||
});
|
||||
const brightnessCommand = AirgradientMapper.commandForService(snapshot, {
|
||||
domain: 'number',
|
||||
service: 'set_value',
|
||||
target: { entityId: brightnessEntity.id },
|
||||
data: { value: 42 },
|
||||
});
|
||||
const switchCommand = AirgradientMapper.commandForService(snapshot, {
|
||||
domain: 'switch',
|
||||
service: 'turn_off',
|
||||
target: { entityId: postDataEntity.id },
|
||||
});
|
||||
const buttonCommand = AirgradientMapper.commandForService(snapshot, {
|
||||
domain: 'button',
|
||||
service: 'press',
|
||||
target: { entityId: ledTestEntity.id },
|
||||
});
|
||||
|
||||
expect(pmCommand?.type === 'set_config' ? pmCommand.payload : undefined).toEqual({ pmStandard: 'ugm3' });
|
||||
expect(brightnessCommand?.type === 'set_config' ? brightnessCommand.payload : undefined).toEqual({ displayBrightness: 42 });
|
||||
expect(switchCommand?.type === 'set_config' ? switchCommand.payload : undefined).toEqual({ postDataToAirGradient: false });
|
||||
expect(buttonCommand?.type === 'set_config' ? buttonCommand.payload : undefined).toEqual({ ledBarTestRequested: true });
|
||||
expect(AirgradientMapper.commandForService(snapshot, { domain: 'airgradient', service: 'set_config', target: {}, data: { field: 'unknown', value: true } })).toBeUndefined();
|
||||
});
|
||||
|
||||
tap.test('does not fake live AirGradient command success for snapshot-only configs', async () => {
|
||||
const client = new AirgradientClient({ snapshot });
|
||||
const command = AirgradientMapper.commandForService(snapshot, {
|
||||
domain: 'number',
|
||||
service: 'set_value',
|
||||
target: { entityId: 'number.living_room_airgradient_display_brightness' },
|
||||
data: { value: 50 },
|
||||
})!;
|
||||
const result = await client.sendCommand(command);
|
||||
expect(result.success).toBeFalse();
|
||||
expect(result.error).toContain('requires config.host');
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
Reference in New Issue
Block a user