262 lines
11 KiB
TypeScript
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;
|
|
}
|
|
}
|