Add native local device integrations

This commit is contained in:
2026-05-05 18:26:11 +00:00
parent accfa82f36
commit 282283d344
69 changed files with 9713 additions and 182 deletions
@@ -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();