Add native media and network integrations
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { createAndroidtvDiscoveryDescriptor } from '../../ts/integrations/androidtv/index.js';
|
||||
|
||||
tap.test('matches Android TV mDNS setup hints', async () => {
|
||||
const descriptor = createAndroidtvDiscoveryDescriptor();
|
||||
const matcher = descriptor.getMatchers()[0];
|
||||
const result = await matcher.matches({
|
||||
type: '_androidtvremote2._tcp.local.',
|
||||
name: 'Living Room TV._androidtvremote2._tcp.local.',
|
||||
host: 'living-room-tv.local',
|
||||
port: 6466,
|
||||
txt: {
|
||||
id: 'androidtv-123',
|
||||
fn: 'Living Room TV',
|
||||
md: 'Android TV',
|
||||
},
|
||||
}, {});
|
||||
expect(result.matched).toBeTrue();
|
||||
expect(result.candidate?.host).toEqual('living-room-tv.local');
|
||||
expect(result.candidate?.port).toEqual(5555);
|
||||
expect(result.normalizedDeviceId).toEqual('androidtv-123');
|
||||
});
|
||||
|
||||
tap.test('matches manual Android TV host entries', async () => {
|
||||
const descriptor = createAndroidtvDiscoveryDescriptor();
|
||||
const matcher = descriptor.getMatchers()[1];
|
||||
const result = await matcher.matches({
|
||||
host: '192.168.1.55',
|
||||
deviceName: 'Den Fire TV',
|
||||
manufacturer: 'Amazon',
|
||||
model: 'Fire TV Stick',
|
||||
}, {});
|
||||
expect(result.matched).toBeTrue();
|
||||
expect(result.candidate?.host).toEqual('192.168.1.55');
|
||||
expect(result.candidate?.metadata?.deviceClass).toEqual('firetv');
|
||||
});
|
||||
|
||||
tap.test('validates ADB host candidates', async () => {
|
||||
const descriptor = createAndroidtvDiscoveryDescriptor();
|
||||
const validator = descriptor.getValidators()[0];
|
||||
const result = await validator.validate({
|
||||
source: 'custom',
|
||||
integrationDomain: 'androidtv',
|
||||
host: '192.168.1.56',
|
||||
port: 5555,
|
||||
model: 'Android TV',
|
||||
}, {});
|
||||
expect(result.matched).toBeTrue();
|
||||
expect(result.normalizedDeviceId).toEqual('192.168.1.56');
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
@@ -0,0 +1,40 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { AndroidtvMapper } from '../../ts/integrations/androidtv/index.js';
|
||||
|
||||
const snapshot = {
|
||||
deviceInfo: {
|
||||
id: 'shield-tv-123',
|
||||
name: 'Living Room Shield',
|
||||
manufacturer: 'NVIDIA',
|
||||
model: 'SHIELD Android TV',
|
||||
deviceClass: 'androidtv' as const,
|
||||
host: '192.168.1.57',
|
||||
port: 5555,
|
||||
},
|
||||
state: {
|
||||
rawState: 'playing',
|
||||
available: true,
|
||||
currentAppId: 'com.netflix.ninja',
|
||||
runningAppIds: ['com.netflix.ninja', 'org.xbmc.kodi'],
|
||||
volumeLevel: 0.42,
|
||||
isVolumeMuted: false,
|
||||
hdmiInput: 'HW1',
|
||||
},
|
||||
apps: [
|
||||
{ id: 'com.netflix.ninja' },
|
||||
{ id: 'org.xbmc.kodi', name: 'Kodi' },
|
||||
],
|
||||
};
|
||||
|
||||
tap.test('maps Android TV snapshots to media devices and entities', async () => {
|
||||
const devices = AndroidtvMapper.toDevices(snapshot);
|
||||
const entities = AndroidtvMapper.toEntities(snapshot);
|
||||
expect(devices[0].id).toEqual('androidtv.device.shield_tv_123');
|
||||
expect(devices[0].state.some((stateArg) => stateArg.featureId === 'volume' && stateArg.value === 42)).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?.sourceList as string[]).includes('Kodi')).toBeTrue();
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
@@ -0,0 +1,54 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { createDenonavrDiscoveryDescriptor } from '../../ts/integrations/denonavr/index.js';
|
||||
|
||||
tap.test('matches Denon AVR SSDP records', async () => {
|
||||
const descriptor = createDenonavrDiscoveryDescriptor();
|
||||
const matcher = descriptor.getMatchers()[0];
|
||||
const result = await matcher.matches({
|
||||
st: 'urn:schemas-upnp-org:device:MediaRenderer:1',
|
||||
usn: 'uuid:denon-avr-123::urn:schemas-upnp-org:device:MediaRenderer:1',
|
||||
location: 'http://192.168.1.50:8080/description.xml',
|
||||
upnp: {
|
||||
manufacturer: 'Denon',
|
||||
modelName: 'AVR-X1700H',
|
||||
serialNumber: 'ABC12345',
|
||||
friendlyName: 'Living Room AVR',
|
||||
deviceType: 'urn:schemas-upnp-org:device:MediaRenderer:1',
|
||||
},
|
||||
}, {});
|
||||
expect(result.matched).toBeTrue();
|
||||
expect(result.candidate?.host).toEqual('192.168.1.50');
|
||||
expect(result.candidate?.manufacturer).toEqual('Denon');
|
||||
expect(result.normalizedDeviceId).toEqual('AVR-X1700H-ABC12345');
|
||||
});
|
||||
|
||||
tap.test('rejects HEOS speaker models', async () => {
|
||||
const descriptor = createDenonavrDiscoveryDescriptor();
|
||||
const matcher = descriptor.getMatchers()[0];
|
||||
const result = await matcher.matches({
|
||||
st: 'urn:schemas-upnp-org:device:MediaRenderer:1',
|
||||
location: 'http://192.168.1.51:8080/description.xml',
|
||||
upnp: {
|
||||
manufacturer: 'Denon',
|
||||
modelName: 'HEOS 5',
|
||||
serialNumber: 'HEOS123',
|
||||
},
|
||||
}, {});
|
||||
expect(result.matched).toBeFalse();
|
||||
});
|
||||
|
||||
tap.test('validates manual Marantz candidates', async () => {
|
||||
const descriptor = createDenonavrDiscoveryDescriptor();
|
||||
const validator = descriptor.getValidators()[0];
|
||||
const result = await validator.validate({
|
||||
source: 'manual',
|
||||
integrationDomain: 'denonavr',
|
||||
host: '192.168.1.52',
|
||||
manufacturer: 'Marantz',
|
||||
model: 'SR6015',
|
||||
}, {});
|
||||
expect(result.matched).toBeTrue();
|
||||
expect(result.confidence).toEqual('high');
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
@@ -0,0 +1,69 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { DenonavrMapper, type IDenonavrSnapshot } from '../../ts/integrations/denonavr/index.js';
|
||||
|
||||
const snapshot: IDenonavrSnapshot = {
|
||||
receiverInfo: {
|
||||
host: '192.168.1.50',
|
||||
port: 80,
|
||||
name: 'Living Room AVR',
|
||||
manufacturer: 'Denon',
|
||||
modelName: 'AVR-X1700H',
|
||||
serialNumber: 'ABC12345',
|
||||
receiverType: 'avr-x',
|
||||
},
|
||||
zones: [{
|
||||
zone: 'Main',
|
||||
name: 'Main Zone',
|
||||
power: 'ON',
|
||||
state: 'playing',
|
||||
volumeDb: -35,
|
||||
muted: false,
|
||||
source: 'Media Server',
|
||||
sourceList: ['TV', 'Media Server', 'Bluetooth'],
|
||||
soundMode: 'DOLBY DIGITAL',
|
||||
soundModeRaw: 'DOLBY AUDIO - DOLBY DIGITAL',
|
||||
dynamicEq: true,
|
||||
ecoMode: 'Auto',
|
||||
media: {
|
||||
title: 'Track One',
|
||||
artist: 'Artist One',
|
||||
album: 'Album One',
|
||||
contentType: 'music',
|
||||
},
|
||||
available: true,
|
||||
}, {
|
||||
zone: 'Zone2',
|
||||
name: 'Patio',
|
||||
power: 'STANDBY',
|
||||
state: 'off',
|
||||
volumeLevel: 0.25,
|
||||
muted: true,
|
||||
source: 'Tuner',
|
||||
available: true,
|
||||
}],
|
||||
};
|
||||
|
||||
tap.test('maps Denon AVR snapshots to canonical devices', async () => {
|
||||
const devices = DenonavrMapper.toDevices(snapshot);
|
||||
expect(devices[0].id).toEqual('denonavr.receiver.abc12345');
|
||||
expect(devices[0].manufacturer).toEqual('Denon');
|
||||
expect(devices[0].state.some((stateArg) => stateArg.featureId === 'main_source' && stateArg.value === 'Media Server')).toBeTrue();
|
||||
expect(devices[0].state.some((stateArg) => stateArg.featureId === 'zone2_power' && stateArg.value === 'off')).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('maps Denon AVR zones to media, sensor, and switch entities', async () => {
|
||||
const entities = DenonavrMapper.toEntities(snapshot);
|
||||
const media = entities.find((entityArg) => entityArg.id === 'media_player.living_room_avr');
|
||||
const source = entities.find((entityArg) => entityArg.id === 'sensor.living_room_avr_source');
|
||||
const mute = entities.find((entityArg) => entityArg.id === 'switch.living_room_avr_zone2_mute');
|
||||
const dynamicEq = entities.find((entityArg) => entityArg.id === 'switch.living_room_avr_dynamic_eq');
|
||||
|
||||
expect(media?.platform).toEqual('media_player');
|
||||
expect(media?.state).toEqual('playing');
|
||||
expect(media?.attributes?.volumeLevel).toEqual(0.45);
|
||||
expect(source?.state).toEqual('Media Server');
|
||||
expect(mute?.state).toEqual(true);
|
||||
expect(dynamicEq?.state).toEqual(true);
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
@@ -0,0 +1,48 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { createKodiDiscoveryDescriptor } from '../../ts/integrations/kodi/index.js';
|
||||
|
||||
tap.test('matches Kodi JSON-RPC mDNS records', async () => {
|
||||
const descriptor = createKodiDiscoveryDescriptor();
|
||||
const matcher = descriptor.getMatchers()[0];
|
||||
const result = await matcher.matches({
|
||||
type: '_xbmc-jsonrpc-h._tcp.local.',
|
||||
name: 'Living Room Kodi._xbmc-jsonrpc-h._tcp.local.',
|
||||
host: 'living-room-kodi.local',
|
||||
port: 8080,
|
||||
txt: {
|
||||
uuid: 'kodi-uuid-123',
|
||||
version: '21.0',
|
||||
},
|
||||
}, {});
|
||||
expect(result.matched).toBeTrue();
|
||||
expect(result.candidate?.host).toEqual('living-room-kodi.local');
|
||||
expect(result.candidate?.port).toEqual(8080);
|
||||
expect(result.normalizedDeviceId).toEqual('kodi-uuid-123');
|
||||
});
|
||||
|
||||
tap.test('matches manual Kodi host entries', async () => {
|
||||
const descriptor = createKodiDiscoveryDescriptor();
|
||||
const matcher = descriptor.getMatchers()[1];
|
||||
const result = await matcher.matches({
|
||||
host: '192.168.1.70',
|
||||
name: 'Living Room Kodi',
|
||||
}, {});
|
||||
expect(result.matched).toBeTrue();
|
||||
expect(result.candidate?.host).toEqual('192.168.1.70');
|
||||
expect(result.candidate?.port).toEqual(8080);
|
||||
});
|
||||
|
||||
tap.test('validates Kodi candidates', async () => {
|
||||
const descriptor = createKodiDiscoveryDescriptor();
|
||||
const validator = descriptor.getValidators()[0];
|
||||
const result = await validator.validate({
|
||||
source: 'mdns',
|
||||
integrationDomain: 'kodi',
|
||||
host: '192.168.1.71',
|
||||
port: 8080,
|
||||
}, {});
|
||||
expect(result.matched).toBeTrue();
|
||||
expect(result.confidence).toEqual('high');
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
@@ -0,0 +1,58 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { KodiMapper, type IKodiSnapshot } from '../../ts/integrations/kodi/index.js';
|
||||
|
||||
const snapshot: IKodiSnapshot = {
|
||||
deviceInfo: {
|
||||
uuid: 'kodi-uuid-123',
|
||||
name: 'Living Room Kodi',
|
||||
host: '192.168.1.70',
|
||||
port: 8080,
|
||||
manufacturer: 'Kodi',
|
||||
version: '21.0',
|
||||
},
|
||||
application: {
|
||||
name: 'Kodi',
|
||||
volume: 42,
|
||||
muted: false,
|
||||
version: { major: 21, minor: 0 },
|
||||
},
|
||||
players: [{ playerid: 1, type: 'video', playertype: 'internal' }],
|
||||
player: { playerid: 1, type: 'video', playertype: 'internal' },
|
||||
playerProperties: {
|
||||
speed: 1,
|
||||
time: { hours: 0, minutes: 3, seconds: 4 },
|
||||
totaltime: { hours: 1, minutes: 2, seconds: 3 },
|
||||
live: false,
|
||||
},
|
||||
item: {
|
||||
id: 77,
|
||||
type: 'movie',
|
||||
title: 'The Test Movie',
|
||||
file: 'smb://media/test.mkv',
|
||||
thumbnail: 'image://poster.jpg/',
|
||||
streamdetails: { video: [{ hdrtype: 'hdr10' }] },
|
||||
},
|
||||
online: true,
|
||||
};
|
||||
|
||||
tap.test('maps Kodi JSON-RPC snapshots to media devices', async () => {
|
||||
const devices = KodiMapper.toDevices(snapshot);
|
||||
expect(devices[0].id).toEqual('kodi.device.kodi_uuid_123');
|
||||
expect(devices[0].protocol).toEqual('http');
|
||||
expect(devices[0].state.some((stateArg) => stateArg.featureId === 'volume' && stateArg.value === 42)).toBeTrue();
|
||||
expect(devices[0].state.some((stateArg) => stateArg.featureId === 'current_title' && stateArg.value === 'The Test Movie')).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('maps Kodi JSON-RPC snapshots to media player entities', async () => {
|
||||
const entities = KodiMapper.toEntities(snapshot);
|
||||
expect(entities[0].id).toEqual('media_player.living_room_kodi');
|
||||
expect(entities[0].platform).toEqual('media_player');
|
||||
expect(entities[0].state).toEqual('playing');
|
||||
expect(entities[0].attributes?.volumeLevel).toEqual(0.42);
|
||||
expect(entities[0].attributes?.mediaContentType).toEqual('movie');
|
||||
expect(entities[0].attributes?.mediaDuration).toEqual(3723);
|
||||
expect(entities[0].attributes?.mediaPosition).toEqual(184);
|
||||
expect(entities[0].attributes?.dynamicRange).toEqual('hdr10');
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
@@ -0,0 +1,58 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { createSamsungtvDiscoveryDescriptor } from '../../ts/integrations/samsungtv/index.js';
|
||||
|
||||
tap.test('matches Samsung TV SSDP MainTVAgent records', async () => {
|
||||
const descriptor = createSamsungtvDiscoveryDescriptor();
|
||||
const matcher = descriptor.getMatchers()[0];
|
||||
const result = await matcher.matches({
|
||||
st: 'urn:samsung.com:service:MainTVAgent2:1',
|
||||
usn: 'uuid:tv-udn-123::urn:samsung.com:service:MainTVAgent2:1',
|
||||
location: 'http://192.168.1.55:8001/api/v2/',
|
||||
headers: {
|
||||
manufacturer: 'Samsung Electronics',
|
||||
modelName: 'QN90A',
|
||||
},
|
||||
}, {});
|
||||
|
||||
expect(result.matched).toBeTrue();
|
||||
expect(result.candidate?.host).toEqual('192.168.1.55');
|
||||
expect(result.normalizedDeviceId).toEqual('tv-udn-123');
|
||||
expect(result.candidate?.metadata?.ssdpMainTvAgentLocation).toEqual('http://192.168.1.55:8001/api/v2/');
|
||||
});
|
||||
|
||||
tap.test('matches Samsung TV mDNS AirPlay records', async () => {
|
||||
const descriptor = createSamsungtvDiscoveryDescriptor();
|
||||
const matcher = descriptor.getMatchers()[1];
|
||||
const result = await matcher.matches({
|
||||
type: '_airplay._tcp.local.',
|
||||
name: 'Living Room TV',
|
||||
host: 'living-room-tv.local',
|
||||
port: 7000,
|
||||
properties: {
|
||||
manufacturer: 'Samsung Electronics',
|
||||
deviceid: 'AA:BB:CC:DD:EE:FF',
|
||||
model: 'Tizen TV',
|
||||
},
|
||||
}, {});
|
||||
|
||||
expect(result.matched).toBeTrue();
|
||||
expect(result.candidate?.host).toEqual('living-room-tv.local');
|
||||
expect(result.candidate?.port).toEqual(8001);
|
||||
expect(result.normalizedDeviceId).toEqual('AA:BB:CC:DD:EE:FF');
|
||||
});
|
||||
|
||||
tap.test('validates Samsung TV candidates', async () => {
|
||||
const descriptor = createSamsungtvDiscoveryDescriptor();
|
||||
const validator = descriptor.getValidators()[0];
|
||||
const result = await validator.validate({
|
||||
source: 'manual',
|
||||
integrationDomain: 'samsungtv',
|
||||
host: '192.168.1.55',
|
||||
manufacturer: 'Samsung',
|
||||
}, {});
|
||||
|
||||
expect(result.matched).toBeTrue();
|
||||
expect(result.confidence).toEqual('high');
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
@@ -0,0 +1,39 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { SamsungtvMapper } from '../../ts/integrations/samsungtv/index.js';
|
||||
|
||||
const snapshot = {
|
||||
deviceInfo: {
|
||||
id: 'tv-udn-123',
|
||||
device: {
|
||||
type: 'Samsung SmartTV',
|
||||
name: '[TV] Living Room',
|
||||
modelName: 'QN90A',
|
||||
wifiMac: 'AA:BB:CC:DD:EE:FF',
|
||||
PowerState: 'on',
|
||||
},
|
||||
},
|
||||
state: {
|
||||
playback: 'playing' as const,
|
||||
volumeLevel: 35,
|
||||
muted: false,
|
||||
},
|
||||
apps: [
|
||||
{ id: '11101200001', name: 'Netflix' },
|
||||
{ id: '3201512006785', name: 'Prime Video' },
|
||||
],
|
||||
activeApp: { id: '11101200001', name: 'Netflix' },
|
||||
};
|
||||
|
||||
tap.test('maps Samsung TV snapshots to media devices and entities', async () => {
|
||||
const devices = SamsungtvMapper.toDevices(snapshot);
|
||||
const entities = SamsungtvMapper.toEntities(snapshot);
|
||||
|
||||
expect(devices[0].id).toEqual('samsungtv.device.tv_udn_123');
|
||||
expect(devices[0].state.some((stateArg) => stateArg.featureId === 'source' && stateArg.value === 'Netflix')).toBeTrue();
|
||||
expect(entities[0].id).toEqual('media_player.living_room');
|
||||
expect(entities[0].platform).toEqual('media_player');
|
||||
expect(entities[0].state).toEqual('playing');
|
||||
expect((entities[0].attributes?.sourceList as string[]).includes('Netflix')).toBeTrue();
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
@@ -0,0 +1,59 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { createTplinkDiscoveryDescriptor } from '../../ts/integrations/tplink/index.js';
|
||||
|
||||
tap.test('matches TP-Link Kasa/Tapo mDNS records', async () => {
|
||||
const descriptor = createTplinkDiscoveryDescriptor();
|
||||
const matcher = descriptor.getMatchers()[0];
|
||||
const result = await matcher.matches({
|
||||
type: '_tplink._tcp.local.',
|
||||
name: 'Living Plug._tplink._tcp.local.',
|
||||
host: 'living-plug.local',
|
||||
port: 80,
|
||||
txt: {
|
||||
model: 'KP125M',
|
||||
mac: 'F0-A7-31-00-11-22',
|
||||
alias: 'Living Plug',
|
||||
},
|
||||
}, {});
|
||||
|
||||
expect(result.matched).toBeTrue();
|
||||
expect(result.candidate?.integrationDomain).toEqual('tplink');
|
||||
expect(result.normalizedDeviceId).toEqual('f0:a7:31:00:11:22');
|
||||
});
|
||||
|
||||
tap.test('matches Home Assistant TP-Link DHCP rules', async () => {
|
||||
const descriptor = createTplinkDiscoveryDescriptor();
|
||||
const matcher = descriptor.getMatchers()[1];
|
||||
const result = await matcher.matches({
|
||||
hostname: 'HS110-Office',
|
||||
ipAddress: '192.168.1.44',
|
||||
macAddress: '50:C7:BF:AA:BB:CC',
|
||||
}, {});
|
||||
|
||||
expect(result.matched).toBeTrue();
|
||||
expect(result.candidate?.host).toEqual('192.168.1.44');
|
||||
});
|
||||
|
||||
tap.test('validates manual snapshot candidates', async () => {
|
||||
const descriptor = createTplinkDiscoveryDescriptor();
|
||||
const manualMatcher = descriptor.getMatchers()[2];
|
||||
const validator = descriptor.getValidators()[0];
|
||||
const manual = await manualMatcher.matches({
|
||||
host: '192.168.1.55',
|
||||
model: 'Tapo P110',
|
||||
alias: 'Desk Plug',
|
||||
snapshot: {
|
||||
connected: true,
|
||||
devices: [],
|
||||
entities: [],
|
||||
events: [],
|
||||
},
|
||||
}, {});
|
||||
const validated = await validator.validate(manual.candidate!, {});
|
||||
|
||||
expect(manual.matched).toBeTrue();
|
||||
expect(validated.matched).toBeTrue();
|
||||
expect(validated.metadata?.encryptedLocalProtocolImplemented).toEqual(false);
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
@@ -0,0 +1,97 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { TplinkMapper, type ITplinkSnapshot } from '../../ts/integrations/tplink/index.js';
|
||||
|
||||
const snapshot: ITplinkSnapshot = {
|
||||
connected: true,
|
||||
devices: [
|
||||
{
|
||||
id: 'bulb-1',
|
||||
alias: 'Living Lamp',
|
||||
model: 'KL130',
|
||||
type: 'bulb',
|
||||
macAddress: '1C:3B:F3:00:11:22',
|
||||
state: {
|
||||
state: true,
|
||||
brightness: 80,
|
||||
color_temperature: 3000,
|
||||
rgb: { r: 255, g: 100, b: 10 },
|
||||
current_consumption: 7.5,
|
||||
rssi: -53,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'strip-1',
|
||||
alias: 'Office Strip',
|
||||
model: 'HS300',
|
||||
type: 'strip',
|
||||
state: { state: true },
|
||||
children: [
|
||||
{
|
||||
id: 'strip-1-outlet-1',
|
||||
alias: 'Printer',
|
||||
model: 'HS300 Outlet',
|
||||
type: 'plug',
|
||||
state: {
|
||||
state: false,
|
||||
current_consumption: 2.25,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'sensor-1',
|
||||
alias: 'Back Door',
|
||||
model: 'T110',
|
||||
type: 'sensor',
|
||||
state: {
|
||||
is_open: true,
|
||||
battery_level: 92,
|
||||
},
|
||||
},
|
||||
],
|
||||
entities: [],
|
||||
events: [],
|
||||
};
|
||||
|
||||
tap.test('maps TP-Link plugs, strips, bulbs, and sensors', async () => {
|
||||
const devices = TplinkMapper.toDevices(snapshot);
|
||||
const entities = TplinkMapper.toEntities(snapshot);
|
||||
|
||||
expect(devices.some((deviceArg) => deviceArg.id === 'tplink.device.1c_3b_f3_00_11_22')).toBeTrue();
|
||||
expect(devices.some((deviceArg) => deviceArg.id === 'tplink.device.strip_1_outlet_1')).toBeTrue();
|
||||
expect(entities.find((entityArg) => entityArg.id === 'light.living_lamp')?.state).toEqual('on');
|
||||
expect(entities.find((entityArg) => entityArg.id === 'switch.printer')?.state).toEqual('off');
|
||||
expect(entities.find((entityArg) => entityArg.id === 'sensor.living_lamp_current_consumption')?.state).toEqual(7.5);
|
||||
expect(entities.find((entityArg) => entityArg.id === 'binary_sensor.back_door_is_open')?.state).toEqual('on');
|
||||
expect(entities.find((entityArg) => entityArg.id === 'sensor.back_door_battery_level')?.state).toEqual(92);
|
||||
});
|
||||
|
||||
tap.test('maps canonical services to TP-Link feature commands', async () => {
|
||||
const turnOnCommand = TplinkMapper.commandForService(snapshot, {
|
||||
domain: 'light',
|
||||
service: 'turn_on',
|
||||
target: { entityId: 'light.living_lamp' },
|
||||
data: { brightness_pct: 50, color_temp_kelvin: 2700, rgb_color: [10, 20, 30] },
|
||||
});
|
||||
const switchCommand = TplinkMapper.commandForService(snapshot, {
|
||||
domain: 'switch',
|
||||
service: 'turn_off',
|
||||
target: { entityId: 'switch.printer' },
|
||||
});
|
||||
const numberCommand = TplinkMapper.commandForService(snapshot, {
|
||||
domain: 'number',
|
||||
service: 'set_value',
|
||||
target: { deviceId: 'tplink.device.1c_3b_f3_00_11_22' },
|
||||
data: { featureId: 'brightness', value: 30 },
|
||||
});
|
||||
|
||||
expect(turnOnCommand?.payload.brightness).toEqual(50);
|
||||
expect(turnOnCommand?.payload.color_temperature).toEqual(2700);
|
||||
expect(turnOnCommand?.payload.rgb).toEqual({ r: 10, g: 20, b: 30 });
|
||||
expect(switchCommand?.featureId).toEqual('state');
|
||||
expect(switchCommand?.value).toEqual(false);
|
||||
expect(numberCommand?.featureId).toEqual('brightness');
|
||||
expect(numberCommand?.value).toEqual(30);
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
@@ -0,0 +1,59 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { createUnifiDiscoveryDescriptor } from '../../ts/integrations/unifi/index.js';
|
||||
|
||||
tap.test('matches UniFi mDNS, SSDP, manual, and discovery records', async () => {
|
||||
const descriptor = createUnifiDiscoveryDescriptor();
|
||||
const mdnsMatcher = descriptor.getMatchers()[0];
|
||||
const ssdpMatcher = descriptor.getMatchers()[1];
|
||||
const manualMatcher = descriptor.getMatchers()[2];
|
||||
const discoveryMatcher = descriptor.getMatchers()[3];
|
||||
|
||||
const mdnsResult = await mdnsMatcher.matches({
|
||||
type: '_unifi._tcp.local.',
|
||||
name: 'UniFi Network._unifi._tcp.local.',
|
||||
host: 'unifi.local',
|
||||
txt: {
|
||||
mac: 'b4fbe4123456',
|
||||
model: 'UniFi Dream Machine',
|
||||
controller_uuid: 'controller-1',
|
||||
},
|
||||
}, {});
|
||||
const ssdpResult = await ssdpMatcher.matches({
|
||||
location: 'https://192.168.1.1:443/description.xml',
|
||||
manufacturer: 'Ubiquiti Networks',
|
||||
modelDescription: 'UniFi Dream Machine',
|
||||
usn: 'uuid:udm-1',
|
||||
}, {});
|
||||
const manualResult = await manualMatcher.matches({
|
||||
host: '192.168.1.2',
|
||||
site: 'default',
|
||||
model: 'UniFi Network',
|
||||
}, {});
|
||||
const discoveryResult = await discoveryMatcher.matches({
|
||||
source_ip: '192.168.1.1',
|
||||
hw_addr: 'B4:FB:E4:12:34:56',
|
||||
services: { network: true },
|
||||
}, {});
|
||||
|
||||
expect(mdnsResult.matched).toBeTrue();
|
||||
expect(mdnsResult.normalizedDeviceId).toEqual('controller-1');
|
||||
expect(ssdpResult.matched).toBeTrue();
|
||||
expect(ssdpResult.candidate?.host).toEqual('192.168.1.1');
|
||||
expect(manualResult.matched).toBeTrue();
|
||||
expect(manualResult.candidate?.port).toEqual(443);
|
||||
expect(discoveryResult.normalizedDeviceId).toEqual('b4:fb:e4:12:34:56');
|
||||
});
|
||||
|
||||
tap.test('validates UniFi candidates', async () => {
|
||||
const validator = createUnifiDiscoveryDescriptor().getValidators()[0];
|
||||
const result = await validator.validate({
|
||||
source: 'ssdp',
|
||||
host: '192.168.1.1',
|
||||
manufacturer: 'Ubiquiti Networks',
|
||||
model: 'UniFi Dream Machine SE',
|
||||
}, {});
|
||||
expect(result.matched).toBeTrue();
|
||||
expect(result.confidence).toEqual('high');
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
@@ -0,0 +1,113 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { UnifiMapper, type IUnifiSnapshot } from '../../ts/integrations/unifi/index.js';
|
||||
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
|
||||
const snapshot: IUnifiSnapshot = {
|
||||
connected: true,
|
||||
host: '192.168.1.1',
|
||||
port: 443,
|
||||
site: 'default',
|
||||
controller: {
|
||||
id: 'controller-1',
|
||||
name: 'UniFi Network',
|
||||
version: '9.0.0',
|
||||
connected: true,
|
||||
},
|
||||
sites: [{ id: 'site-1', name: 'default', description: 'Default' }],
|
||||
clients: [
|
||||
{
|
||||
mac: 'aa:bb:cc:dd:ee:ff',
|
||||
name: 'Kitchen Phone',
|
||||
ip: '192.168.1.55',
|
||||
essid: 'Guest WiFi',
|
||||
is_wired: false,
|
||||
blocked: false,
|
||||
last_seen: now,
|
||||
ap_mac: 'b4:fb:e4:12:34:56',
|
||||
'rx_bytes-r': 2000000,
|
||||
'tx_bytes-r': 1000000,
|
||||
rssi: -55,
|
||||
},
|
||||
],
|
||||
devices: [
|
||||
{
|
||||
mac: 'b4:fb:e4:12:34:56',
|
||||
name: 'Switch 24',
|
||||
model: 'USW-24-PoE',
|
||||
version: '6.6.1',
|
||||
ip: '192.168.1.10',
|
||||
state: 1,
|
||||
uptime: 3600,
|
||||
general_temperature: 42,
|
||||
'system-stats': { cpu: '12.5', mem: '31.1' },
|
||||
port_table: [
|
||||
{
|
||||
deviceMac: 'b4:fb:e4:12:34:56',
|
||||
port_idx: 1,
|
||||
name: 'Office AP',
|
||||
enable: true,
|
||||
up: true,
|
||||
port_poe: true,
|
||||
poe_mode: 'auto',
|
||||
poe_power: '7.2',
|
||||
speed: 1000,
|
||||
'rx_bytes-r': 1024,
|
||||
'tx_bytes-r': 2048,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
wlans: [
|
||||
{
|
||||
_id: 'wlan-1',
|
||||
name: 'Guest WiFi',
|
||||
enabled: true,
|
||||
security: 'wpapsk',
|
||||
is_guest: true,
|
||||
},
|
||||
],
|
||||
ports: [],
|
||||
events: [],
|
||||
};
|
||||
|
||||
tap.test('maps UniFi clients, devices, WLANs, and ports', async () => {
|
||||
const devices = UnifiMapper.toDevices(snapshot);
|
||||
const entities = UnifiMapper.toEntities(snapshot);
|
||||
|
||||
expect(devices.some((deviceArg) => deviceArg.id === 'unifi.client.aa_bb_cc_dd_ee_ff')).toBeTrue();
|
||||
expect(devices.some((deviceArg) => deviceArg.id === 'unifi.device.b4_fb_e4_12_34_56')).toBeTrue();
|
||||
expect(devices.some((deviceArg) => deviceArg.id === 'unifi.wlan.wlan_1')).toBeTrue();
|
||||
expect(entities.find((entityArg) => entityArg.id === 'binary_sensor.kitchen_phone_connected')?.state).toEqual('on');
|
||||
expect(entities.find((entityArg) => entityArg.id === 'switch.guest_wifi')?.state).toEqual('on');
|
||||
expect(entities.find((entityArg) => entityArg.id === 'switch.switch_24_office_ap_poe')?.state).toEqual('on');
|
||||
expect(entities.find((entityArg) => entityArg.id === 'sensor.switch_24_office_ap_poe_power')?.state).toEqual(7.2);
|
||||
});
|
||||
|
||||
tap.test('maps services to UniFi commands', async () => {
|
||||
const blockCommand = UnifiMapper.commandForService(snapshot, {
|
||||
domain: 'unifi',
|
||||
service: 'block_client',
|
||||
target: {},
|
||||
data: { mac: 'aa:bb:cc:dd:ee:ff' },
|
||||
});
|
||||
const wlanCommand = UnifiMapper.commandForService(snapshot, {
|
||||
domain: 'switch',
|
||||
service: 'turn_off',
|
||||
target: { entityId: 'switch.guest_wifi' },
|
||||
});
|
||||
const poeCommand = UnifiMapper.commandForService(snapshot, {
|
||||
domain: 'switch',
|
||||
service: 'turn_off',
|
||||
target: { entityId: 'switch.switch_24_office_ap_poe' },
|
||||
});
|
||||
|
||||
expect(blockCommand?.type).toEqual('blockClient');
|
||||
expect(blockCommand?.block).toBeTrue();
|
||||
expect(wlanCommand?.type).toEqual('setWlanEnabled');
|
||||
expect(wlanCommand?.enabled).toBeFalse();
|
||||
expect(poeCommand?.type).toEqual('setPoePortEnabled');
|
||||
expect(poeCommand?.portIdx).toEqual('1');
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
Reference in New Issue
Block a user