homebridge-appletv-enhanced
Version:
Plugin that exposes the Apple TV to HomeKit with much richer features than the vanilla Apple TV implementation of HomeKit.
211 lines • 10.7 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.AppleTVEnhancedPlatform = void 0;
const settings_1 = require("./settings");
const appleTVEnhancedAccessory_1 = require("./appleTVEnhancedAccessory");
const CustomPyAtvInstance_1 = __importDefault(require("./CustomPyAtvInstance"));
const PythonChecker_1 = __importDefault(require("./PythonChecker"));
const PrefixLogger_1 = __importDefault(require("./PrefixLogger"));
const LogLevelLogger_1 = __importDefault(require("./LogLevelLogger"));
const os_1 = require("os");
// compatible model identifiers according to https://pyatv.dev/api/const/#pyatv.const.DeviceModel
const ALLOWED_MODELS = [
'Gen4',
'Gen4K',
'AppleTV4KGen2',
'AppleTV4KGen3',
];
const DEV_MODE = process.env.APPLETV_ENHANCED_DEV?.toLowerCase() === 'true';
class AppleTVEnhancedPlatform {
config;
api;
characteristic;
logLevelLogger;
service;
atvAccessories = [];
log;
publishedUUIDs = [];
constructor(ogLog, config, api) {
this.config = config;
this.api = api;
this.logLevelLogger = new LogLevelLogger_1.default(ogLog, this.config.logLevel);
this.log = new PrefixLogger_1.default(this.logLevelLogger, 'Platform');
this.service = this.api.hap.Service;
this.characteristic = this.api.hap.Characteristic;
if (DEV_MODE === true) {
this.log.warn('Development mode activated');
}
this.log.info('Finished initializing platform:', this.config.name);
// When this event is fired it means Homebridge has restored all cached accessories from disk.
// Dynamic Platform plugins should only register new accessories after this event was fired,
// in order to ensure they weren't added to homebridge already. This event can also be used
// to start discovery of new accessories.
// eslint-disable-next-line @typescript-eslint/no-misused-promises
this.api.on('didFinishLaunching', async () => {
this.log.debug('Executed didFinishLaunching callback');
// make sure the Python environment is ready
await new PythonChecker_1.default(this.logLevelLogger, this.api.user.storagePath(), this.config.pythonExecutable)
.allInOne(this.config.forceVenvRecreate);
// run the method to discover / register your devices as accessories
this.log.debug(`Setting the storage path of the PyATV instance to ${this.api.user.storagePath()}`);
CustomPyAtvInstance_1.default.setLogger(this.logLevelLogger);
CustomPyAtvInstance_1.default.setStoragePath(this.api.user.storagePath());
if (this.config.discover?.multicast === false &&
(this.config.discover.unicast === undefined || this.config.discover.unicast.length === 0)) {
this.log.error('Neither multicast nor unicast discovery is enabled.');
return;
}
this.log.info('Starting device discovery ...');
void this.discoverDevices();
setInterval(() => {
void this.discoverDevices();
}, 60000);
setTimeout(() => {
this.warnNoDevices();
}, 150000);
});
// Shutdown event will stop all ATV accessories
this.api.on('shutdown', () => {
for (const atvAccessory of this.atvAccessories) {
atvAccessory.stop().catch(() => {
this.log.error(`Failed to stop ${atvAccessory.name} (${atvAccessory.mac})`);
});
}
});
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function
configureAccessory(_accessory) { }
/**
* This is an example method showing how to register discovered accessories.
* Accessories must only be registered once, previously created accessories
* must not be registered again to prevent "duplicate UUID" errors.
*/
async discoverDevices() {
this.log.debug('Starting device discovery ...');
let scanResults = [];
// multicast discovery
if (this.config.discover?.multicast === undefined ||
this.config.discover.multicast === true) {
try {
const multicastResults = await CustomPyAtvInstance_1.default.customFind();
multicastResults.errors.forEach((error) => {
if (error.exception !== undefined && typeof error.exception === 'string') {
this.log.error(`multicast discovery - ${error.exception}`);
this.log.debug(JSON.stringify(error, undefined, 2));
}
else {
this.log.error(JSON.stringify(error, undefined, 2));
}
});
scanResults = multicastResults.devices;
this.log.debug('finished multicast device discovery');
}
catch (e) {
if (typeof e === 'object' && e instanceof Error) {
this.log.error(`${e.name}: ${e.message}`);
if (e.stack !== undefined && e.stack !== null) {
this.log.debug(e.stack);
}
}
else {
throw e;
}
}
}
// unicast discovery
if (this.config.discover?.unicast &&
this.config.discover.unicast.length !== 0) {
try {
const unicastResults = await CustomPyAtvInstance_1.default.customFind({ hosts: this.config.discover?.unicast });
unicastResults.errors.forEach((error) => {
if (error.exception !== undefined && typeof error.exception === 'string') {
this.log.error(`unicast discovery - ${error.exception}`);
this.log.debug(JSON.stringify(error, undefined, 2));
}
else {
this.log.error(JSON.stringify(error, undefined, 2));
}
});
scanResults = [...scanResults, ...unicastResults.devices];
this.log.debug('finished unicast device discovery');
}
catch (e) {
if (typeof e === 'object' && e instanceof Error) {
this.log.error(`${e.name}: ${e.message}`);
if (e.stack !== undefined && e.stack !== null) {
this.log.debug(e.stack);
}
}
else {
throw e;
}
}
}
const appleTVs = scanResults.filter((d) => ALLOWED_MODELS.includes(d.model ?? '') && d.os === 'TvOS');
// loop over the discovered devices and register each one if it has not already been registered
for (const appleTV of appleTVs) {
this.log.debug(`Found ${appleTV.name} (${appleTV.mac}).`);
if (appleTV.mac === undefined || appleTV.mac === null) {
this.log.debug(`${appleTV.name} is skipped since the MAC address could not be determined.`);
continue;
}
const mac = appleTV.mac.toUpperCase();
if (this.config.discover?.blacklist) {
if (this.config.discover.blacklist.map((e) => e.toUpperCase()).includes(mac)) {
this.log.debug(`${appleTV.name} (${appleTV.mac}) is on the blacklist. Skipping.`);
continue;
}
if (this.config.discover.blacklist.includes(appleTV.host ?? '')) {
this.log.debug(`${appleTV.name} (${appleTV.host}) is on the blacklist. Skipping.`);
continue;
}
}
// generate a unique id for the accessory this should be generated from
// something globally unique, but constant, for example, the device serial
// number or MAC address
let uuid = this.api.hap.uuid.generate(mac);
if (DEV_MODE === true) {
const localHostname = (0, os_1.hostname)();
uuid = this.api.hap.uuid.generate(`${localHostname}${mac}`);
this.log.debug(`Generated UUID ${uuid} for ${appleTV.name} from local hostname ${localHostname} and MAC address ${mac} \
since development mode is enabled.`);
}
if (this.publishedUUIDs.includes(uuid)) {
this.log.debug(`${appleTV.name} (${appleTV.mac}) with UUID ${uuid} already exists. Skipping.`);
continue;
}
this.publishedUUIDs.push(uuid);
// the accessory does not yet exist, so we need to create it
this.log.info(`Adding ${appleTV.name} (${appleTV.mac})`);
// create a new accessory
const newAccessory = new this.api.platformAccessory(appleTV.name, uuid);
// store a copy of the device object in the `accessory.context`
// the `context` property can be used to store any data about the accessory you may need
newAccessory.context.mac = mac;
// create the accessory handler for the newly create accessory
// this is imported from `platformAccessory.ts`
void (async () => {
this.log.debug(`Waiting for ${appleTV.name} (${appleTV.mac}) to boot ...`);
const newAtvAccessory = new appleTVEnhancedAccessory_1.AppleTVEnhancedAccessory(this, newAccessory);
await newAtvAccessory.untilBooted();
this.atvAccessories.push(newAtvAccessory);
// link the accessory to your platform
this.log.debug(`${appleTV.name} (${appleTV.mac}) finished booting. Publishing the accessory now.`);
this.api.publishExternalAccessories(settings_1.PLUGIN_NAME, [newAccessory]);
})();
}
this.log.debug('Finished device discovery.');
}
warnNoDevices() {
if (this.publishedUUIDs.length === 0) {
this.log.warn('The device discovery could not find any Apple TV devices until now. Are you sure that you have a compatible \
Apple TV and the Apple TV is in the same subnet? (see \
https://github.com/maxileith/homebridge-appletv-enhanced/tree/main?tab=readme-ov-file#requirements)');
}
}
}
exports.AppleTVEnhancedPlatform = AppleTVEnhancedPlatform;
//# sourceMappingURL=appleTVEnhancedPlatform.js.map