homebridge-airport-express-connected
Version:
Homebridge plugin that uses mDNS request data to show a occupancy sensor that is activated when connecting to an airport express devices via AirPlay 2.
220 lines • 11.3 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const mdns_js_1 = __importDefault(require("mdns-js"));
const settings_1 = require("./settings");
const airportExpressAccessory_1 = __importDefault(require("./airportExpressAccessory"));
/**
* HomebridgePlatform
* This class is the main constructor for your plugin, this is where you should
* parse the user config and discover/register accessories with Homebridge.
*/
class AirportExpressConnectedPlatform {
log;
config;
api;
// this is used to track restored cached accessories
accessories = [];
characteristic;
service;
constructor(log, config, api) {
this.log = log;
this.config = config;
this.api = api;
this.characteristic = this.api.hap.Characteristic;
this.service = this.api.hap.Service;
this.log.debug('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.
this.api.on('didFinishLaunching', () => {
this.log.debug('Executed didFinishLaunching callback');
this.log.debug('Processing configuration:');
this.log.debug(this.config);
this.config = this.processConfiguration(this.config);
this.log.debug(this.config);
this.loadCachedDevices();
// discover devices periodically
if (this.config.discovery.enabled) {
// discover devices periodically
this.log.info('Searching for AirPort Express devices ...');
this.discoverDevices();
if (this.config.discovery.always) {
setInterval(this.discoverDevices.bind(this), this.config.discovery.intervals * 1000);
}
}
else {
this.log.info('Not searching for AirPort Express devices, due to the discovery being disabled.');
}
});
}
/**
* This function is invoked when homebridge restores cached accessories from disk at startup.
* It should be used to setup event handlers for characteristics and update respective values.
*/
configureAccessory(accessory) {
this.log.info(`Cache: Loading accessory ${accessory.displayName}`);
// add the restored accessory to the accessories cache so we can track if it has already been registered
this.accessories.push(accessory);
this.log.debug(`Cache: Finished loading accessory ${accessory.displayName}`);
}
/**
* 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.
*/
discoverDevices() {
this.log.debug('Discovery: Creating browser');
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
const mdnsBrowser = mdns_js_1.default.createBrowser(mdns_js_1.default.tcp('airplay'));
// discover devices
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
mdnsBrowser.on('ready', () => {
this.log.debug('Discovery: Starting discovery with browser');
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
mdnsBrowser.discover();
});
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
mdnsBrowser.on('update', (data) => {
// make sure we are looking at an AirPort Express 2nd Gen.
if (data.txt === undefined || data.txt.includes('model=AirPort10,115') === false) {
return;
}
// extract serial number
const serialNumber = data.txt.find((str) => str.includes('serialNumber'))?.replace('serialNumber=', '') ?? '';
// generate distinct ID
const uuid = this.api.hap.uuid.generate(serialNumber);
this.log.debug(`Discovery: AirPort Express with serial number ${serialNumber} found. Generated uuid ${uuid}.`);
// see if an accessory with the same uuid has already been registered and restored from
// the cached devices we stored in the `configureAccessory` method above
const existingAccessory = this.accessories.find((accessory) => accessory.UUID === uuid);
if (existingAccessory) {
this.log.debug(`Discovery: AirPort Express with uuid ${uuid} already exists.`);
return;
}
if (!this.evalBlackWhiteList(serialNumber)) {
const l = this.config.discovery.whitelist.enabled
? 'not on the whitelist'
: 'on the blacklist';
this.log.debug(`Discovery: Won't add AirPort Express with serial number ${serialNumber} since it is ${l}.`);
return;
}
// check if fullname is legit
if (!data.fullname?.includes('._airplay._tcp.local')) {
this.log.debug(`Dicovery: Fullname "${data.fullname}" is invalid. Not adding as an accessory.`);
return;
}
// extract additional meta information
const displayName = data.fullname.replace('._airplay._tcp.local', '');
// the accessory does not yet exist, so we need to create it
this.log.info('Discovery: Adding new accessory ', displayName);
// create a new accessory
const accessory = new this.api.platformAccessory(displayName, 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.device = {
serialNumber: serialNumber,
displayName: displayName,
data: data,
};
// create the accessory handler for the newly create accessory
// this is imported from `platformAccessory.ts`
new airportExpressAccessory_1.default(this, accessory);
// link the accessory to your platform
this.api.registerPlatformAccessories(settings_1.PLUGIN_NAME, settings_1.PLATFORM_NAME, [accessory]);
this.configureAccessory(accessory);
this.log.debug(`Discovery: Finished creating and configuring accessory ${displayName}`);
});
setTimeout(() => {
try {
this.log.debug('Discovery: Stopping browser');
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
mdnsBrowser.stop();
}
catch (err) {
this.log.debug(`Discovery: Error during stopping the browser: ${err}`);
}
}, this.config.discovery.intervals * 1000);
}
evalBlackWhiteList(serialNumber) {
if (this.config.discovery.whitelist.enabled &&
this.config.discovery.blacklist.enabled) {
this.log.error('Whitelist and Blacklist cannot be enabled at the same time');
return true;
}
if (this.config.discovery.whitelist.enabled) {
return this.config.discovery.whitelist.list.includes(serialNumber) ? true : false;
}
if (this.config.discovery.blacklist.enabled) {
return !this.config.discovery.blacklist.list.includes(serialNumber);
}
return true;
}
loadCachedDevices() {
for (const accessory of this.accessories) {
const device = accessory.context.device;
if (!this.evalBlackWhiteList(device.serialNumber)) {
const l = this.config.discovery.whitelist.enabled
? 'not whitelisted'
: 'blacklisted';
if (this.config.discovery.discardKnownDevices) {
this.api.unregisterPlatformAccessories(settings_1.PLUGIN_NAME, settings_1.PLATFORM_NAME, [accessory]);
this.log.warn(`Cache: Unregistered "${device.displayName}" (serial number: \
${device.serialNumber}) since it is ${l}.`);
return;
}
else {
this.log.warn(`Cache: The cached AirPort Express device "${device.displayName}" (serial number: \
${device.serialNumber}) would not be discovered with the current configuration as it is ${l}. You can remove the cached \
device in the Homebridge UI or activate to discard known devices that do not match the white- or blacklist rules in the configuration.`);
}
}
this.log.info('Cache: Restoring existing accessory from cache: ', device.displayName);
// create the accessory handler for the restored accessory
// this is imported from `platformAccessory.ts`
new airportExpressAccessory_1.default(this, accessory);
this.log.debug('Finished restoring accessory from cache: ', device.displayName);
}
}
processConfiguration(config) {
const c = {
name: config.name,
platform: config.platform,
update: {
refreshRate: config.update?.refreshRate ?? 3,
ignoreGroupWithLeadingDevice: config.update?.ignoreGroupWithLeadingDevice === false ? false : true,
unreachable: {
ignore: config.update?.unreachable?.ignore ?? false,
threshold: config.update?.unreachable?.threshold ?? 30,
reportDisconnect: config.update?.unreachable?.reportDisconnect ?? false,
},
},
discovery: {
enabled: config.discovery?.enabled ?? true,
always: config.discovery?.always ?? true,
intervals: config.discovery?.intervals ?? 60,
whitelist: {
enabled: config.discovery?.whitelist?.enabled ?? false,
list: config.discovery?.whitelist?.list ?? [],
},
blacklist: {
enabled: config.discovery?.blacklist?.enabled ?? false,
list: config.discovery?.blacklist?.list ?? [],
},
discardKnownDevices: config.discovery?.discardKnownDevices ?? false,
},
};
if (c.discovery.whitelist.enabled &&
c.discovery.blacklist.enabled) {
this.log.warn('Both whitelist and blacklist are enabled. Disabling both. Only enable one or the other in the configuration.');
c.discovery.whitelist.enabled = false;
c.discovery.blacklist.enabled = false;
}
return c;
}
}
exports.default = AirportExpressConnectedPlatform;
//# sourceMappingURL=airportExpressConnectedPlatform.js.map