Add native camera and media service integrations

This commit is contained in:
2026-05-05 17:13:54 +00:00
parent 489d9d5243
commit e7441844c9
112 changed files with 18608 additions and 327 deletions
+50
View File
@@ -0,0 +1,50 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { createAxisDiscoveryDescriptor } from '../../ts/integrations/axis/index.js';
tap.test('matches Axis mDNS records by service type and OUI', async () => {
const descriptor = createAxisDiscoveryDescriptor();
const matcher = descriptor.getMatchers().find((matcherArg) => matcherArg.id === 'axis-mdns-match');
const result = await matcher!.matches({
type: '_axis-video._tcp.local.',
name: 'AXIS P3265._axis-video._tcp.local.',
host: 'axis-p3265.local',
port: 80,
txt: {
macaddress: 'ACCC8E123456',
},
}, {});
expect(result.matched).toBeTrue();
expect(result.normalizedDeviceId).toEqual('accc8e123456');
expect(result.candidate?.integrationDomain).toEqual('axis');
});
tap.test('matches Axis SSDP records by manufacturer', async () => {
const descriptor = createAxisDiscoveryDescriptor();
const matcher = descriptor.getMatchers().find((matcherArg) => matcherArg.id === 'axis-ssdp-match');
const result = await matcher!.matches({
manufacturer: 'AXIS',
location: 'http://192.168.1.50:80/',
upnp: {
friendlyName: 'AXIS Door Station',
serialNumber: '00408C654321',
modelName: 'I8116-E',
},
}, {});
expect(result.matched).toBeTrue();
expect(result.candidate?.host).toEqual('192.168.1.50');
expect(result.candidate?.model).toEqual('I8116-E');
});
tap.test('validates manual Axis candidates', async () => {
const descriptor = createAxisDiscoveryDescriptor();
const matcher = descriptor.getMatchers().find((matcherArg) => matcherArg.id === 'axis-manual-match');
const match = await matcher!.matches({ host: 'axis.local', protocol: 'http', model: 'AXIS P3265' }, {});
expect(match.matched).toBeTrue();
const validator = descriptor.getValidators()[0];
const validation = await validator.validate(match.candidate!, {});
expect(validation.matched).toBeTrue();
expect(validation.candidate?.manufacturer).toEqual('Axis Communications AB');
});
export default tap.start();
+68
View File
@@ -0,0 +1,68 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { AxisMapper, type IAxisSnapshot } from '../../ts/integrations/axis/index.js';
const snapshot: IAxisSnapshot = {
deviceInfo: {
id: 'accc8e123456',
serialNumber: 'ACCC8E123456',
macAddress: 'accc8e123456',
name: 'Front Door Axis',
manufacturer: 'Axis Communications AB',
model: 'I8116-E',
firmwareVersion: '11.10.0',
host: '192.168.1.50',
port: 80,
protocol: 'http',
},
cameras: [{
id: '1',
name: 'Front Door Camera',
enabled: true,
videoSource: 1,
snapshotUrl: 'http://192.168.1.50/axis-cgi/jpg/image.cgi?camera=1',
mjpegUrl: 'http://192.168.1.50/axis-cgi/mjpg/video.cgi?camera=1',
rtspUrl: 'rtsp://192.168.1.50/axis-media/media.amp?videocodec=h264&camera=1',
supportsPtz: true,
}],
sensors: [{ id: 'firmware_version', name: 'Firmware version', value: '11.10.0' }],
binarySensors: [{ id: 'port_0', name: 'Call button', isOn: false, deviceClass: 'connectivity', source: '0' }],
events: [{ id: 'doorbell', name: 'Doorbell', topicBase: 'tns1:Device/tnsaxis:IO/Port', isTripped: false, deviceClass: 'doorbell' }],
ports: [{ id: '0', name: 'Call button', direction: 'input', state: 'open', normalState: 'open' }],
relays: [{ id: '1', name: 'Door strike', direction: 'output', state: 'open', normalState: 'open' }],
switches: [{ id: '1', name: 'Door strike', direction: 'output', state: 'open', normalState: 'open' }],
lights: [],
apiDiscovery: [{ id: 'io-port-management', version: '1.0' }, { id: 'ptz-control', version: '1.0' }],
connected: true,
updatedAt: '2026-01-01T00:00:00.000Z',
};
tap.test('maps Axis cameras, sensors, events, and relays', async () => {
const devices = AxisMapper.toDevices(snapshot);
const entities = AxisMapper.toEntities(snapshot);
expect(devices.length).toEqual(1);
expect(devices[0].features.some((featureArg) => featureArg.capability === 'camera')).toBeTrue();
expect(entities.some((entityArg) => entityArg.id === 'camera.front_door_camera')).toBeTrue();
expect(entities.some((entityArg) => entityArg.id === 'switch.door_strike')).toBeTrue();
expect(entities.some((entityArg) => entityArg.id === 'binary_sensor.call_button')).toBeTrue();
expect(entities.some((entityArg) => entityArg.id === 'event.doorbell')).toBeTrue();
});
tap.test('maps relay and PTZ service commands', async () => {
const relayCommand = AxisMapper.relayCommandForService(snapshot, {
domain: 'switch',
service: 'turn_on',
target: { entityId: 'switch.door_strike' },
});
expect(relayCommand).toEqual({ portId: '1', state: 'closed' });
const ptzCommand = AxisMapper.ptzCommandForService(snapshot, {
domain: 'axis',
service: 'ptz_control',
target: {},
data: { camera: '1', move: 'left', speed: 50 },
});
expect(ptzCommand?.move).toEqual('left');
expect(ptzCommand?.speed).toEqual(50);
});
export default tap.start();
@@ -0,0 +1,68 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { createBraviatvDiscoveryDescriptor } from '../../ts/integrations/braviatv/index.js';
tap.test('matches Sony Bravia ScalarWebAPI SSDP records', async () => {
const descriptor = createBraviatvDiscoveryDescriptor();
const matcher = descriptor.getMatchers()[0];
const result = await matcher.matches({
st: 'urn:schemas-sony-com:service:ScalarWebAPI:1',
usn: 'uuid:bravia-udn-123::urn:schemas-sony-com:service:ScalarWebAPI:1',
location: 'http://192.168.1.80:52323/dmr.xml',
headers: {
manufacturer: 'Sony Corporation',
},
upnp: {
friendlyName: 'Living Room Bravia',
modelName: 'XR-55A80J',
X_ScalarWebAPI_DeviceInfo: {
X_ScalarWebAPI_BaseURL: 'http://192.168.1.80/sony',
X_ScalarWebAPI_ServiceList: {
X_ScalarWebAPI_ServiceType: ['guide', 'system', 'audio', 'avContent', 'appControl', 'videoScreen'],
},
},
},
}, {});
expect(result.matched).toBeTrue();
expect(result.candidate?.host).toEqual('192.168.1.80');
expect(result.normalizedDeviceId).toEqual('bravia-udn-123');
expect((result.candidate?.metadata?.scalarWebApiServices as string[]).includes('videoScreen')).toBeTrue();
});
tap.test('matches Sony Bravia mDNS records', async () => {
const descriptor = createBraviatvDiscoveryDescriptor();
const matcher = descriptor.getMatchers()[1];
const result = await matcher.matches({
type: '_sonyapilib._tcp.local.',
name: 'Living Room Bravia._sonyapilib._tcp.local.',
host: 'living-room-bravia.local',
port: 80,
txt: {
manufacturer: 'Sony Corporation',
model: 'BRAVIA XR',
cid: 'sony-cid-123',
},
}, {});
expect(result.matched).toBeTrue();
expect(result.candidate?.host).toEqual('living-room-bravia.local');
expect(result.candidate?.port).toEqual(80);
expect(result.normalizedDeviceId).toEqual('sony-cid-123');
});
tap.test('validates Sony Bravia candidates', async () => {
const descriptor = createBraviatvDiscoveryDescriptor();
const validator = descriptor.getValidators()[0];
const result = await validator.validate({
source: 'manual',
integrationDomain: 'braviatv',
host: '192.168.1.81',
manufacturer: 'Sony',
model: 'BRAVIA',
}, {});
expect(result.matched).toBeTrue();
expect(result.confidence).toEqual('high');
});
export default tap.start();
@@ -0,0 +1,48 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { BraviatvMapper } from '../../ts/integrations/braviatv/index.js';
const snapshot = {
systemInfo: {
cid: 'sony-cid-123',
macAddr: 'AA:BB:CC:DD:EE:FF',
name: 'Living Room Bravia',
model: 'XR-55A80J',
serial: '1000001',
generation: '6.5.0',
},
state: {
powerStatus: 'active' as const,
power: 'on' as const,
playback: 'playing' as const,
source: 'HDMI 1',
sourceUri: 'extInput:hdmi?port=1',
volumeLevel: 0.35,
muted: false,
mediaTitle: 'Movie Night',
},
sources: [
{ title: 'HDMI 1', uri: 'extInput:hdmi?port=1', type: 'input' as const },
{ title: 'HDMI 2', uri: 'extInput:hdmi?port=2', type: 'input' as const },
],
apps: [
{ title: 'Netflix', uri: 'com.sony.dtv.com.netflix.ninja', type: 'app' as const },
{ title: 'YouTube', uri: 'com.sony.dtv.com.google.android.youtube.tv', type: 'app' as const },
],
channels: [],
};
tap.test('maps Sony Bravia snapshots to media devices and entities', async () => {
const devices = BraviatvMapper.toDevices(snapshot);
const entities = BraviatvMapper.toEntities(snapshot);
expect(devices[0].id).toEqual('braviatv.device.sony_cid_123');
expect(devices[0].state.some((stateArg) => stateArg.featureId === 'source' && stateArg.value === 'HDMI 1')).toBeTrue();
expect(devices[0].state.some((stateArg) => stateArg.featureId === 'volume' && stateArg.value === 35)).toBeTrue();
expect(entities[0].id).toEqual('media_player.living_room_bravia');
expect(entities[0].platform).toEqual('media_player');
expect(entities[0].state).toEqual('playing');
expect((entities[0].attributes?.sourceList as string[]).includes('Netflix')).toBeTrue();
expect(entities[0].attributes?.volumeLevel).toEqual(0.35);
});
export default tap.start();
@@ -0,0 +1,37 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { createDlnaDmrDiscoveryDescriptor } from '../../ts/integrations/dlna_dmr/index.js';
tap.test('matches DLNA DMR SSDP records', async () => {
const descriptor = createDlnaDmrDiscoveryDescriptor();
const matcher = descriptor.getMatchers()[0];
const result = await matcher.matches({
st: 'urn:schemas-upnp-org:device:MediaRenderer:1',
usn: 'uuid:renderer-1::urn:schemas-upnp-org:device:MediaRenderer:1',
location: 'http://192.168.1.50:8000/description.xml',
upnp: {
friendlyName: 'Living Room Renderer',
manufacturer: 'Example',
modelName: 'Renderer',
serviceList: {
service: [
{ serviceId: 'urn:upnp-org:serviceId:AVTransport' },
{ serviceId: 'urn:upnp-org:serviceId:ConnectionManager' },
{ serviceId: 'urn:upnp-org:serviceId:RenderingControl' },
],
},
},
}, {});
expect(result.matched).toBeTrue();
expect(result.normalizedDeviceId).toEqual('uuid:renderer-1');
expect(result.candidate?.integrationDomain).toEqual('dlna_dmr');
});
tap.test('matches manual DLNA DMR entries', async () => {
const descriptor = createDlnaDmrDiscoveryDescriptor();
const matcher = descriptor.getMatchers()[1];
const result = await matcher.matches({ url: 'http://renderer.local/device.xml', name: 'Manual Renderer' }, {});
expect(result.matched).toBeTrue();
expect(result.candidate?.metadata?.location).toEqual('http://renderer.local/device.xml');
});
export default tap.start();
@@ -0,0 +1,56 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { DlnaDmrMapper } from '../../ts/integrations/dlna_dmr/index.js';
import type { IDlnaDmrSnapshot } from '../../ts/integrations/dlna_dmr/index.js';
tap.test('maps DLNA renderer snapshots to canonical devices and entities', async () => {
const snapshot: IDlnaDmrSnapshot = {
device: {
location: 'http://192.168.1.50:8000/description.xml',
udn: 'uuid:renderer-1',
deviceType: 'urn:schemas-upnp-org:device:MediaRenderer:1',
friendlyName: 'Living Room Renderer',
manufacturer: 'Example',
modelName: 'DMR 1',
services: {},
},
state: {
online: true,
transport: {
currentTransportState: 'PLAYING',
currentTransportActions: ['Play', 'Pause', 'Stop'],
},
rendering: {
volume: 42,
muted: false,
presets: ['FactoryDefaults', 'Movie'],
selectedPreset: 'Movie',
},
media: {
currentTrackUri: 'http://media.local/song.mp3',
metadata: {
title: 'Test Song',
artist: 'Test Artist',
album: 'Test Album',
upnpClass: 'object.item.audioItem.musicTrack',
},
position: {
trackDurationSeconds: 240,
relativeTimeSeconds: 12,
},
},
sinkProtocolInfo: ['http-get:*:audio/mpeg:*'],
updatedAt: '2026-01-01T00:00:00.000Z',
},
};
const devices = DlnaDmrMapper.toDevices(snapshot);
const entities = DlnaDmrMapper.toEntities(snapshot);
expect(devices[0].id).toEqual('dlna_dmr.renderer.uuid_renderer_1');
expect(devices[0].state.find((stateArg) => stateArg.featureId === 'playback')?.value).toEqual('playing');
expect(entities[0].id).toEqual('media_player.living_room_renderer');
expect(entities[0].state).toEqual('playing');
expect(entities[0].attributes?.volumeLevel).toEqual(0.42);
expect(entities[0].attributes?.mediaContentType).toEqual('music');
});
export default tap.start();
@@ -0,0 +1,47 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { createJellyfinDiscoveryDescriptor } from '../../ts/integrations/jellyfin/index.js';
tap.test('matches manual Jellyfin server URLs', async () => {
const descriptor = createJellyfinDiscoveryDescriptor();
const matcher = descriptor.getMatchers().find((matcherArg) => matcherArg.id === 'jellyfin-manual-match');
const result = await matcher?.matches({
url: 'http://jellyfin.local:8096',
name: 'Home Jellyfin',
}, {});
expect(result?.matched).toBeTrue();
expect(result?.candidate?.host).toEqual('jellyfin.local');
expect(result?.candidate?.port).toEqual(8096);
});
tap.test('matches Jellyfin SSDP records', async () => {
const descriptor = createJellyfinDiscoveryDescriptor();
const matcher = descriptor.getMatchers().find((matcherArg) => matcherArg.id === 'jellyfin-ssdp-match');
const result = await matcher?.matches({
st: 'urn:schemas-upnp-org:device:MediaServer:1',
usn: 'uuid:jellyfin-server-1::urn:schemas-upnp-org:device:MediaServer:1',
location: 'http://192.168.1.20:8096/dlna/server/description.xml',
server: 'Jellyfin/10.10.7 UPnP/1.0',
headers: {
manufacturer: 'Jellyfin',
modelName: 'Jellyfin Server',
},
}, {});
expect(result?.matched).toBeTrue();
expect(result?.normalizedDeviceId).toEqual('jellyfin-server-1');
expect(result?.candidate?.host).toEqual('192.168.1.20');
});
tap.test('validates Jellyfin candidates', async () => {
const descriptor = createJellyfinDiscoveryDescriptor();
const validator = descriptor.getValidators()[0];
const result = await validator.validate({
source: 'manual',
integrationDomain: 'jellyfin',
host: '192.168.1.20',
port: 8096,
}, {});
expect(result.matched).toBeTrue();
expect(result.confidence).toEqual('high');
});
export default tap.start();
@@ -0,0 +1,64 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { JellyfinMapper, type IJellyfinSnapshot } from '../../ts/integrations/jellyfin/index.js';
const snapshot: IJellyfinSnapshot = {
server: {
Id: 'server-1',
Name: 'Home Jellyfin',
Version: '10.10.7',
},
online: true,
updatedAt: '2026-05-05T12:00:00.000Z',
sessions: [
{
Id: 'session-1',
UserName: 'phil',
Client: 'Jellyfin Web',
DeviceName: 'Living Room Browser',
DeviceId: 'device-1',
ApplicationVersion: '10.10.7',
IsActive: true,
SupportsRemoteControl: true,
Capabilities: {
SupportsMediaControl: true,
SupportsPersistentIdentifier: true,
SupportedCommands: ['Pause', 'Unpause', 'Stop', 'SetVolume'],
},
LastPlaybackCheckIn: '2026-05-05T12:01:00.000Z',
PlayState: {
IsPaused: false,
IsMuted: false,
PositionTicks: 1800000000,
VolumeLevel: 55,
},
NowPlayingItem: {
Id: 'movie-1',
Name: 'Example Movie',
Type: 'Movie',
RunTimeTicks: 72000000000,
},
},
],
};
tap.test('maps active Jellyfin sessions to media devices', async () => {
const devices = JellyfinMapper.toDevices(snapshot);
expect(devices.some((deviceArg) => deviceArg.id === 'jellyfin.server.server_1')).toBeTrue();
const sessionDevice = devices.find((deviceArg) => deviceArg.id === 'jellyfin.session.device_1');
expect(sessionDevice?.name).toEqual('Living Room Browser');
expect(sessionDevice?.features.some((featureArg) => featureArg.id === 'remote_command')).toBeTrue();
expect(sessionDevice?.state.some((stateArg) => stateArg.featureId === 'current_title' && stateArg.value === 'Example Movie')).toBeTrue();
});
tap.test('maps active Jellyfin sessions to media player entities', async () => {
const entities = JellyfinMapper.toEntities(snapshot);
const player = entities.find((entityArg) => entityArg.platform === 'media_player');
expect(player?.id).toEqual('media_player.living_room_browser');
expect(player?.state).toEqual('playing');
expect(player?.attributes?.volumeLevel).toEqual(0.55);
expect(player?.attributes?.mediaContentType).toEqual('movie');
expect(player?.attributes?.mediaDuration).toEqual(7200);
expect(player?.attributes?.mediaPosition).toEqual(180);
});
export default tap.start();
+32
View File
@@ -0,0 +1,32 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { createMpdDiscoveryDescriptor } from '../../ts/integrations/mpd/index.js';
tap.test('matches MPD mDNS records', async () => {
const descriptor = createMpdDiscoveryDescriptor();
const matcher = descriptor.getMatchers()[0];
const result = await matcher.matches({
name: 'Living Room MPD',
type: '_mpd._tcp.local.',
host: 'mpd.local',
port: 6600,
}, {});
expect(result.matched).toBeTrue();
expect(result.candidate?.integrationDomain).toEqual('mpd');
expect(result.candidate?.port).toEqual(6600);
expect(result.metadata?.mdnsType).toEqual('_mpd._tcp');
});
tap.test('matches and validates manual MPD entries', async () => {
const descriptor = createMpdDiscoveryDescriptor();
const matcher = descriptor.getMatchers()[1];
const result = await matcher.matches({ host: '192.168.1.50', name: 'Kitchen MPD' }, {});
expect(result.matched).toBeTrue();
expect(result.candidate?.port).toEqual(6600);
const validator = descriptor.getValidators()[0];
const validation = await validator.validate(result.candidate!, {});
expect(validation.matched).toBeTrue();
expect(validation.normalizedDeviceId).toEqual('192.168.1.50:6600');
});
export default tap.start();
+79
View File
@@ -0,0 +1,79 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { MpdMapper, type IMpdSnapshot } from '../../ts/integrations/mpd/index.js';
const snapshot: IMpdSnapshot = {
server: {
id: 'mpd-living-room',
host: '192.168.1.50',
port: 6600,
name: 'Living Room MPD',
protocolVersion: '0.24.0',
},
status: {
volume: '42',
repeat: '1',
single: '0',
random: '1',
playlist: '12',
playlistlength: '7',
state: 'play',
songid: '23',
elapsed: '31.2',
duration: '180.5',
audio: '44100:16:2',
bitrate: '320',
lastloadedplaylist: 'Favorites',
},
currentSong: {
file: 'artist/album/example.flac',
artist: ['Example Artist', 'Guest Artist'],
album: 'Example Album',
title: 'Example Track',
time: '180',
id: '23',
},
outputs: [{
outputid: 0,
outputname: 'Main ALSA',
plugin: 'alsa',
outputenabled: true,
attributes: { dop: '0' },
}, {
outputid: 1,
outputname: 'Headphones',
plugin: 'pulse',
outputenabled: false,
}],
commands: ['status', 'currentsong', 'play', 'pause', 'setvol', 'outputs'],
playlists: [{ playlist: 'Favorites' }, { playlist: 'Radio' }],
stats: { songs: '2000', uptime: '3600' },
online: true,
updatedAt: '2026-01-01T00:00:00.000Z',
source: 'manual',
};
tap.test('maps MPD server and outputs to canonical devices', async () => {
const devices = MpdMapper.toDevices(snapshot);
expect(devices.some((deviceArg) => deviceArg.id === 'mpd.server.mpd_living_room')).toBeTrue();
expect(devices.some((deviceArg) => deviceArg.id === 'mpd.output.mpd_living_room.0')).toBeTrue();
});
tap.test('maps MPD status current song and outputs to entities', async () => {
const entities = MpdMapper.toEntities(snapshot);
const player = entities.find((entityArg) => entityArg.id === 'media_player.living_room_mpd');
const status = entities.find((entityArg) => entityArg.id === 'sensor.living_room_mpd_mpd_status');
const output = entities.find((entityArg) => entityArg.id === 'switch.living_room_mpd_main_alsa_mpd_output');
expect(player?.platform).toEqual('media_player');
expect(player?.state).toEqual('playing');
expect(player?.attributes?.volumeLevel).toEqual(0.42);
expect(player?.attributes?.mediaTitle).toEqual('Example Track');
expect(player?.attributes?.mediaArtist).toEqual('Example Artist, Guest Artist');
expect(player?.attributes?.sourceList).toEqual(['Favorites', 'Radio']);
expect(status?.state).toEqual('play');
expect(output?.platform).toEqual('switch');
expect(output?.state).toEqual('on');
expect(output?.attributes?.mpdOutputId).toEqual(0);
});
export default tap.start();
+67
View File
@@ -0,0 +1,67 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { createOnvifDiscoveryDescriptor } from '../../ts/integrations/onvif/index.js';
tap.test('matches ONVIF WS-Discovery camera records', async () => {
const descriptor = createOnvifDiscoveryDescriptor();
const matcher = descriptor.getMatchers()[0];
const result = await matcher.matches({
epr: 'urn:uuid:camera-001',
xaddrs: ['http://192.168.1.50:8899/onvif/device_service'],
types: ['dn:NetworkVideoTransmitter'],
scopes: [
'onvif://www.onvif.org/Profile/Streaming',
'onvif://www.onvif.org/name/Driveway%20Camera',
'onvif://www.onvif.org/hardware/IPC-123',
'onvif://www.onvif.org/mac/AA-BB-CC-11-22-33',
],
}, {});
expect(result.matched).toBeTrue();
expect(result.candidate?.host).toEqual('192.168.1.50');
expect(result.candidate?.port).toEqual(8899);
expect(result.candidate?.name).toEqual('Driveway Camera');
expect(result.normalizedDeviceId).toEqual('aa:bb:cc:11:22:33');
});
tap.test('matches ONVIF mDNS camera records', async () => {
const descriptor = createOnvifDiscoveryDescriptor();
const matcher = descriptor.getMatchers()[1];
const result = await matcher.matches({
type: '_onvif._tcp.local.',
name: 'Porch Camera._onvif._tcp.local.',
host: 'porch-camera.local',
port: 80,
txt: {
model: 'IPC-321',
mac: '00:11:22:33:44:55',
},
}, {});
expect(result.matched).toBeTrue();
expect(result.candidate?.integrationDomain).toEqual('onvif');
expect(result.normalizedDeviceId).toEqual('00:11:22:33:44:55');
});
tap.test('validates manual ONVIF candidates', async () => {
const descriptor = createOnvifDiscoveryDescriptor();
const manualMatcher = descriptor.getMatchers()[2];
const validator = descriptor.getValidators()[0];
const manual = await manualMatcher.matches({
host: '192.168.1.51',
port: 80,
name: 'Garage Camera',
deviceInfo: {
manufacturer: 'ExampleCam',
model: 'Model S',
serialNumber: 'SN123',
},
profiles: [],
}, {});
const validated = await validator.validate(manual.candidate!, {});
expect(manual.matched).toBeTrue();
expect(validated.matched).toBeTrue();
expect(validated.metadata?.manualSupported).toEqual(true);
});
export default tap.start();
+106
View File
@@ -0,0 +1,106 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { OnvifMapper, type IOnvifSnapshot } from '../../ts/integrations/onvif/index.js';
const snapshot: IOnvifSnapshot = {
id: 'front-door',
name: 'Front Door',
host: '192.168.1.60',
port: 80,
transport: 'http',
connected: true,
configured: true,
cameras: [
{
id: 'front-door',
name: 'Front Door',
host: '192.168.1.60',
port: 80,
online: true,
deviceInfo: {
manufacturer: 'ExampleCam',
model: 'IPC-4K',
firmwareVersion: '1.2.3',
serialNumber: 'FD1234',
macAddress: 'AA:BB:CC:DD:EE:FF',
},
capabilities: {
snapshot: true,
stream: true,
ptz: true,
events: true,
},
profiles: [
{
index: 0,
token: 'profile_1',
name: 'Main',
video: {
encoding: 'H264',
resolution: { width: 1920, height: 1080 },
},
streamUri: 'rtsp://192.168.1.60/stream1',
snapshotUri: 'http://192.168.1.60/snapshot.jpg',
ptz: {
relative: true,
presets: ['1'],
},
},
],
streams: [
{
profileToken: 'profile_1',
uri: 'rtsp://192.168.1.60/stream1',
protocol: 'rtsp',
encoding: 'H264',
resolution: { width: 1920, height: 1080 },
},
],
events: [
{
uid: 'front_motion',
name: 'Motion',
platform: 'binary_sensor',
deviceClass: 'motion',
value: true,
},
],
},
],
};
tap.test('maps ONVIF cameras and profiles to canonical devices and constrained entities', async () => {
const devices = OnvifMapper.toDevices(snapshot);
const entities = OnvifMapper.toEntities(snapshot);
expect(devices[0].id).toEqual('onvif.camera.aa_bb_cc_dd_ee_ff');
expect(devices[0].features.some((featureArg) => featureArg.capability === 'camera')).toBeTrue();
expect(entities.find((entityArg) => entityArg.id === 'sensor.front_door_main_camera')?.platform).toEqual('sensor');
expect(entities.find((entityArg) => entityArg.id === 'sensor.front_door_main_camera')?.attributes?.capability).toEqual('camera');
expect(entities.find((entityArg) => entityArg.id === 'binary_sensor.front_door_motion')?.state).toEqual('on');
});
tap.test('maps camera stream, snapshot, and PTZ services to ONVIF commands', async () => {
const streamCommand = OnvifMapper.commandForService(snapshot, {
domain: 'camera',
service: 'stream_metadata',
target: { entityId: 'sensor.front_door_main_camera' },
});
const snapshotCommand = OnvifMapper.commandForService(snapshot, {
domain: 'camera',
service: 'snapshot_metadata',
target: { entityId: 'sensor.front_door_main_camera' },
});
const ptzCommand = OnvifMapper.commandForService(snapshot, {
domain: 'camera',
service: 'ptz',
target: { entityId: 'sensor.front_door_main_camera' },
data: { move_mode: 'RelativeMove', pan: 'LEFT', distance: 0.1 },
});
expect(streamCommand?.type).toEqual('stream_metadata');
expect(snapshotCommand?.type).toEqual('snapshot_metadata');
expect(ptzCommand?.ptz?.moveMode).toEqual('RelativeMove');
expect(ptzCommand?.ptz?.pan).toEqual('LEFT');
});
export default tap.start();
+70
View File
@@ -0,0 +1,70 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { createPlexDiscoveryDescriptor } from '../../ts/integrations/plex/index.js';
tap.test('matches Plex GDM server responses', async () => {
const descriptor = createPlexDiscoveryDescriptor();
const matcher = descriptor.getMatchers()[0];
const result = await matcher.matches({
data: {
'Content-Type': 'plex/media-server',
Name: 'Media Box',
Port: '32400',
'Resource-Identifier': 'server-abc',
Version: '1.41.0',
},
from: ['192.168.1.10', 32414],
}, {});
expect(result.matched).toBeTrue();
expect(result.candidate?.integrationDomain).toEqual('plex');
expect(result.candidate?.host).toEqual('192.168.1.10');
expect(result.candidate?.port).toEqual(32400);
expect(result.normalizedDeviceId).toEqual('server-abc');
});
tap.test('matches Plex zeroconf records', async () => {
const descriptor = createPlexDiscoveryDescriptor();
const matcher = descriptor.getMatchers()[1];
const result = await matcher.matches({
type: '_plexmediasvr._tcp.local.',
name: 'Media Box._plexmediasvr._tcp.local.',
host: 'media-box.local',
port: 32400,
txt: {
machineIdentifier: 'server-abc',
},
}, {});
expect(result.matched).toBeTrue();
expect(result.candidate?.host).toEqual('media-box.local');
expect(result.candidate?.metadata?.discoveryProtocol).toEqual('mdns');
});
tap.test('matches Plex SSDP records when advertised', async () => {
const descriptor = createPlexDiscoveryDescriptor();
const matcher = descriptor.getMatchers()[2];
const result = await matcher.matches({
headers: {
location: 'http://192.168.1.10:32400/description.xml',
server: 'Plex UPnP/1.0',
usn: 'uuid:server-abc::urn:schemas-upnp-org:device:MediaServer:1',
},
friendlyName: 'Media Box Plex',
}, {});
expect(result.matched).toBeTrue();
expect(result.candidate?.host).toEqual('192.168.1.10');
expect(result.candidate?.port).toEqual(32400);
});
tap.test('matches and validates manual Plex entries', async () => {
const descriptor = createPlexDiscoveryDescriptor();
const matcher = descriptor.getMatchers()[3];
const result = await matcher.matches({ host: '192.168.1.10', token: 'secret', name: 'Media Box' }, {});
expect(result.matched).toBeTrue();
expect(result.candidate?.port).toEqual(32400);
const validator = descriptor.getValidators()[0];
const validation = await validator.validate(result.candidate!, {});
expect(validation.matched).toBeTrue();
expect(validation.confidence).toEqual('certain');
});
export default tap.start();
+99
View File
@@ -0,0 +1,99 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { PlexMapper, type IPlexSnapshot } from '../../ts/integrations/plex/index.js';
const snapshot: IPlexSnapshot = {
capturedAt: '2026-01-01T00:00:00.000Z',
source: 'manual',
online: true,
server: {
machineIdentifier: 'server-abc',
friendlyName: 'Media Box',
version: '1.41.0',
platform: 'Linux',
url: 'http://192.168.1.10:32400',
host: '192.168.1.10',
port: 32400,
online: true,
},
clients: [{
machineIdentifier: 'client-abc',
title: 'Living Room TV',
product: 'Plex for Android TV',
platform: 'Android',
host: '192.168.1.55',
port: 32500,
protocolCapabilities: ['playback', 'timeline'],
source: 'GDM',
volumeLevel: 0.42,
muted: false,
}],
sessions: [{
sessionKey: '7',
ratingKey: '1001',
key: '/library/metadata/1001',
title: 'The Test Episode',
type: 'episode',
summary: 'A test episode.',
duration: 3600000,
viewOffset: 125000,
librarySectionTitle: 'TV Shows',
grandparentTitle: 'Example Show',
parentTitle: 'Season 1',
parentIndex: 1,
index: 2,
thumb: '/library/metadata/1001/thumb/1',
state: 'playing',
mediaPositionUpdatedAt: '2026-01-01T00:00:00.000Z',
User: { id: '1', title: 'Owner' },
Player: {
machineIdentifier: 'client-abc',
title: 'Living Room TV',
product: 'Plex for Android TV',
platform: 'Android',
state: 'playing',
protocolCapabilities: ['playback', 'timeline'],
},
Session: { id: 'session-1', bandwidth: 9000, location: 'lan' },
}],
libraries: [{
key: '1',
uuid: 'library-tv',
title: 'TV Shows',
type: 'show',
itemCount: 123,
counts: { show: 10, season: 20, episode: 123 },
refreshing: false,
lastAddedItem: 'Example Show - S01E02 - The Test Episode',
lastAddedTimestamp: '2026-01-01T00:00:00.000Z',
}],
};
tap.test('maps Plex servers and media clients to canonical devices', async () => {
const devices = PlexMapper.toDevices(snapshot);
const server = devices.find((deviceArg) => deviceArg.id === 'plex.server.server_abc');
const client = devices.find((deviceArg) => deviceArg.id === 'plex.client.server_abc.client_abc');
expect(server?.online).toBeTrue();
expect(server?.state.some((stateArg) => stateArg.featureId === 'active_sessions' && stateArg.value === 1)).toBeTrue();
expect(client?.manufacturer).toEqual('Android');
expect(client?.state.some((stateArg) => stateArg.featureId === 'current_title' && stateArg.value === 'The Test Episode')).toBeTrue();
});
tap.test('maps Plex activity, media players, and libraries to entities', async () => {
const entities = PlexMapper.toEntities(snapshot);
const activity = entities.find((entityArg) => entityArg.id === 'sensor.media_box_plex');
const player = entities.find((entityArg) => entityArg.id === 'media_player.living_room_tv_plex');
const library = entities.find((entityArg) => entityArg.id === 'sensor.media_box_tv_shows_plex_library');
expect(activity?.state).toEqual(1);
expect(activity?.attributes?.watching).toEqual({ 'Owner - Plex for Android TV': 'Example Show - S1:E2 - The Test Episode' });
expect(player?.state).toEqual('playing');
expect(player?.attributes?.mediaContentType).toEqual('tvshow');
expect(player?.attributes?.mediaDuration).toEqual(3600);
expect(player?.attributes?.mediaPosition).toEqual(125);
expect(player?.attributes?.volumeLevel).toEqual(0.42);
expect(library?.state).toEqual(123);
expect(library?.attributes?.primaryType).toEqual('episode');
});
export default tap.start();
@@ -0,0 +1,32 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { createRainbirdDiscoveryDescriptor } from '../../ts/integrations/rainbird/index.js';
tap.test('matches manual Rain Bird setup entries', async () => {
const descriptor = createRainbirdDiscoveryDescriptor();
const matcher = descriptor.getMatchers().find((matcherArg) => matcherArg.id === 'rainbird-manual-match');
const result = await matcher!.matches({
host: '192.168.1.40',
protocol: 'http',
macAddress: 'AA:BB:CC:12:34:56',
model: 'Rain Bird ESP-TM2',
}, {});
expect(result.matched).toBeTrue();
expect(result.normalizedDeviceId).toEqual('aabbcc123456');
expect(result.candidate?.integrationDomain).toEqual('rainbird');
expect(result.candidate?.port).toEqual(80);
});
tap.test('validates Rain Bird candidates', async () => {
const descriptor = createRainbirdDiscoveryDescriptor();
const validator = descriptor.getValidators()[0];
const result = await validator.validate({
source: 'manual',
integrationDomain: 'rainbird',
host: 'rainbird.local',
manufacturer: 'Rain Bird',
}, {});
expect(result.matched).toBeTrue();
expect(result.candidate?.manufacturer).toEqual('Rain Bird');
});
export default tap.start();
@@ -0,0 +1,68 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { RainbirdMapper, type IRainbirdSnapshot } from '../../ts/integrations/rainbird/index.js';
const snapshot: IRainbirdSnapshot = {
controller: {
id: 'aabbcc123456',
name: 'Backyard Controller',
manufacturer: 'Rain Bird',
modelName: 'ESP-TM2',
macAddress: 'aabbcc123456',
rainSensorActive: false,
rainDelayDays: 2,
host: '192.168.1.40',
},
zones: [
{ id: 1, name: 'Front Lawn', active: true, defaultDurationMinutes: 10 },
{ id: 2, name: 'Back Beds', active: false, defaultDurationMinutes: 8 },
],
programs: [{
id: 0,
name: 'PGM A',
enabled: true,
starts: ['06:00'],
frequency: 'custom',
zoneDurations: [{ zoneId: 1, durationMinutes: 10 }, { zoneId: 2, durationMinutes: 8 }],
}],
events: [],
connected: true,
updatedAt: '2026-01-01T00:00:00.000Z',
};
tap.test('maps Rain Bird zones and programs to canonical devices and entities', async () => {
const devices = RainbirdMapper.toDevices(snapshot);
const entities = RainbirdMapper.toEntities(snapshot);
expect(devices.some((deviceArg) => deviceArg.id === 'rainbird.controller.aabbcc123456')).toBeTrue();
expect(devices.some((deviceArg) => deviceArg.id === 'rainbird.zone.aabbcc123456.1')).toBeTrue();
expect(devices.some((deviceArg) => deviceArg.id === 'rainbird.program.aabbcc123456.0')).toBeTrue();
expect(entities.some((entityArg) => entityArg.id === 'switch.front_lawn' && entityArg.state === 'on')).toBeTrue();
expect(entities.some((entityArg) => entityArg.id === 'sensor.pgm_a')).toBeTrue();
expect(entities.some((entityArg) => entityArg.id === 'sensor.raindelay' && entityArg.state === 2)).toBeTrue();
});
tap.test('maps Rain Bird services to controller commands', async () => {
const startCommand = RainbirdMapper.commandForService(snapshot, {
domain: 'rainbird',
service: 'start_zone',
target: {},
data: { zoneId: 2, duration: 7 },
});
expect(startCommand).toEqual({ type: 'start_zone', zoneId: 2, durationMinutes: 7, entityId: undefined, deviceId: undefined });
const switchCommand = RainbirdMapper.commandForService(snapshot, {
domain: 'switch',
service: 'turn_off',
target: { entityId: 'switch.front_lawn' },
});
expect(switchCommand).toEqual({ type: 'stop_zone', zoneId: 1, entityId: 'switch.front_lawn', deviceId: undefined });
const rainDelayCommand = RainbirdMapper.commandForService(snapshot, {
domain: 'rainbird',
service: 'set_rain_delay',
target: {},
data: { duration: 4 },
});
expect(rainDelayCommand).toEqual({ type: 'set_rain_delay', days: 4, entityId: undefined, deviceId: undefined });
});
export default tap.start();
@@ -0,0 +1,32 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { createSnapcastDiscoveryDescriptor } from '../../ts/integrations/snapcast/index.js';
tap.test('matches Snapcast mDNS control records', async () => {
const descriptor = createSnapcastDiscoveryDescriptor();
const matcher = descriptor.getMatchers()[0];
const result = await matcher.matches({
name: 'Snapcast',
type: '_snapcast-ctrl._tcp.local.',
host: 'snapserver.local',
port: 1705,
}, {});
expect(result.matched).toBeTrue();
expect(result.candidate?.integrationDomain).toEqual('snapcast');
expect(result.candidate?.port).toEqual(1705);
expect(result.metadata?.transport).toEqual('tcp');
});
tap.test('matches and validates manual Snapcast entries', async () => {
const descriptor = createSnapcastDiscoveryDescriptor();
const matcher = descriptor.getMatchers()[1];
const result = await matcher.matches({ host: '192.168.1.20', transport: 'http' }, {});
expect(result.matched).toBeTrue();
expect(result.candidate?.port).toEqual(1780);
const validator = descriptor.getValidators()[0];
const validation = await validator.validate(result.candidate!, {});
expect(validation.matched).toBeTrue();
expect(validation.normalizedDeviceId).toEqual('192.168.1.20:1780');
});
export default tap.start();
@@ -0,0 +1,55 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { SnapcastMapper, type ISnapcastSnapshot } from '../../ts/integrations/snapcast/index.js';
const snapshot: ISnapcastSnapshot = {
capturedAt: '2026-01-01T00:00:00.000Z',
source: 'manual',
server: {
groups: [{
id: 'group-1',
muted: false,
name: 'Kitchen Group',
stream_id: 'music',
clients: [{
id: 'client-1',
connected: true,
host: { name: 'Kitchen', ip: '192.168.1.31', mac: '00:11:22:33:44:55' },
config: {
latency: 25,
name: 'Kitchen',
volume: { muted: false, percent: 42 },
},
snapclient: { name: 'Snapclient', version: '0.29.0', protocolVersion: 2 },
}],
}],
streams: [{
id: 'music',
status: 'playing',
uri: { raw: 'pipe:///tmp/snapfifo?name=music', scheme: 'pipe', query: { name: 'music' } },
metadata: { title: 'Example Track', artist: ['Example Artist'], album: 'Example Album' },
properties: { position: 12 },
}],
},
};
tap.test('maps Snapcast clients groups and streams to canonical devices', async () => {
const devices = SnapcastMapper.toDevices(snapshot);
expect(devices.some((deviceArg) => deviceArg.id === 'snapcast.client.client_1')).toBeTrue();
expect(devices.some((deviceArg) => deviceArg.id === 'snapcast.group.group_1')).toBeTrue();
expect(devices.some((deviceArg) => deviceArg.id === 'snapcast.stream.music')).toBeTrue();
});
tap.test('maps Snapcast clients groups and streams to entities', async () => {
const entities = SnapcastMapper.toEntities(snapshot);
const clientEntity = entities.find((entityArg) => entityArg.id === 'media_player.kitchen_snapcast_client');
const groupEntity = entities.find((entityArg) => entityArg.id === 'media_player.kitchen_group_snapcast_group');
const streamEntity = entities.find((entityArg) => entityArg.id === 'sensor.music_snapcast_stream');
expect(clientEntity?.platform).toEqual('media_player');
expect(clientEntity?.state).toEqual('playing');
expect(clientEntity?.attributes?.volumeLevel).toEqual(0.42);
expect(groupEntity?.attributes?.source).toEqual('music');
expect(streamEntity?.state).toEqual('playing');
});
export default tap.start();
@@ -0,0 +1,57 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { createVolumioDiscoveryDescriptor } from '../../ts/integrations/volumio/index.js';
tap.test('matches Volumio zeroconf records', async () => {
const descriptor = createVolumioDiscoveryDescriptor();
const matcher = descriptor.getMatchers()[0];
const result = await matcher.matches({
type: '_Volumio._tcp.local.',
name: 'Kitchen._Volumio._tcp.local.',
host: 'kitchen-volumio.local',
port: 3000,
txt: {
volumioName: 'Kitchen Volumio',
UUID: 'volumio-uuid-123',
},
}, {});
expect(result.matched).toBeTrue();
expect(result.confidence).toEqual('certain');
expect(result.normalizedDeviceId).toEqual('volumio-uuid-123');
expect(result.candidate?.integrationDomain).toEqual('volumio');
expect(result.candidate?.host).toEqual('kitchen-volumio.local');
expect(result.candidate?.port).toEqual(3000);
expect(result.candidate?.name).toEqual('Kitchen Volumio');
});
tap.test('matches manual Volumio host entries and validates candidates', async () => {
const descriptor = createVolumioDiscoveryDescriptor();
const matcher = descriptor.getMatchers()[1];
const matched = await matcher.matches({
host: '192.168.1.81',
name: 'Office Volumio',
uuid: 'manual-volumio-1',
}, {});
expect(matched.matched).toBeTrue();
expect(matched.candidate?.port).toEqual(3000);
const validator = descriptor.getValidators()[0];
const validated = await validator.validate(matched.candidate!, {});
expect(validated.matched).toBeTrue();
expect(validated.confidence).toEqual('high');
});
tap.test('rejects unrelated mDNS records', async () => {
const descriptor = createVolumioDiscoveryDescriptor();
const matcher = descriptor.getMatchers()[0];
const result = await matcher.matches({
type: '_http._tcp.local.',
name: 'Office Printer',
host: 'printer.local',
}, {});
expect(result.matched).toBeFalse();
});
export default tap.start();
+67
View File
@@ -0,0 +1,67 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { VolumioMapper, type IVolumioSnapshot } from '../../ts/integrations/volumio/index.js';
const snapshot: IVolumioSnapshot = {
deviceInfo: {
uuid: 'volumio-uuid-123',
name: 'Kitchen Volumio',
host: '192.168.1.81',
port: 3000,
manufacturer: 'Volumio',
hardware: 'Raspberry Pi',
systemVersion: '3.661',
},
systemInfo: {
id: 'volumio-uuid-123',
name: 'Kitchen Volumio',
},
systemVersion: {
hardware: 'Raspberry Pi',
systemversion: '3.661',
},
state: {
status: 'play',
title: 'Test Track',
artist: 'Test Artist',
album: 'Test Album',
albumart: 'http://192.168.1.81:3000/albumart?cacheid=1',
uri: 'music-library/NAS/test.flac',
trackType: 'flac',
seek: 123000,
duration: 245,
volume: 42,
mute: false,
random: true,
repeat: false,
service: 'mpd',
samplerate: '44100',
bitdepth: '16',
},
playlists: [{ name: 'Morning' }, { name: 'Evening' }],
online: true,
updatedAt: '2026-01-01T00:00:00.000Z',
};
tap.test('maps Volumio snapshots to media devices', async () => {
const devices = VolumioMapper.toDevices(snapshot);
expect(devices[0].id).toEqual('volumio.device.volumio_uuid_123');
expect(devices[0].protocol).toEqual('http');
expect(devices[0].state.some((stateArg) => stateArg.featureId === 'playback' && stateArg.value === 'playing')).toBeTrue();
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 === 'Test Track')).toBeTrue();
});
tap.test('maps Volumio snapshots to media player entities', async () => {
const entities = VolumioMapper.toEntities(snapshot);
expect(entities[0].id).toEqual('media_player.kitchen_volumio');
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?.mediaTitle).toEqual('Test Track');
expect(entities[0].attributes?.mediaArtist).toEqual('Test Artist');
expect(entities[0].attributes?.mediaPosition).toEqual(123);
expect(entities[0].attributes?.mediaDuration).toEqual(245);
expect(entities[0].attributes?.sourceList).toEqual(['Morning', 'Evening']);
});
export default tap.start();
@@ -0,0 +1,58 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { createYamahaMusiccastDiscoveryDescriptor } from '../../ts/integrations/yamaha_musiccast/index.js';
tap.test('matches Yamaha MusicCast mDNS records', async () => {
const descriptor = createYamahaMusiccastDiscoveryDescriptor();
const matcher = descriptor.getMatchers()[0];
const result = await matcher.matches({
name: 'Living Room MusicCast',
type: '_http._tcp.local.',
host: 'yamaha-rx.local',
port: 80,
txt: {
manufacturer: 'Yamaha Corporation',
model: 'RX-V685',
system_id: '03E88CF3',
device_id: '4C1B86A6CBF5',
},
}, {});
expect(result.matched).toBeTrue();
expect(result.normalizedDeviceId).toEqual('03E88CF3');
expect(result.candidate?.integrationDomain).toEqual('yamaha_musiccast');
expect(result.candidate?.host).toEqual('yamaha-rx.local');
});
tap.test('matches manual Yamaha MusicCast entries and validates candidates', async () => {
const descriptor = createYamahaMusiccastDiscoveryDescriptor();
const matcher = descriptor.getMatchers()[2];
const matched = await matcher.matches({
host: '192.168.1.70',
name: 'Kitchen MusicCast',
model: 'WX-021',
systemId: 'ABCD1234',
}, {});
expect(matched.matched).toBeTrue();
expect(matched.candidate?.port).toEqual(80);
const validator = descriptor.getValidators()[0];
const validated = await validator.validate(matched.candidate!, {});
expect(validated.matched).toBeTrue();
expect(validated.confidence).toEqual('high');
});
tap.test('rejects unrelated mDNS records', async () => {
const descriptor = createYamahaMusiccastDiscoveryDescriptor();
const matcher = descriptor.getMatchers()[0];
const result = await matcher.matches({
name: 'Office Printer',
type: '_ipp._tcp.local.',
host: 'printer.local',
txt: { manufacturer: 'Brother' },
}, {});
expect(result.matched).toBeFalse();
});
export default tap.start();
@@ -0,0 +1,106 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { YamahaMusiccastMapper, type IYamahaMusiccastSnapshot } from '../../ts/integrations/yamaha_musiccast/index.js';
const snapshot: IYamahaMusiccastSnapshot = {
deviceInfo: {
model_name: 'RX-V685',
device_id: '4C1B86A6CBF5',
system_id: '03E88CF3',
serial_number: 'Y459229YO',
system_version: 1.96,
api_version: 2.11,
},
networkStatus: {
network_name: 'Living Room RX',
ip_address: '192.168.1.70',
mac_address: {
wired_lan: '4C1B86A6CBF5',
},
},
inputNames: {
hdmi1: 'Apple TV',
server: 'Media Server',
tuner: 'Tuner',
audio1: 'AUDIO1',
},
netusb: {
input: 'server',
playback: 'pause',
repeat: 'all',
shuffle: 'on',
artist: 'Artist One',
album: 'Album One',
track: 'Track One',
total_time: 240,
play_time: 12,
},
distribution: {
group_id: '00000000000000000000000000000000',
role: 'none',
},
zones: [{
zone: 'main',
name: 'Main Zone',
power: 'on',
available: true,
volume: 80,
minVolume: 0,
maxVolume: 160,
muted: false,
input: 'server',
inputList: ['hdmi1', 'server', 'tuner'],
soundProgram: 'straight',
soundProgramList: ['straight', '7ch_stereo'],
toneControl: { mode: 'manual', bass: 1, treble: -1 },
toneControlModeList: ['manual', 'auto', 'bypass'],
linkControl: 'standard',
linkControlList: ['speed', 'standard', 'stability'],
extraBass: true,
enhancer: false,
rangeStep: [
{ id: 'volume', min: 0, max: 160, step: 1 },
{ id: 'tone_control', min: -12, max: 12, step: 1 },
],
}, {
zone: 'zone2',
name: 'Patio',
power: 'standby',
available: true,
volumeLevel: 0.25,
muted: true,
input: 'audio1',
inputList: ['server', 'tuner', 'audio1'],
}],
};
tap.test('maps Yamaha MusicCast zones to canonical devices', async () => {
const devices = YamahaMusiccastMapper.toDevices(snapshot);
expect(devices.length).toEqual(2);
expect(devices[0].id).toEqual('yamaha_musiccast.player.03e88cf3');
expect(devices[0].manufacturer).toEqual('Yamaha Corporation');
expect(devices[0].state.some((stateArg) => stateArg.featureId === 'source' && stateArg.value === 'server')).toBeTrue();
expect(devices[0].state.some((stateArg) => stateArg.featureId === 'capability_extra_bass' && stateArg.value === true)).toBeTrue();
expect(devices[1].metadata?.viaDeviceId).toEqual('yamaha_musiccast.player.03e88cf3');
});
tap.test('maps Yamaha MusicCast zones to media, switch, select, and number entities', async () => {
const entities = YamahaMusiccastMapper.toEntities(snapshot);
const media = entities.find((entityArg) => entityArg.id === 'media_player.living_room_rx');
const zone2 = entities.find((entityArg) => entityArg.id === 'media_player.living_room_rx_zone2');
const extraBass = entities.find((entityArg) => entityArg.id === 'switch.living_room_rx_extra_bass');
const linkControl = entities.find((entityArg) => entityArg.id === 'select.living_room_rx_link_control');
const toneBass = entities.find((entityArg) => entityArg.id === 'number.living_room_rx_tone_control_bass');
expect(media?.state).toEqual('paused');
expect(media?.attributes?.volumeLevel).toEqual(0.5);
expect(media?.attributes?.source).toEqual('Media Server');
expect(media?.attributes?.mediaTitle).toEqual('Track One');
expect(zone2?.state).toEqual('off');
expect(extraBass?.state).toEqual(true);
expect(extraBass?.attributes?.capabilityId).toEqual('extra_bass');
expect(linkControl?.state).toEqual('standard');
expect(toneBass?.state).toEqual(1);
expect(toneBass?.attributes?.nativeMinValue).toEqual(-12);
});
export default tap.start();