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
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 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