UNPKG

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.

235 lines 12.2 kB
"use strict"; 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")); /** * Platform Accessory * An instance of this class is created for each accessory your platform registers * Each accessory may expose multiple services of different service types. */ class AirportExpressAccessory { platform; accessory; accessoryInformation; device; lastOnline = 0; reachable = true; service; constructor(platform, accessory) { this.platform = platform; this.accessory = accessory; this.device = this.accessory.context.device; this.platform.log.debug(`${this.device.displayName} - Accessory: Constructing`); // set accessory information this.accessoryInformation = this.accessory .getService(this.platform.service.AccessoryInformation) .setCharacteristic(this.platform.characteristic.Manufacturer, 'Apple Inc.') .setCharacteristic(this.platform.characteristic.Model, 'A1392') .setCharacteristic(this.platform.characteristic.SerialNumber, this.device.serialNumber); // get the OccupancySensor service if it exists, otherwise create a new OccupancySensor service // you can create multiple services for each accessory this.service = this.accessory.getService(this.platform.service.OccupancySensor) || this.accessory.addService(this.platform.service.OccupancySensor); // set the service name, this is what is displayed as the default name on the Home app // in this case we are using the name we stored in the `accessory.context` in the `discoverDevice this.service.setCharacteristic(this.platform.characteristic.Name, this.device.displayName); // create handlers for required characteristics this.service .getCharacteristic(this.platform.characteristic.OccupancyDetected) .onGet(this.handleGet.bind(this)); // log that an device has been created this.platform.log.info(`${this.device.displayName} - Accessory: AirPort Express device with serial number ${this.device.serialNumber} created!`); // update the connection state periodically this.platform.log.debug(`${this.device.displayName} - Accessory: Starting update loop`); this.updateConnectedStatus(); setInterval(this.updateConnectedStatus.bind(this), this.platform.config.update.refreshRate * 1000); } changeFirmware(mdnsTxtRecord) { let firmwareVersion = mdnsTxtRecord .find((r) => r.includes('fv')) .replace('fv=', ''); firmwareVersion = firmwareVersion === 'p20.78100.3' ? '7.8.1' : firmwareVersion; const prevFirmwareVersion = this.accessoryInformation.getCharacteristic(this.platform.characteristic.FirmwareRevision).value; if (firmwareVersion !== prevFirmwareVersion) { this.platform.log.info(`Set Firmware of ${this.device.displayName} to "${firmwareVersion}".`); this.accessoryInformation.setCharacteristic(this.platform.characteristic.FirmwareRevision, firmwareVersion); } } changeName(fullname) { const displayName = fullname.replace('._airplay._tcp.local', ''); if (this.device.displayName !== displayName) { this.platform.log.info(`${this.device.displayName} - Update: Renaming to "${displayName}" since the AirPlay speaker fullname was changed.`); this.device.displayName = displayName; this.service.setCharacteristic(this.platform.characteristic.Name, displayName); } else { this.platform.log.debug(`${this.device.displayName} - Update: Name unchanged.`); } } async handleGet() { const connected = this.service.getCharacteristic(this.platform.characteristic.OccupancyDetected).value; const answer = !this.reachable ? 'not responding' : connected === this.platform.characteristic.OccupancyDetected.OCCUPANCY_DETECTED ? 'connected' : 'disconnected'; this.platform.log.debug(`${this.device.displayName} - Pull: Received GET request from HomeKit. Answer: ${answer}`); if (!this.reachable) { throw new this.platform.api.hap.HapStatusError(-70402 /* this.platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE */); } return connected; } hexStringToBitString(hex) { return this.reverseString(parseInt(hex, 16).toString(2).padStart(20, '0')); } isDeviceConnected(mdnsTxtRecord) { const flagsHex = mdnsTxtRecord .find((r) => r.includes('flag')) .replace('flags=', ''); this.platform.log.debug(`${this.device.displayName} - Update: Flags hex ${flagsHex}`); const flagsBits = this.hexStringToBitString(flagsHex); this.platform.log.debug(`${this.device.displayName} - Update: Flags Bits ${flagsBits}`); /* bit11 corresponds to playing * see https://openairplay.github.io/airplay-spec/status_flags.html */ const bit11 = flagsBits.charAt(11) === '1'; this.platform.log.debug(`${this.device.displayName} - Update: Bit 11 is "${bit11}"`); const gcgl = mdnsTxtRecord .find((r) => r.includes('gcgl')) .replace('gcgl=', '') === '1'; this.platform.log.debug(`${this.device.displayName} - Update: gcgl is "${gcgl}"`); if (this.platform.config.update.ignoreGroupWithLeadingDevice) { if (gcgl && bit11 || !bit11) { return this.platform.characteristic.OccupancyDetected .OCCUPANCY_NOT_DETECTED; } else { return this.platform.characteristic.OccupancyDetected .OCCUPANCY_DETECTED; } } else { if (bit11) { return this.platform.characteristic.OccupancyDetected .OCCUPANCY_DETECTED; } else { return this.platform.characteristic.OccupancyDetected .OCCUPANCY_NOT_DETECTED; } } } reverseString(value) { return value.split('').reverse().join(''); } setConnectStatus(status) { // exit if there is no status change if (this.service.getCharacteristic(this.platform.characteristic.OccupancyDetected).value === status) { this.platform.log.debug(`${this.device.displayName} - Update: Connection Status unchanged: ${status ? 'connected' : 'disconnected'}`); return; } this.service.setCharacteristic(this.platform.characteristic.OccupancyDetected, status); if (status === this.platform.characteristic.OccupancyDetected.OCCUPANCY_DETECTED) { this.platform.log.info(`${this.device.displayName} - Update: Has now an active AirPlay connection.`); } else { this.platform.log.info(`${this.device.displayName} - Update: Has no active AirPlay connection.`); } } setReachableStatus(reachable) { // check if unreachable should be ignored if (this.platform.config.update.unreachable.ignore) { return; } // exit if there is no status change if (this.reachable === reachable) { this.platform.log.debug(`${this.device.displayName} - Update: Reachable status unchanged: ${reachable ? 'reachable' : 'unreachable'}`); return; } this.reachable = reachable; if (reachable) { this.platform.log.info(`${this.device.displayName} - Update: reachable`); } else { this.platform.log.warn(`${this.device.displayName} - Update: unreachable`); // report a disconnect if this has been configured to be done if (this.platform.config.update.unreachable.reportDisconnect) { this.setConnectStatus(this.platform.characteristic.OccupancyDetected .OCCUPANCY_NOT_DETECTED); } } } updateConnectedStatus() { let found = false; this.platform.log.debug(`${this.device.displayName} - Update: AirPort Express with serial number ${this.device.serialNumber}`); this.platform.log.debug(`${this.device.displayName} - Update: 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')); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access mdnsBrowser.on('ready', () => { this.platform.log.debug(`${this.device.displayName} - Update: 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) => { try { if (data?.txt) { const foundSerialNumber = data.txt .find((str) => str.includes('serialNumber')) ?.replace('serialNumber=', ''); if (data.txt.includes('model=AirPort10,115') && foundSerialNumber !== undefined && this.device.serialNumber === foundSerialNumber) { this.platform.log.debug(`${this.device.displayName} - Update: Got mDNS reply from correct device with serial number \ ${this.device.serialNumber}`); this.changeName(data.fullname); this.platform.log.debug(`${this.device.displayName} - Update: txt record contents: ${data.txt}`); this.changeFirmware(data.txt); this.setConnectStatus(this.isDeviceConnected(data.txt)); this.setReachableStatus(true); this.lastOnline = Date.now() / 1000; this.platform.log.debug(`${this.device.displayName} - Update: Stopping browser`); found = true; // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access mdnsBrowser.stop(); } } } catch (error) { this.platform.log.error(`${this.device.displayName} - Update: Error in mDNS check, found invalid record`); this.platform.log.debug(error); this.platform.log.debug(`${this.device.displayName} - Update: Stopping browser`); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access mdnsBrowser.stop(); } }); setTimeout(() => { try { if (!found) { const secondsOffline = Math.round(Date.now() / 1000 - this.lastOnline); this.platform.log.debug(`${this.device.displayName} - Update: Device did not respond to the mDNS disovery. The device is not responding \ since ${secondsOffline} seconds.`); if (this.lastOnline + this.platform.config.update.unreachable.threshold <= Date.now() / 1000) { this.setReachableStatus(false); } } // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access mdnsBrowser.stop(); } catch (err) { this.platform.log.debug(`${this.device.displayName} - Update: Error during stopping the browser: ${err}`); } }, this.platform.config.update.refreshRate * 1000); } } exports.default = AirportExpressAccessory; //# sourceMappingURL=airportExpressAccessory.js.map