94 lines
3.8 KiB
TypeScript
94 lines
3.8 KiB
TypeScript
import { DiscoveryDescriptor } from '../../core/classes.discoverydescriptor.js';
|
|
import type { IDiscoveryCandidate, IDiscoveryMatch, IDiscoveryMatcher, IDiscoveryValidator } from '../../core/types.js';
|
|
import type { IRokuManualEntry, IRokuSsdpRecord } from './roku.types.js';
|
|
|
|
export class RokuSsdpMatcher implements IDiscoveryMatcher<IRokuSsdpRecord> {
|
|
public id = 'roku-ssdp-match';
|
|
public source = 'ssdp' as const;
|
|
public description = 'Recognize Roku ECP SSDP advertisements.';
|
|
|
|
public async matches(recordArg: IRokuSsdpRecord): Promise<IDiscoveryMatch> {
|
|
const st = recordArg.st || recordArg.headers?.st || recordArg.headers?.ST;
|
|
const usn = recordArg.usn || recordArg.headers?.usn || recordArg.headers?.USN;
|
|
const location = recordArg.location || recordArg.headers?.location || recordArg.headers?.LOCATION;
|
|
const manufacturer = recordArg.headers?.manufacturer || recordArg.headers?.MANUFACTURER;
|
|
const matched = st === 'roku:ecp' || manufacturer === 'Roku' || Boolean(usn?.toLowerCase().includes('roku'));
|
|
if (!matched) {
|
|
return { matched: false, confidence: 'low', reason: 'SSDP record is not Roku ECP.' };
|
|
}
|
|
const url = location ? new URL(location) : undefined;
|
|
const id = usn?.replace(/^uuid:/i, '').split('::')[0];
|
|
return {
|
|
matched: true,
|
|
confidence: id ? 'certain' : 'high',
|
|
reason: 'SSDP record matches Roku ECP metadata.',
|
|
normalizedDeviceId: id,
|
|
candidate: {
|
|
source: 'ssdp',
|
|
integrationDomain: 'roku',
|
|
id,
|
|
host: url?.hostname,
|
|
port: url?.port ? Number(url.port) : 8060,
|
|
manufacturer: 'Roku',
|
|
model: recordArg.headers?.modelName || recordArg.headers?.MODELNAME,
|
|
metadata: { st, usn, location },
|
|
},
|
|
};
|
|
}
|
|
}
|
|
|
|
export class RokuManualMatcher implements IDiscoveryMatcher<IRokuManualEntry> {
|
|
public id = 'roku-manual-match';
|
|
public source = 'manual' as const;
|
|
public description = 'Recognize manual Roku setup entries.';
|
|
|
|
public async matches(inputArg: IRokuManualEntry): Promise<IDiscoveryMatch> {
|
|
const matched = Boolean(inputArg.host || inputArg.model?.toLowerCase().includes('roku') || inputArg.metadata?.roku);
|
|
if (!matched) {
|
|
return { matched: false, confidence: 'low', reason: 'Manual entry does not contain Roku setup hints.' };
|
|
}
|
|
return {
|
|
matched: true,
|
|
confidence: inputArg.host ? 'high' : 'medium',
|
|
reason: 'Manual entry can start Roku setup.',
|
|
normalizedDeviceId: inputArg.id,
|
|
candidate: {
|
|
source: 'manual',
|
|
integrationDomain: 'roku',
|
|
id: inputArg.id,
|
|
host: inputArg.host,
|
|
port: inputArg.port || 8060,
|
|
name: inputArg.name,
|
|
manufacturer: 'Roku',
|
|
model: inputArg.model,
|
|
metadata: inputArg.metadata,
|
|
},
|
|
};
|
|
}
|
|
}
|
|
|
|
export class RokuCandidateValidator implements IDiscoveryValidator {
|
|
public id = 'roku-candidate-validator';
|
|
public description = 'Validate Roku ECP candidate metadata.';
|
|
|
|
public async validate(candidateArg: IDiscoveryCandidate): Promise<IDiscoveryMatch> {
|
|
const manufacturer = candidateArg.manufacturer?.toLowerCase() || '';
|
|
const model = candidateArg.model?.toLowerCase() || '';
|
|
const matched = candidateArg.integrationDomain === 'roku' || manufacturer === 'roku' || model.includes('roku');
|
|
return {
|
|
matched,
|
|
confidence: matched && candidateArg.host ? 'high' : matched ? 'medium' : 'low',
|
|
reason: matched ? 'Candidate has Roku metadata.' : 'Candidate is not Roku.',
|
|
candidate: matched ? candidateArg : undefined,
|
|
normalizedDeviceId: candidateArg.id,
|
|
};
|
|
}
|
|
}
|
|
|
|
export const createRokuDiscoveryDescriptor = (): DiscoveryDescriptor => {
|
|
return new DiscoveryDescriptor({ integrationDomain: 'roku', displayName: 'Roku' })
|
|
.addMatcher(new RokuSsdpMatcher())
|
|
.addMatcher(new RokuManualMatcher())
|
|
.addValidator(new RokuCandidateValidator());
|
|
};
|