Add native local infrastructure integrations

This commit is contained in:
2026-05-05 19:06:21 +00:00
parent cfab8c593e
commit a144ef687c
70 changed files with 11607 additions and 183 deletions
+31
View File
@@ -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();
+56
View File
@@ -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();
+104
View File
@@ -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();
+74
View File
@@ -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();