Add native local infrastructure integrations
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { HeosConfigFlow } from '../../ts/integrations/heos/index.js';
|
||||
|
||||
tap.test('creates HEOS config from discovered host and account options', async () => {
|
||||
const flow = new HeosConfigFlow();
|
||||
const step = await flow.start({
|
||||
source: 'ssdp',
|
||||
integrationDomain: 'heos',
|
||||
host: '192.168.1.80',
|
||||
name: 'Living Room HEOS',
|
||||
manufacturer: 'Denon',
|
||||
model: 'HEOS 7',
|
||||
serialNumber: 'HEOS12345',
|
||||
}, {});
|
||||
|
||||
expect(step.kind).toEqual('form');
|
||||
const done = await step.submit!({ username: 'listener@example.com', password: 'secret' });
|
||||
expect(done.kind).toEqual('done');
|
||||
expect(done.config?.host).toEqual('192.168.1.80');
|
||||
expect(done.config?.port).toEqual(1255);
|
||||
expect(done.config?.username).toEqual('listener@example.com');
|
||||
});
|
||||
|
||||
tap.test('requires a HEOS host for manual setup', async () => {
|
||||
const flow = new HeosConfigFlow();
|
||||
const step = await flow.start({ source: 'manual', integrationDomain: 'heos' }, {});
|
||||
const result = await step.submit!({});
|
||||
expect(result.kind).toEqual('error');
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
@@ -0,0 +1,56 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { createHeosDiscoveryDescriptor } from '../../ts/integrations/heos/index.js';
|
||||
|
||||
tap.test('matches Home Assistant HEOS SSDP records', async () => {
|
||||
const descriptor = createHeosDiscoveryDescriptor();
|
||||
const matcher = descriptor.getMatchers()[0];
|
||||
const result = await matcher.matches({
|
||||
st: 'urn:schemas-denon-com:device:ACT-Denon:1',
|
||||
usn: 'uuid:heos-123::urn:schemas-denon-com:device:ACT-Denon:1',
|
||||
location: 'http://192.168.1.80:60006/upnp/desc/aios_device/aios_device.xml',
|
||||
upnp: {
|
||||
manufacturer: 'Denon',
|
||||
modelName: 'HEOS 7',
|
||||
serialNumber: 'HEOS12345',
|
||||
friendlyName: 'Living Room HEOS',
|
||||
},
|
||||
}, {});
|
||||
|
||||
expect(result.matched).toBeTrue();
|
||||
expect(result.confidence).toEqual('certain');
|
||||
expect(result.normalizedDeviceId).toEqual('HEOS12345');
|
||||
expect(result.candidate?.host).toEqual('192.168.1.80');
|
||||
expect(result.candidate?.port).toEqual(1255);
|
||||
});
|
||||
|
||||
tap.test('matches HEOS zeroconf and manual candidates', async () => {
|
||||
const descriptor = createHeosDiscoveryDescriptor();
|
||||
const mdns = await descriptor.getMatchers()[1].matches({
|
||||
name: 'Kitchen HEOS',
|
||||
type: '_heos-audio._tcp.local.',
|
||||
host: 'kitchen-heos.local',
|
||||
port: 10101,
|
||||
txt: { model: 'HEOS 5', serial: 'HEOS5678' },
|
||||
}, {});
|
||||
expect(mdns.matched).toBeTrue();
|
||||
expect(mdns.candidate?.port).toEqual(1255);
|
||||
|
||||
const manual = await descriptor.getMatchers()[2].matches({ host: '192.168.1.81', name: 'Manual HEOS' }, {});
|
||||
expect(manual.matched).toBeTrue();
|
||||
expect(manual.candidate?.host).toEqual('192.168.1.81');
|
||||
|
||||
const validation = await descriptor.getValidators()[0].validate(manual.candidate!, {});
|
||||
expect(validation.matched).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('rejects unrelated SSDP records', async () => {
|
||||
const descriptor = createHeosDiscoveryDescriptor();
|
||||
const result = await descriptor.getMatchers()[0].matches({
|
||||
st: 'urn:schemas-upnp-org:device:Printer:1',
|
||||
location: 'http://192.168.1.90/device.xml',
|
||||
upnp: { manufacturer: 'Brother', modelName: 'Printer' },
|
||||
}, {});
|
||||
expect(result.matched).toBeFalse();
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
@@ -0,0 +1,104 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { HeosMapper, type IHeosSnapshot } from '../../ts/integrations/heos/index.js';
|
||||
|
||||
const snapshot: IHeosSnapshot = {
|
||||
system: {
|
||||
host: '192.168.1.80',
|
||||
currentHost: '192.168.1.80',
|
||||
signedInUsername: 'listener@example.com',
|
||||
isSignedIn: true,
|
||||
},
|
||||
players: [{
|
||||
name: 'Living Room',
|
||||
playerId: 101,
|
||||
model: 'Denon HEOS 7',
|
||||
serial: 'LR123',
|
||||
version: '3.40.0',
|
||||
ipAddress: '192.168.1.80',
|
||||
network: 'wired',
|
||||
state: 'play',
|
||||
volume: 45,
|
||||
muted: false,
|
||||
repeat: 'on_all',
|
||||
shuffle: true,
|
||||
groupId: 201,
|
||||
available: true,
|
||||
nowPlayingMedia: {
|
||||
type: 'station',
|
||||
station: 'Jazz Radio',
|
||||
albumId: 'fav-jazz',
|
||||
mediaId: 'station-1',
|
||||
sourceId: 3,
|
||||
duration: 180000,
|
||||
currentPosition: 30000,
|
||||
supportedControls: ['play', 'pause', 'stop'],
|
||||
},
|
||||
}, {
|
||||
name: 'Kitchen',
|
||||
playerId: 102,
|
||||
model: 'Denon HEOS 5',
|
||||
serial: 'KT456',
|
||||
version: '3.40.0',
|
||||
ipAddress: '192.168.1.81',
|
||||
network: 'wifi',
|
||||
state: 'pause',
|
||||
volume: 25,
|
||||
muted: true,
|
||||
groupId: 201,
|
||||
available: true,
|
||||
nowPlayingMedia: {
|
||||
type: 'station',
|
||||
station: 'HDMI ARC',
|
||||
mediaId: 'inputs/hdmi_arc_1',
|
||||
sourceId: 1027,
|
||||
},
|
||||
}],
|
||||
groups: [{
|
||||
name: 'Living Room + Kitchen',
|
||||
groupId: 201,
|
||||
leadPlayerId: 101,
|
||||
memberPlayerIds: [102],
|
||||
volume: 40,
|
||||
muted: false,
|
||||
}],
|
||||
favorites: {
|
||||
1: { sourceId: 1028, name: 'Jazz Radio', type: 'station', mediaId: 'fav-jazz', playable: true },
|
||||
},
|
||||
inputSources: [{
|
||||
sourceId: 1027,
|
||||
name: 'HDMI ARC',
|
||||
type: 'station',
|
||||
mediaId: 'inputs/hdmi_arc_1',
|
||||
playable: true,
|
||||
}],
|
||||
};
|
||||
|
||||
tap.test('maps HEOS players and system snapshot to devices', async () => {
|
||||
const devices = HeosMapper.toDevices(snapshot);
|
||||
expect(devices.some((deviceArg) => deviceArg.id === 'heos.system.192_168_1_80')).toBeTrue();
|
||||
const player = devices.find((deviceArg) => deviceArg.id === 'heos.player.101');
|
||||
expect(player?.manufacturer).toEqual('Denon');
|
||||
expect(player?.state.some((stateArg) => stateArg.featureId === 'source' && stateArg.value === 'Jazz Radio')).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('maps HEOS players groups sources and media entities', async () => {
|
||||
const entities = HeosMapper.toEntities(snapshot);
|
||||
const player = entities.find((entityArg) => entityArg.id === 'media_player.living_room');
|
||||
const kitchen = entities.find((entityArg) => entityArg.id === 'media_player.kitchen');
|
||||
const sources = entities.find((entityArg) => entityArg.id === 'sensor.heos_system_sources');
|
||||
const groups = entities.find((entityArg) => entityArg.id === 'sensor.heos_system_groups');
|
||||
const media = entities.find((entityArg) => entityArg.id === 'sensor.living_room_heos_media');
|
||||
|
||||
expect(player?.state).toEqual('playing');
|
||||
expect(player?.attributes?.volumeLevel).toEqual(0.45);
|
||||
expect(player?.attributes?.source).toEqual('Jazz Radio');
|
||||
expect(player?.attributes?.groupMembers).toEqual(['media_player.living_room', 'media_player.kitchen']);
|
||||
expect(player?.attributes?.mediaDuration).toEqual(180);
|
||||
expect(player?.attributes?.mediaPosition).toEqual(30);
|
||||
expect(kitchen?.attributes?.source).toEqual('HDMI ARC');
|
||||
expect(sources?.state).toEqual(2);
|
||||
expect(groups?.state).toEqual(1);
|
||||
expect(media?.state).toEqual('Jazz Radio');
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
@@ -0,0 +1,74 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { HeosIntegration, type IHeosRawCommandRequest, type IHeosSnapshot } from '../../ts/integrations/heos/index.js';
|
||||
|
||||
const snapshot: IHeosSnapshot = {
|
||||
system: { host: '192.168.1.80', currentHost: '192.168.1.80' },
|
||||
players: [{
|
||||
name: 'Living Room',
|
||||
playerId: 101,
|
||||
model: 'Denon HEOS 7',
|
||||
state: 'pause',
|
||||
volume: 20,
|
||||
groupId: 201,
|
||||
available: true,
|
||||
}, {
|
||||
name: 'Kitchen',
|
||||
playerId: 102,
|
||||
model: 'Denon HEOS 5',
|
||||
state: 'pause',
|
||||
volume: 30,
|
||||
groupId: 201,
|
||||
available: true,
|
||||
}],
|
||||
groups: [{ name: 'Downstairs', groupId: 201, leadPlayerId: 101, memberPlayerIds: [102] }],
|
||||
favorites: {
|
||||
1: { sourceId: 1028, name: 'Jazz Radio', type: 'station', mediaId: 'fav-jazz', playable: true },
|
||||
},
|
||||
inputSources: [{ sourceId: 1027, name: 'HDMI ARC', type: 'station', mediaId: 'inputs/hdmi_arc_1', playable: true }],
|
||||
};
|
||||
|
||||
tap.test('models HEOS media player commands through an executor', async () => {
|
||||
const commands: IHeosRawCommandRequest[] = [];
|
||||
const runtime = await new HeosIntegration().setup({
|
||||
snapshot,
|
||||
commandExecutor: {
|
||||
execute: async (requestArg) => {
|
||||
commands.push(requestArg);
|
||||
return { command: requestArg.command, result: true, message: {} };
|
||||
},
|
||||
},
|
||||
}, {});
|
||||
|
||||
const play = await runtime.callService!({ domain: 'media_player', service: 'media_play', target: { entityId: 'media_player.living_room' } });
|
||||
const volume = await runtime.callService!({ domain: 'media_player', service: 'volume_set', target: { entityId: 'media_player.living_room' }, data: { volume_level: 0.35 } });
|
||||
const source = await runtime.callService!({ domain: 'media_player', service: 'select_source', target: { entityId: 'media_player.living_room' }, data: { source: 'Jazz Radio' } });
|
||||
const join = await runtime.callService!({ domain: 'media_player', service: 'join', target: { entityId: 'media_player.living_room' }, data: { group_members: ['media_player.kitchen'] } });
|
||||
const groupVolume = await runtime.callService!({ domain: 'heos', service: 'group_volume_set', target: { entityId: 'media_player.living_room' }, data: { volume_level: 0.5 } });
|
||||
|
||||
expect(play.success).toBeTrue();
|
||||
expect(volume.success).toBeTrue();
|
||||
expect(source.success).toBeTrue();
|
||||
expect(join.success).toBeTrue();
|
||||
expect(groupVolume.success).toBeTrue();
|
||||
expect(commands.map((commandArg) => commandArg.command)).toEqual([
|
||||
'player/set_play_state',
|
||||
'player/set_volume',
|
||||
'browse/play_preset',
|
||||
'group/set_group',
|
||||
'group/set_volume',
|
||||
]);
|
||||
expect(commands[0].parameters).toEqual({ pid: 101, state: 'play' });
|
||||
expect(commands[1].parameters).toEqual({ pid: 101, level: 35 });
|
||||
expect(commands[2].parameters).toEqual({ pid: 101, preset: 1 });
|
||||
expect(commands[3].parameters).toEqual({ pid: '101,102' });
|
||||
expect(commands[4].parameters).toEqual({ gid: 201, level: 50 });
|
||||
});
|
||||
|
||||
tap.test('does not report live command success for static snapshots without transport', async () => {
|
||||
const runtime = await new HeosIntegration().setup({ snapshot }, {});
|
||||
const result = await runtime.callService!({ domain: 'media_player', service: 'media_play', target: { entityId: 'media_player.living_room' } });
|
||||
expect(result.success).toBeFalse();
|
||||
expect(result.error?.includes('config.host or commandExecutor')).toBeTrue();
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
Reference in New Issue
Block a user