Add native local network integrations

This commit is contained in:
2026-05-05 18:45:46 +00:00
parent 282283d344
commit cfab8c593e
70 changed files with 9688 additions and 176 deletions
@@ -0,0 +1,41 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { createArcamFmjDiscoveryDescriptor } from '../../ts/integrations/arcam_fmj/index.js';
tap.test('matches Arcam FMJ SSDP media renderer records', async () => {
const descriptor = createArcamFmjDiscoveryDescriptor();
const matcher = descriptor.getMatchers()[0];
const result = await matcher.matches({
st: 'urn:schemas-upnp-org:device:MediaRenderer:1',
usn: 'uuid:2f402f80-da50-11e1-9b23-001788abcdef::urn:schemas-upnp-org:device:MediaRenderer:1',
location: 'http://192.168.1.60:8080/dd.xml',
upnp: {
manufacturer: 'ARCAM',
modelName: 'AVR20',
friendlyName: 'Living Room Arcam',
deviceType: 'urn:schemas-upnp-org:device:MediaRenderer:1',
UDN: 'uuid:2f402f80-da50-11e1-9b23-001788abcdef',
},
}, {});
expect(result.matched).toBeTrue();
expect(result.candidate?.integrationDomain).toEqual('arcam_fmj');
expect(result.candidate?.host).toEqual('192.168.1.60');
expect(result.candidate?.port).toEqual(50000);
expect(result.normalizedDeviceId).toEqual('001788abcdef');
});
tap.test('matches and validates manual Arcam FMJ entries', async () => {
const descriptor = createArcamFmjDiscoveryDescriptor();
const matcher = descriptor.getMatchers()[1];
const result = await matcher.matches({ host: '192.168.1.61', name: 'Cinema Arcam', model: 'AVR850' }, {});
expect(result.matched).toBeTrue();
expect(result.candidate?.port).toEqual(50000);
const validator = descriptor.getValidators()[0];
const validation = await validator.validate(result.candidate!, {});
expect(validation.matched).toBeTrue();
expect(validation.confidence).toEqual('high');
expect(validation.normalizedDeviceId).toEqual('192.168.1.61:50000');
});
export default tap.start();
@@ -0,0 +1,74 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { ArcamFmjMapper, type IArcamFmjSnapshot } from '../../ts/integrations/arcam_fmj/index.js';
const snapshot: IArcamFmjSnapshot = {
deviceInfo: {
host: '192.168.1.60',
port: 50000,
name: 'Living Room Arcam',
manufacturer: 'Arcam',
model: 'AVR20',
revision: '1.2.3',
uniqueId: 'arcam-abc123',
apiModel: 'APIHDA_SERIES',
},
zones: [{
zone: 1,
name: 'Main Zone',
power: true,
volume: 50,
muted: false,
source: 'BD',
sourceList: ['BD', 'SAT', 'FM', 'DAB', 'NET'],
soundMode: 'DOLBY_SURROUND',
media: {
title: 'Blu-ray',
contentType: 'video',
},
available: true,
}, {
zone: 2,
name: 'Patio',
power: false,
volumeLevel: 0.25,
muted: true,
source: 'FM',
media: {
title: 'FM - Radio One',
channel: 'Radio One',
contentType: 'music',
contentId: 'preset:4',
},
available: true,
}],
online: true,
source: 'snapshot',
lastUpdated: '2026-01-01T00:00:00.000Z',
};
tap.test('maps Arcam FMJ receiver zones to canonical devices', async () => {
const devices = ArcamFmjMapper.toDevices(snapshot);
expect(devices.length).toEqual(2);
expect(devices[0].id).toEqual('arcam_fmj.receiver.arcam_abc123');
expect(devices[0].manufacturer).toEqual('Arcam');
expect(devices[0].state.some((stateArg) => stateArg.featureId === 'source' && stateArg.value === 'BD')).toBeTrue();
expect(devices[1].metadata?.viaDeviceId).toEqual('arcam_fmj.receiver.arcam_abc123');
expect(devices[1].state.some((stateArg) => stateArg.featureId === 'power' && stateArg.value === 'off')).toBeTrue();
});
tap.test('maps Arcam FMJ zones to media player entities', async () => {
const entities = ArcamFmjMapper.toEntities(snapshot);
const main = entities.find((entityArg) => entityArg.id === 'media_player.living_room_arcam');
const zone2 = entities.find((entityArg) => entityArg.id === 'media_player.living_room_arcam_zone_2');
expect(main?.platform).toEqual('media_player');
expect(main?.state).toEqual('on');
expect(main?.attributes?.volumeLevel).toEqual(50 / 99);
expect(main?.attributes?.source).toEqual('BD');
expect(main?.attributes?.soundMode).toEqual('DOLBY_SURROUND');
expect(zone2?.state).toEqual('off');
expect(zone2?.attributes?.mediaChannel).toEqual('Radio One');
expect(zone2?.attributes?.mediaContentId).toEqual('preset:4');
});
export default tap.start();
@@ -0,0 +1,85 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { ArcamFmjIntegration, type IArcamFmjModeledCommand, type IArcamFmjSnapshot } from '../../ts/integrations/arcam_fmj/index.js';
const snapshot: IArcamFmjSnapshot = {
deviceInfo: {
name: 'Living Room Arcam',
model: 'AVR450',
uniqueId: 'arcam-abc123',
apiModel: 'API450_SERIES',
},
zones: [{
zone: 1,
power: true,
source: 'BD',
available: true,
}, {
zone: 2,
power: true,
source: 'FM',
available: true,
}],
online: true,
};
tap.test('models Arcam FMJ volume services through an explicit executor', async () => {
const executed: IArcamFmjModeledCommand[] = [];
const integration = new ArcamFmjIntegration();
const runtime = await integration.setup({
snapshot,
commandExecutor: {
execute: async (commandArg) => {
executed.push(commandArg);
return { accepted: true };
},
},
}, {});
const result = await runtime.callService!({
domain: 'media_player',
service: 'volume_set',
target: { entityId: 'media_player.living_room_arcam_zone_2' },
data: { volume_level: 0.25 },
});
expect(result.success).toBeTrue();
expect(executed[0].zone).toEqual(2);
expect(executed[0].commandCodeName).toEqual('VOLUME');
expect(executed[0].data).toEqual([25]);
expect(executed[0].responseExpected).toBeTrue();
});
tap.test('models Arcam FMJ source and power commands without pretending TCP success', async () => {
const executed: IArcamFmjModeledCommand[] = [];
const integration = new ArcamFmjIntegration();
const runtimeWithExecutor = await integration.setup({
snapshot,
commandExecutor: {
execute: async (commandArg) => {
executed.push(commandArg);
},
},
}, {});
const sourceResult = await runtimeWithExecutor.callService!({
domain: 'media_player',
service: 'select_source',
target: { entityId: 'media_player.living_room_arcam' },
data: { source: 'BD' },
});
expect(sourceResult.success).toBeTrue();
expect(executed[0].commandCodeName).toEqual('SIMULATE_RC5_IR_COMMAND');
expect(executed[0].data).toEqual([16, 4]);
expect(executed[0].usesRc5).toBeTrue();
const runtimeWithoutExecutor = await integration.setup({ snapshot }, {});
const turnOnResult = await runtimeWithoutExecutor.callService!({
domain: 'media_player',
service: 'turn_on',
target: { entityId: 'media_player.living_room_arcam' },
});
expect(turnOnResult.success).toBeFalse();
expect(turnOnResult.error).toContain('config.host or commandExecutor');
});
export default tap.start();