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