Files
integrations/ts/integrations/snapcast/snapcast.mapper.ts
T

262 lines
11 KiB
TypeScript

import * as plugins from '../../plugins.js';
import type { IIntegrationEntity } from '../../core/types.js';
import type { ISnapcastClient, ISnapcastGroup, ISnapcastServerStatus, ISnapcastSnapshot, ISnapcastStream } from './snapcast.types.js';
export class SnapcastMapper {
public static toDevices(snapshotArg: ISnapcastSnapshot | ISnapcastServerStatus): plugins.shxInterfaces.data.IDeviceDefinition[] {
const server = this.serverStatus(snapshotArg);
const updatedAt = new Date().toISOString();
const devices: plugins.shxInterfaces.data.IDeviceDefinition[] = [];
for (const group of server.groups) {
const stream = this.streamForGroup(server, group);
devices.push(this.groupDevice(group, stream, updatedAt));
for (const client of group.clients) {
devices.push(this.clientDevice(client, group, stream, updatedAt));
}
}
for (const stream of server.streams) {
devices.push(this.streamDevice(stream, updatedAt));
}
return devices;
}
public static toEntities(snapshotArg: ISnapcastSnapshot | ISnapcastServerStatus): IIntegrationEntity[] {
const server = this.serverStatus(snapshotArg);
const entities: IIntegrationEntity[] = [];
for (const group of server.groups) {
const stream = this.streamForGroup(server, group);
entities.push({
id: `media_player.${this.slug(this.groupName(group))}_snapcast_group`,
uniqueId: `snapcast_group_${this.slug(group.id)}`,
integrationDomain: 'snapcast',
deviceId: this.groupDeviceId(group),
platform: 'media_player',
name: `${this.groupName(group)} Snapcast Group`,
state: this.playbackState(group, stream),
attributes: {
snapcastGroupId: group.id,
muted: Boolean(group.muted),
source: this.groupStreamId(group),
sourceList: server.streams.map((streamArg) => streamArg.id),
groupMembers: group.clients.map((clientArg) => `media_player.${this.slug(this.clientName(clientArg))}_snapcast_client`),
clientIds: group.clients.map((clientArg) => clientArg.id),
mediaTitle: stream?.metadata?.title,
mediaArtist: this.joinArtist(stream?.metadata?.artist),
mediaAlbum: stream?.metadata?.album,
mediaImageUrl: stream?.metadata?.artUrl,
},
available: group.clients.some((clientArg) => clientArg.connected),
});
for (const client of group.clients) {
entities.push({
id: `media_player.${this.slug(this.clientName(client))}_snapcast_client`,
uniqueId: `snapcast_client_${this.slug(client.id)}`,
integrationDomain: 'snapcast',
deviceId: this.clientDeviceId(client),
platform: 'media_player',
name: `${this.clientName(client)} Snapcast Client`,
state: this.playbackState(group, stream, client),
attributes: {
snapcastClientId: client.id,
snapcastGroupId: group.id,
latency: client.config.latency,
volumeLevel: typeof client.config.volume?.percent === 'number' ? client.config.volume.percent / 100 : undefined,
volumePercent: client.config.volume?.percent,
muted: client.config.volume?.muted,
source: this.groupStreamId(group),
sourceList: server.streams.map((streamArg) => streamArg.id),
groupMembers: group.clients.map((clientArg) => `media_player.${this.slug(this.clientName(clientArg))}_snapcast_client`),
hostName: client.host?.name,
hostIp: client.host?.ip,
hostMac: client.host?.mac,
mediaTitle: stream?.metadata?.title,
mediaArtist: this.joinArtist(stream?.metadata?.artist),
mediaAlbum: stream?.metadata?.album,
mediaImageUrl: stream?.metadata?.artUrl,
mediaDuration: stream?.metadata?.duration,
mediaPosition: typeof stream?.properties?.position === 'number' ? stream.properties.position : undefined,
},
available: client.connected,
});
}
}
for (const stream of server.streams) {
entities.push({
id: `sensor.${this.slug(this.streamName(stream))}_snapcast_stream`,
uniqueId: `snapcast_stream_${this.slug(stream.id)}`,
integrationDomain: 'snapcast',
deviceId: this.streamDeviceId(stream),
platform: 'sensor',
name: `${this.streamName(stream)} Snapcast Stream`,
state: stream.status || 'unknown',
attributes: {
snapcastStreamId: stream.id,
uri: stream.uri?.raw,
scheme: stream.uri?.scheme,
metadata: stream.metadata,
properties: stream.properties,
},
available: true,
});
}
return entities;
}
public static clientDeviceId(clientArg: ISnapcastClient): string {
return `snapcast.client.${this.slug(clientArg.id)}`;
}
public static groupDeviceId(groupArg: ISnapcastGroup): string {
return `snapcast.group.${this.slug(groupArg.id)}`;
}
public static streamDeviceId(streamArg: ISnapcastStream): string {
return `snapcast.stream.${this.slug(streamArg.id)}`;
}
public static slug(valueArg: string): string {
return valueArg.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '') || 'snapcast';
}
private static clientDevice(clientArg: ISnapcastClient, groupArg: ISnapcastGroup, streamArg: ISnapcastStream | undefined, updatedAtArg: string): plugins.shxInterfaces.data.IDeviceDefinition {
return {
id: this.clientDeviceId(clientArg),
integrationDomain: 'snapcast',
name: this.clientName(clientArg),
protocol: 'http',
manufacturer: 'Snapcast',
model: clientArg.snapclient?.name || 'Snapclient',
online: clientArg.connected,
features: [
{ id: 'playback', capability: 'media', name: 'Playback', readable: true, writable: false },
{ id: 'volume', capability: 'media', name: 'Volume', readable: true, writable: true, unit: '%' },
{ id: 'muted', capability: 'media', name: 'Muted', readable: true, writable: true },
{ id: 'latency', capability: 'media', name: 'Latency', readable: true, writable: true, unit: 'ms' },
{ id: 'stream', capability: 'media', name: 'Stream', readable: true, writable: true },
{ id: 'group', capability: 'media', name: 'Group', readable: true, writable: true },
],
state: [
{ featureId: 'playback', value: this.playbackState(groupArg, streamArg, clientArg), updatedAt: updatedAtArg },
{ featureId: 'volume', value: clientArg.config.volume?.percent ?? null, updatedAt: updatedAtArg },
{ featureId: 'muted', value: clientArg.config.volume?.muted ?? null, updatedAt: updatedAtArg },
{ featureId: 'latency', value: clientArg.config.latency ?? null, updatedAt: updatedAtArg },
{ featureId: 'stream', value: this.groupStreamId(groupArg) || null, updatedAt: updatedAtArg },
{ featureId: 'group', value: groupArg.id, updatedAt: updatedAtArg },
],
metadata: {
snapcastClientId: clientArg.id,
snapcastGroupId: groupArg.id,
host: clientArg.host,
snapclient: clientArg.snapclient,
lastSeen: clientArg.lastSeen,
},
};
}
private static groupDevice(groupArg: ISnapcastGroup, streamArg: ISnapcastStream | undefined, updatedAtArg: string): plugins.shxInterfaces.data.IDeviceDefinition {
return {
id: this.groupDeviceId(groupArg),
integrationDomain: 'snapcast',
name: this.groupName(groupArg),
protocol: 'http',
manufacturer: 'Snapcast',
model: 'Snapcast Group',
online: groupArg.clients.some((clientArg) => clientArg.connected),
features: [
{ id: 'playback', capability: 'media', name: 'Playback', readable: true, writable: false },
{ id: 'muted', capability: 'media', name: 'Muted', readable: true, writable: true },
{ id: 'stream', capability: 'media', name: 'Stream', readable: true, writable: true },
{ id: 'client_count', capability: 'sensor', name: 'Client count', readable: true, writable: false },
],
state: [
{ featureId: 'playback', value: this.playbackState(groupArg, streamArg), updatedAt: updatedAtArg },
{ featureId: 'muted', value: Boolean(groupArg.muted), updatedAt: updatedAtArg },
{ featureId: 'stream', value: this.groupStreamId(groupArg) || null, updatedAt: updatedAtArg },
{ featureId: 'client_count', value: groupArg.clients.length, updatedAt: updatedAtArg },
],
metadata: {
snapcastGroupId: groupArg.id,
clientIds: groupArg.clients.map((clientArg) => clientArg.id),
},
};
}
private static streamDevice(streamArg: ISnapcastStream, updatedAtArg: string): plugins.shxInterfaces.data.IDeviceDefinition {
return {
id: this.streamDeviceId(streamArg),
integrationDomain: 'snapcast',
name: this.streamName(streamArg),
protocol: 'http',
manufacturer: 'Snapcast',
model: `${streamArg.uri?.scheme || 'audio'} stream`,
online: true,
features: [
{ id: 'status', capability: 'media', name: 'Status', readable: true, writable: false },
{ id: 'current_title', capability: 'media', name: 'Current title', readable: true, writable: false },
],
state: [
{ featureId: 'status', value: streamArg.status || 'unknown', updatedAt: updatedAtArg },
{ featureId: 'current_title', value: streamArg.metadata?.title || null, updatedAt: updatedAtArg },
],
metadata: {
snapcastStreamId: streamArg.id,
uri: streamArg.uri,
metadata: streamArg.metadata,
properties: streamArg.properties,
},
};
}
private static serverStatus(snapshotArg: ISnapcastSnapshot | ISnapcastServerStatus): ISnapcastServerStatus {
return Array.isArray((snapshotArg as ISnapcastServerStatus).groups) ? snapshotArg as ISnapcastServerStatus : (snapshotArg as ISnapcastSnapshot).server;
}
private static streamForGroup(serverArg: ISnapcastServerStatus, groupArg: ISnapcastGroup): ISnapcastStream | undefined {
const streamId = this.groupStreamId(groupArg);
return serverArg.streams.find((streamArg) => streamArg.id === streamId);
}
private static playbackState(groupArg: ISnapcastGroup, streamArg?: ISnapcastStream, clientArg?: ISnapcastClient): string {
if (clientArg && !clientArg.connected) {
return 'off';
}
if (clientArg?.config.volume?.muted || groupArg.muted) {
return 'idle';
}
if (streamArg?.status === 'playing') {
return 'playing';
}
if (streamArg?.status === 'idle') {
return 'idle';
}
return streamArg?.status || 'unknown';
}
private static clientName(clientArg: ISnapcastClient): string {
return clientArg.config.name || clientArg.host?.name || clientArg.id;
}
private static groupName(groupArg: ISnapcastGroup): string {
return groupArg.name || groupArg.clients.map((clientArg) => this.clientName(clientArg)).join(', ') || groupArg.id;
}
private static streamName(streamArg: ISnapcastStream): string {
return streamArg.uri?.query?.name || streamArg.id;
}
private static groupStreamId(groupArg: ISnapcastGroup): string {
return groupArg.stream_id || groupArg.streamId || '';
}
private static joinArtist(valueArg: string[] | string | undefined): string | undefined {
return Array.isArray(valueArg) ? valueArg.join(', ') : valueArg;
}
}