homebridge-hca
Version:
HCA plugin for Homebridge
251 lines (185 loc) • 7.3 kB
JavaScript
const http = require('http');
const HcaClient = require('node-hca');
const itemType = require('node-hca/lib/Design/itemType');
const AccessoryFactory = require('./lib/AccessoryFactory');
let Homebridge, self, uuid;
module.exports = function (homebridge) {
// const Accessory = homebridge.platformAccessory;
// const Service = homebridge.hap.Service;
// const Characteristic = homebridge.hap.Characteristic;
uuid = homebridge.hap.uuid;
Homebridge = homebridge;
// AccessoryFactory = require('./lib/AccessoryFactory')(Accessory, Service, Characteristic, uuid); // !!!
Homebridge.registerPlatform('homebridge-hca', 'HCA', HcaPlatform, true);
}
const HcaPlatform = function (log, config, api) {
self = this;
this.log = log;
this.config = config;
this.accessories = {};
if (!config) {
this.log.warn('WARNING: No configuration found... plugin will not be initialized.');
return;
}
this.host = config.host || 'localhost';
this.port = config.port || 2000;
this.password = config.password;
this.clientName = config.clientName || 'Homebridge';
this.verbose = config.verbose || config.debug || false;
this.log.info(`********************************`);
this.log.info(`* Homebridge HCA *`);
this.log.info(`********************************`);
this.log.info(`Homebridge API version: ${api.version}`);
if (api) {
this.api = api;
this.api.on('didFinishLaunching', function () {
const cachedAccessoryCount = Object.keys(this.accessories).length;
this.log.info('Found %s cached accessories.', cachedAccessoryCount);
init();
}.bind(this));
}
}
HcaPlatform.prototype.configureAccessory = function (accessory) {
accessory.context.isExposed = false;
this.accessories[accessory.UUID] = accessory;
}
HcaPlatform.prototype.discoverAccessories = function () {
self.log.info('Discovering accessories...');
const items = self.client.designManager.getItems();
for (let index in items) {
const item = items[index];
if (item.voiceAssistantEnabled) {
self.addAccessory(item);
}
}
removeUnexposedAccessories();
self.log.info('Discovered %d accessories.', Object.keys(self.accessories).length);
}
HcaPlatform.prototype.addAccessory = function (item) {
const cachedAccessoryUUID = uuid.generate('hca:accessories:' + item.uid);
const cachedAccessory = this.accessories[cachedAccessoryUUID];
if (cachedAccessory) {
this.updateAccessoryName(cachedAccessory);
cachedAccessory.log = this.log;
cachedAccessory.client = this.client;
cachedAccessory.context.item = item;
cachedAccessory.context.isExposed = true;
this.accessoryFactory.restoreAccessory(cachedAccessory);
this.api.updatePlatformAccessories('homebridge-hca', 'HCA', [cachedAccessory]);
this.log.info('Restored %s (%s).', cachedAccessory.displayName, item.id);
return;
}
const accessory = this.accessoryFactory.createAccessory(item);
if (!accessory) {
this.log.warn('Unsupported accessory: %s (%s).', item.name, item.id);
this.log.debug('%s (%s):', item.name, item.id, JSON.stringify(item));
return;
}
accessory.context.isExposed = true;
this.accessories[accessory.UUID] = accessory;
this.api.registerPlatformAccessories('homebridge-hca', 'HCA', [accessory]);
this.log.warn('[+] Added %s (%s).', accessory.name, item.id);
}
HcaPlatform.prototype.removeAccessory = function (accessory) {
let accessoryId = accessory.context.item ? accessory.context.item.id : '??';
if (this.accessories[accessory.UUID]) {
this.log.warn('[-] Removed %s (%s).', accessory.displayName, accessoryId);
delete this.accessories[accessory.UUID];
}
this.api.unregisterPlatformAccessories("homebridge-hca", "HCA", [accessory]);
}
HcaPlatform.prototype.updateAccessoryName = function (accessory) {
const currentName = accessory.displayName;
const newName = accessory.context.item.voiceAssistantName || accessory.context.item.name;
if (currentName === newName)
return;
accessory.name = newName;
accessory.displayName = newName;
accessory._associatedHAPAccessory.displayName = newName;
accessory.services.forEach(function (service) {
if (service.displayName === currentName) {
service.displayName = newName;
}
const nameCharacteristic = service.getCharacteristic(Characteristic.Name);
nameCharacteristic.setValue(newName);
});
this.log.warn(`Accessory name changed: ${currentName} >> ${newName}.`);
}
const removeUnexposedAccessories = function () {
for (let index in self.accessories) {
const accessory = self.accessories[index];
if (!accessory.context.isExposed)
self.removeAccessory(accessory);
}
}
const setUnexposedAccessories = function () {
for (let index in self.accessories) {
const accessory = self.accessories[index];
accessory.context.isExposed = false;
}
}
const init = function () {
self.client = new HcaClient(self.host, self.port, self.clientName);
self.accessoryFactory = new AccessoryFactory(Homebridge, self.log, self.client);
self.client.on('Notify', onNotify);
self.client.on('Connection:Opened', onConnectionOpened);
self.client.on('Connection:Closed', onConnectionClosed);
if (self.verbose) {
self.client.on('Connection:MessageSent', onMessageSent);
self.client.on('Connection:MessageReceived', onMessageReceived);
}
self.client.once(self.client.topics.clientReady, function () {
const serverVersionParts = self.client.serverVersion.split('.');
self.log.info(`HCA Server version: ${self.client.serverVersion} (Protocol ${self.client.serverProtocol}).`)
// Ensure we're running a supported version of HCA.
if (serverVersionParts[0] <= 14 && serverVersionParts[1] < 1) {
self.log.error('This plugin requires HCA Plus version 14.1 (or higher).');
return;
}
self.discoverAccessories();
});
self.client.connect(self.password);
}
const onConnectionOpened = function (e) {
self.log.info('Connection opened.');
}
const onConnectionClosed = function (e) {
const msg = `Connection closed. (Code=${e.code}, Reason=${e.reason}, WasClean=${e.wasClean}).`;
if (!e.wasClean) {
self.log.warn(msg);
self.client.removeListener(self.client.topics.clientReady, self.discoverAccessories);
self.client.once(self.client.topics.clientReady, self.discoverAccessories);
} else {
self.log.info(msg);
}
// setUnreachableAccessories();
}
const onMessageSent = function (params) {
self.log.debug('[SNT]', params);
}
const onMessageReceived = function (response) {
self.log.debug('[RCV]', JSON.stringify(response));
}
const onNotify = function (response) {
const designChangeType = parseInt(response.data[0]);
if (designChangeType !== 2) { // 2 = major, 3 = minor
return;
}
self.log.warn('Major design change made. Rediscovering accessories.')
self.client.designManager.once('Design:Received', function () {
self.discoverAccessories();
});
setUnexposedAccessories();
self.client.designManager.init();
}
process.on('uncaughtException', function (err) {
if (self)
self.log.error(err);
else
console.error(err);
process.exit();
});
process.on('exit', function (code) {
if (self && self.client)
self.client.disconnect();
});