Files
integrations/ts/integrations/cast/cast.discovery.ts
T
2026-05-05 12:41:31 +00:00

115 lines
4.6 KiB
TypeScript

import { DiscoveryDescriptor } from '../../core/classes.discoverydescriptor.js';
import type { IDiscoveryCandidate, IDiscoveryMatch, IDiscoveryMatcher, IDiscoveryValidator } from '../../core/types.js';
import type { ICastManualEntry, ICastMdnsRecord } from './cast.types.js';
export class CastMdnsMatcher implements IDiscoveryMatcher<ICastMdnsRecord> {
public id = 'cast-mdns-match';
public source = 'mdns' as const;
public description = 'Recognize Google Cast mDNS advertisements.';
public async matches(recordArg: ICastMdnsRecord): Promise<IDiscoveryMatch> {
const type = this.normalizeType(recordArg.type);
const txtId = this.stripUuid(this.txt(recordArg, 'id'));
const friendlyName = this.txt(recordArg, 'fn') || recordArg.name;
const model = this.txt(recordArg, 'md');
const matched = type === '_googlecast._tcp.local' || Boolean(txtId || model || friendlyName?.toLowerCase().includes('chromecast'));
if (!matched) {
return { matched: false, confidence: 'low', reason: 'mDNS record is not a Google Cast advertisement.' };
}
return {
matched: true,
confidence: txtId ? 'certain' : 'high',
reason: 'mDNS record matches Google Cast metadata.',
normalizedDeviceId: txtId,
candidate: {
source: 'mdns',
integrationDomain: 'cast',
id: txtId,
host: recordArg.host || recordArg.addresses?.[0],
port: recordArg.port || 8009,
name: friendlyName,
manufacturer: 'Google',
model,
metadata: {
mdnsName: recordArg.name,
mdnsType: recordArg.type,
txt: recordArg.txt,
},
},
};
}
private txt(recordArg: ICastMdnsRecord, keyArg: string): string | undefined {
return recordArg.txt?.[keyArg] || recordArg.txt?.[keyArg.toUpperCase()];
}
private normalizeType(valueArg?: string): string {
return (valueArg || '').toLowerCase().replace(/\.$/, '');
}
private stripUuid(valueArg?: string): string | undefined {
return valueArg?.replace(/^uuid:/i, '').replace(/-/g, '').toLowerCase();
}
}
export class CastManualMatcher implements IDiscoveryMatcher<ICastManualEntry> {
public id = 'cast-manual-match';
public source = 'manual' as const;
public description = 'Recognize manual Google Cast setup entries.';
public async matches(inputArg: ICastManualEntry): Promise<IDiscoveryMatch> {
const model = inputArg.model?.toLowerCase() || '';
const manufacturer = inputArg.manufacturer?.toLowerCase() || '';
const host = inputArg.host || inputArg.knownHosts?.[0];
const matched = Boolean(host || inputArg.metadata?.cast || manufacturer.includes('google') || model.includes('chromecast') || model.includes('google home') || model.includes('nest'));
if (!matched) {
return { matched: false, confidence: 'low', reason: 'Manual entry does not contain Google Cast setup hints.' };
}
return {
matched: true,
confidence: host ? 'high' : 'medium',
reason: 'Manual entry can start Google Cast setup.',
normalizedDeviceId: inputArg.id,
candidate: {
source: 'manual',
integrationDomain: 'cast',
id: inputArg.id,
host,
port: inputArg.port || 8009,
name: inputArg.name,
manufacturer: inputArg.manufacturer || 'Google',
model: inputArg.model,
metadata: {
...inputArg.metadata,
knownHosts: inputArg.knownHosts,
},
},
};
}
}
export class CastCandidateValidator implements IDiscoveryValidator {
public id = 'cast-candidate-validator';
public description = 'Validate Google Cast candidate metadata.';
public async validate(candidateArg: IDiscoveryCandidate): Promise<IDiscoveryMatch> {
const manufacturer = candidateArg.manufacturer?.toLowerCase() || '';
const model = candidateArg.model?.toLowerCase() || '';
const matched = candidateArg.integrationDomain === 'cast' || manufacturer.includes('google') || model.includes('chromecast') || model.includes('google home') || model.includes('nest') || Boolean(candidateArg.metadata?.cast);
return {
matched,
confidence: matched && candidateArg.host ? 'high' : matched ? 'medium' : 'low',
reason: matched ? 'Candidate has Google Cast metadata.' : 'Candidate is not Google Cast.',
candidate: matched ? candidateArg : undefined,
normalizedDeviceId: candidateArg.id,
};
}
}
export const createCastDiscoveryDescriptor = (): DiscoveryDescriptor => {
return new DiscoveryDescriptor({ integrationDomain: 'cast', displayName: 'Google Cast' })
.addMatcher(new CastMdnsMatcher())
.addMatcher(new CastManualMatcher())
.addValidator(new CastCandidateValidator());
};