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,52 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { AndroidtvRemoteConfigFlow, createAndroidtvRemoteDiscoveryDescriptor } from '../../ts/integrations/androidtv_remote/index.js';
tap.test('matches Android TV Remote mDNS advertisements', async () => {
const descriptor = createAndroidtvRemoteDiscoveryDescriptor();
const matcher = descriptor.getMatchers()[0];
const result = await matcher.matches({
type: '_androidtvremote2._tcp.local.',
name: 'Living Room TV._androidtvremote2._tcp.local.',
host: '192.168.1.61',
port: 6466,
properties: {
bt: 'AA:BB:CC:DD:EE:FF',
},
}, {});
expect(result.matched).toBeTrue();
expect(result.candidate?.integrationDomain).toEqual('androidtv_remote');
expect(result.candidate?.host).toEqual('192.168.1.61');
expect(result.candidate?.port).toEqual(6466);
expect(result.candidate?.macAddress).toEqual('AA:BB:CC:DD:EE:FF');
expect(result.normalizedDeviceId).toEqual('AA:BB:CC:DD:EE:FF');
});
tap.test('matches manual Android TV Remote host entries', async () => {
const descriptor = createAndroidtvRemoteDiscoveryDescriptor();
const matcher = descriptor.getMatchers()[1];
const result = await matcher.matches({
host: '192.168.1.62',
deviceName: 'Bedroom Google TV',
macAddress: '11:22:33:44:55:66',
}, {});
expect(result.matched).toBeTrue();
expect(result.candidate?.host).toEqual('192.168.1.62');
expect(result.candidate?.port).toEqual(6466);
expect(result.candidate?.metadata?.pairPort).toEqual(6467);
});
tap.test('creates manual host config flow entries', async () => {
const flow = new AndroidtvRemoteConfigFlow();
const step = await flow.start({ source: 'manual', host: '192.168.1.63', name: 'Office TV' }, {});
const done = await step.submit?.({ host: '192.168.1.63', enableIme: false });
expect(done?.kind).toEqual('done');
expect(done?.config?.host).toEqual('192.168.1.63');
expect(done?.config?.apiPort).toEqual(6466);
expect(done?.config?.pairPort).toEqual(6467);
expect(done?.config?.enableIme).toBeFalse();
});
export default tap.start();
@@ -0,0 +1,59 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { AndroidtvRemoteMapper } from '../../ts/integrations/androidtv_remote/index.js';
const snapshot = {
deviceInfo: {
name: 'Living Room Google TV',
host: '192.168.1.61',
apiPort: 6466,
pairPort: 6467,
macAddress: 'AA:BB:CC:DD:EE:FF',
manufacturer: 'Google',
model: 'Chromecast',
},
state: {
available: true,
isOn: true,
mediaState: 'playing',
currentApp: 'com.netflix.ninja',
volumeInfo: {
level: 7,
max: 10,
muted: false,
},
},
apps: [
{ id: 'com.netflix.ninja' },
{ id: 'com.plexapp.android', name: 'Plex', icon: 'https://example.invalid/plex.png' },
],
};
tap.test('maps Android TV Remote snapshots to media devices and entities', async () => {
const devices = AndroidtvRemoteMapper.toDevices(snapshot);
const entities = AndroidtvRemoteMapper.toEntities(snapshot);
expect(devices[0].id).toEqual('androidtv_remote.device.aa_bb_cc_dd_ee_ff');
expect(devices[0].metadata?.protocol).toEqual('androidtvremote2');
expect(devices[0].state.some((stateArg) => stateArg.featureId === 'volume' && stateArg.value === 70)).toBeTrue();
expect(devices[0].state.some((stateArg) => stateArg.featureId === 'activity' && stateArg.value === 'Netflix')).toBeTrue();
expect(entities[0].platform).toEqual('media_player');
expect(entities[0].state).toEqual('playing');
expect(entities[0].attributes?.source).toEqual('Netflix');
expect(entities[0].attributes?.volumeLevel).toEqual(0.7);
expect((entities[0].attributes?.activityList as string[]).includes('Plex')).toBeTrue();
});
tap.test('maps Android TV Remote power-off snapshots to off state', async () => {
const entities = AndroidtvRemoteMapper.toEntities({
...snapshot,
state: {
available: true,
isOn: false,
mediaState: 'off',
},
});
expect(entities[0].state).toEqual('off');
});
export default tap.start();
@@ -0,0 +1,68 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { AndroidtvRemoteIntegration, type IAndroidtvRemoteCommand, type IAndroidtvRemoteConfig } from '../../ts/integrations/androidtv_remote/index.js';
const baseConfig = (commandsArg: IAndroidtvRemoteCommand[] = []): IAndroidtvRemoteConfig => ({
host: '192.168.1.61',
snapshot: {
deviceInfo: {
name: 'Living Room Google TV',
host: '192.168.1.61',
macAddress: 'AA:BB:CC:DD:EE:FF',
},
state: {
available: true,
isOn: false,
currentApp: 'com.google.android.tvlauncher',
volumeInfo: {
level: 5,
max: 10,
muted: false,
},
},
apps: [
{ id: 'com.netflix.ninja' },
{ id: 'com.google.android.tvlauncher', name: 'Launcher' },
],
},
executor: async (commandArg) => {
commandsArg.push(commandArg);
},
});
tap.test('models media, app, and remote commands through injected executor', async () => {
const commands: IAndroidtvRemoteCommand[] = [];
const runtime = await new AndroidtvRemoteIntegration().setup(baseConfig(commands), {});
expect((await runtime.callService?.({ domain: 'media_player', service: 'turn_on', target: {} }))?.success).toBeTrue();
expect(commands[0].action).toEqual('key_command');
expect(commands[0].reason).toEqual('turn_on');
expect(commands[0].keyCode).toEqual('POWER');
expect(commands[0].direction).toEqual('SHORT');
expect((await runtime.callService?.({ domain: 'media_player', service: 'select_source', target: {}, data: { source: 'Netflix' } }))?.success).toBeTrue();
expect(commands[1].action).toEqual('launch_app');
expect(commands[1].reason).toEqual('select_activity');
expect(commands[1].appId).toEqual('com.netflix.ninja');
expect((await runtime.callService?.({ domain: 'remote', service: 'send_command', target: {}, data: { command: ['left', 'center'], num_repeats: 2, hold_secs: 0.25 } }))?.success).toBeTrue();
expect(commands[2].action).toEqual('remote_send_command');
expect(commands[2].repeats).toEqual(2);
expect(commands[2].holdSecs).toEqual(0.25);
expect(commands[2].keys?.map((keyArg) => keyArg.keyCode)).toEqual(['DPAD_LEFT', 'DPAD_LEFT', 'DPAD_CENTER', 'DPAD_CENTER']);
expect(commands[2].keys?.map((keyArg) => keyArg.direction)).toEqual(['START_LONG', 'END_LONG', 'START_LONG', 'END_LONG']);
expect((await runtime.callService?.({ domain: 'androidtv_remote', service: 'finish_pairing', target: {}, data: { pin: '123456' } }))?.success).toBeTrue();
expect(commands[3].action).toEqual('finish_pairing');
expect(commands[3].reason).toEqual('finish_pairing');
expect(commands[3].pin).toEqual('123456');
});
tap.test('returns explicit unsupported errors without an executor', async () => {
const runtime = await new AndroidtvRemoteIntegration().setup({ host: '192.168.1.61' }, {});
const result = await runtime.callService?.({ domain: 'media_player', service: 'volume_up', target: {} });
expect(result?.success).toBeFalse();
expect(result?.error).toContain('requires an injected executor');
});
export default tap.start();