UNPKG

homebridge-appletv-enhanced

Version:

Plugin that exposes the Apple TV to HomeKit with much richer features than the vanilla Apple TV implementation of HomeKit.

221 lines 11.5 kB
"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 UpdateChecker_1 = __importDefault(require("./UpdateChecker")); const fs_1 = __importDefault(require("fs")); // compatible model identifiers according to https://pyatv.dev/api/const/#pyatv.const.DeviceModel const ALLOWED_MODELS = [ 'Gen4', 'Gen4K', 'AppleTVGen4', // future proof since they will be renamed in pyatv 'AppleTVGen4K', // future proof since they will be renamed in pyatv 'AppleTV4KGen2', 'AppleTV4KGen3', ]; const DEV_MODE = process.env.APPLETV_ENHANCED_DEV?.toLowerCase() === 'true'; class AppleTVEnhancedPlatform { config; api; characteristic; logLevelLogger; service; 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'); // enable update check const updateChecker = new UpdateChecker_1.default(this.logLevelLogger, this.isAutoUpdateOn(), this.config.updateCheckLevel === 'beta', this.config.updateCheckTime); await updateChecker.check('success'); updateChecker.startInterval(true); // 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 !== undefined && 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); }); } // 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} / ${appleTV.host}).`); if (appleTV.mac === undefined || appleTV.mac === null) { this.log.debug(`${appleTV.name} (${appleTV.host}) is skipped since the MAC address could not be determined.`); continue; } const mac = appleTV.mac.toUpperCase(); if (this.config.discover?.blacklist && (this.config.discover.blacklist.map((e) => e.toUpperCase()).includes(mac) || this.config.discover.blacklist.includes(appleTV.host))) { this.log.debug(`${appleTV.name} (${appleTV.mac} / ${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 const uuid = this.api.hap.uuid.generate(DEV_MODE === true ? `x${mac}` : mac); 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 accessory = 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 accessory.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 ...`); await new appleTVEnhancedAccessory_1.AppleTVEnhancedAccessory(this, accessory).untilBooted(); // 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, [accessory]); })(); } this.log.debug('Finished device discovery.'); } isAutoUpdateOn() { switch (this.config.autoUpdate) { case 'on': return true; case 'off': return false; case 'auto': case undefined: default: // by default, autoUpdate should be turned on when the plugin is running as a child bridge const ogConfig = JSON.parse(fs_1.default.readFileSync(this.api.user.configPath(), 'utf-8')); const ogAppleTVEnhancedConfig = ogConfig.platforms.find((p) => p.platform === 'AppleTVEnhanced'); if (ogAppleTVEnhancedConfig !== undefined) { return ogAppleTVEnhancedConfig._bridge !== undefined; } else { this.log.warn('Could not determine whether or not the plugin is running as a child bridge. Therefore, the default \ setting for automatic updates could not be determined. Falling back to "off". You can enable or disable automatic updates in the \ configuration explicitly.'); return false; } } } 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