115 lines
4.6 KiB
TypeScript
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());
|
|
};
|