homebridge-luxor
Version:
Homebridge Plug-in for the FX Luminaire (Luxor) lighting controller
285 lines (267 loc) • 13.1 kB
text/typescript
const axios = require('axios').default;
import { AxiosResponse } from 'axios';
import { API, Characteristic, DynamicPlatformPlugin, Logger, PlatformAccessory, PlatformConfig, Service } from 'homebridge';
import { BaseController, IControllerType, IGroupList, IThemeList } from './controller/BaseController';
import { ControllerFactory } from './controller/ControllerFactory';
import { LightFactory } from './lights/LightFactory';
import { Theme } from './lights/Theme';
import { ILightType } from './lights/ZD_Light';
export class LuxorPlatform implements DynamicPlatformPlugin {
// this is used to track restored cached accessories
public accessories: PlatformAccessory[] = [];
public controller: BaseController;// will be assigned to ZD or ZDC controller
public Name: string;
public lastDateAdded: number;
public readonly Service: typeof Service;
public readonly Characteristic: typeof Characteristic;
private currGroupsAndThemes: IGroupList[] & IThemeList[] = [];
constructor(
public readonly log: Logger,
public readonly config: PlatformConfig,
public readonly api: API
) {
this.config = config;
this.log = log;
this.Service = this.api.hap.Service;
this.Characteristic = this.api.hap.Characteristic;
this.Name = config.name;
this.lastDateAdded = Date.now();
this.controller = ControllerFactory.createController({ type: 'base' }, this.log)
if (api) {
// Save the API object as plugin needs to register new this.api.platformAccessory via this object.
this.api = api;
// Listen to event "didFinishLaunching", this means homebridge already finished loading cached accessories
// Platform Plugin should only register new this.api.platformAccessory that doesn't exist in homebridge after this event.
// Or start discover new accessories
this.api.on('didFinishLaunching', this.didFinishLaunchingAsync.bind(this));
}
}
async sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// Function invoked when homebridge tries to restore cached accessory
// Developer can configure accessory at here (like setup event handler)
configureAccessory(accessory: PlatformAccessory) {
this.log.debug(`Retrieved cached accessory ${accessory.displayName} with UUID ${accessory.UUID}`);
this.accessories[accessory.UUID] = accessory;
}
async getControllerAsync():Promise<boolean> {
// get the name of the controller
this.log.info(this.Name + ": Starting search for controller at: " + this.config.ipAddr);
try {
//Search for controllor and make sure we can find it
const response:AxiosResponse = await axios({
method: 'post',
url: 'http://' + this.config.ipAddr + '/ControllerName.json',
timeout: this.config.commandTimeout || 750
});
if (response.status !== 200) { this.log.error('Received a status code of ' + response.status + ' when trying to connect to the controller.'); return false; }
let controllerNameData = response.data;
controllerNameData.ip = this.config.ipAddr;
controllerNameData.platform = this;
controllerNameData.commandTimeout = this.config.commandTimeout;
if (controllerNameData.Controller.substring(0, 5) === 'luxor') {
controllerNameData.type = IControllerType.ZD;
} else if (controllerNameData.Controller.substring(0, 5) === 'lxzdc') {
controllerNameData.type = IControllerType.ZDC;
} else if (controllerNameData.Controller.substring(0, 5) === 'lxtwo') {
controllerNameData.type = IControllerType.ZDTWO;
} else {
controllerNameData.type = IControllerType.ZDTWO;
this.log.info('Found unknown controller named %s of type %s, assuming a ZDTWO', controllerNameData.Controller, controllerNameData.type);
}
this.log.info(`Found Controller named ${controllerNameData.Controller} of type ${controllerNameData.type}.`);
this.controller = ControllerFactory.createController(controllerNameData, this.log);
return true;
}
catch (err) {
this.log.error(this.Name + ' was not able to connect to connect to the controller. ', err);
return false;
};
}
async getControllerGroupListAsync() {
// Get the list of light groups from the controller
if (this.config.hideGroups) return;
try {
let groupLists = await this.controller.GroupListGetAsync();
this.log.info(`Retrieved ${groupLists.length} light groups from controller.`);
for (var i in groupLists) {
this.currGroupsAndThemes.push(groupLists[i]);
}
}
catch (err) {
this.log.error(`was not able to retrieve light groups from controller.\n${err}\n${err}`);
};
}
async getControllerThemeListAsync() {
// Get the list of light LuxorThemes from the controller
try {
let themeLists = await this.controller.ThemeListGetAsync();
this.log.info(`Retrieved ${themeLists.length} themes from controller.`);
if (typeof this.config.noAllThemes !== 'undefined' && this.config.noAllThemes){
this.log.info(`Not creating Illuminate All and Extinguish All themes per config setting.`);
}
else {
themeLists.push({
Name: 'Illuminate all lights',
ThemeIndex: 100,
OnOff: 0,
isOn: false,
type: ILightType.THEME
});
themeLists.push({
Name: 'Extinguish all lights',
ThemeIndex: 101,
OnOff: 0,
isOn: false,
type: ILightType.THEME
});
}
for (var i in themeLists) {
themeLists[i].type = ILightType.THEME;
this.currGroupsAndThemes.push(themeLists[i]);
}
}
catch (err) {
this.log.error('was not able to retrieve light themes from controller.', err);
};
}
removeAccessories() {
for (var UUID in this.accessories) {
let accessory = this.accessories[UUID];
if (typeof this.config.removeAllAccessories !== 'undefined' && this.config.removeAllAccessories || typeof this.config.removeAccessories !== 'undefined' && this.config.removeAccessories.includes(accessory.UUID)) {
this.log.info(`Removing cached accessory ${accessory.displayName} with UUID ${accessory.UUID} per platform configuration settings.`);
this.api.unregisterPlatformAccessories("homebridge-luxor", "Luxor", [accessory]);
this.accessories = this.accessories.filter(item => item.UUID !== UUID);
};
}
}
addGroupAccessory(lightGroup: IGroupList) {
var accessory = new this.api.platformAccessory(lightGroup.Name, lightGroup.UUID);
let context: IContext = {
lastDateAdded: this.lastDateAdded,
color: lightGroup.Color,
groupNumber: lightGroup.GroupNumber,
brightness: lightGroup.Intensity,
type: lightGroup.type,
isOn: lightGroup.Intensity > 0,
independentColors: this.config.independentColors,
commandTimeout: this.config.commandTimeout
}
accessory.context = context;
LightFactory.createLight(this, accessory);
this.api.registerPlatformAccessories("homebridge-luxor", "Luxor", [accessory]);
}
addThemeAccessory(themeGroup: IThemeList) {
var accessory = new this.api.platformAccessory(themeGroup.Name, themeGroup.UUID);
let context: IContext = {
lastDateAdded: this.lastDateAdded,
type: ILightType.THEME,
isOn: themeGroup.OnOff === 1,
themeIndex: themeGroup.ThemeIndex,
OnOff: themeGroup.OnOff,
commandTimeout: this.config.commandTimeout
}
accessory.context = context;
LightFactory.createLight(this, accessory);
this.accessories[accessory.UUID] = accessory;
this.api.registerPlatformAccessories("homebridge-luxor", "Luxor", [accessory]);
}
assignUUIDs() {
for (let i = 0; i < this.currGroupsAndThemes.length; i++) {
let acc = this.currGroupsAndThemes[i];
if (typeof acc.ThemeIndex !== 'undefined') {
acc.UUID = this.api.hap.uuid.generate('luxor.' + `theme-${acc.ThemeIndex}`);
}
else {
acc.UUID = this.api.hap.uuid.generate('luxor.' + `group.-${acc.GroupNumber}`);
}
}
}
async processAccessories() {
this.assignUUIDs();
this.removeAccessories()
for (var UUID in this.accessories) {
let cachedAcc = this.accessories[UUID];
// look for match on current devices
let remove = true;
for (let j = 0; j < this.currGroupsAndThemes.length; j++) {
let currAcc = this.currGroupsAndThemes[j];
if (cachedAcc.UUID === currAcc.UUID) {
// found existing device
this.log.info(`Loading cached accessory ${cachedAcc.displayName} with UUID ${cachedAcc.UUID}.`);
// update cached device (name, etc)
let context: IContext = cachedAcc.context as IContext;
context.lastDateAdded = this.lastDateAdded;
if (typeof currAcc.Color !== 'undefined') context.color = currAcc.Color;
if (typeof currAcc.GroupNumber !== 'undefined') context.groupNumber = currAcc.GroupNumber;
if (typeof currAcc.ThemeIndex !== 'undefined') context.themeIndex = currAcc.ThemeIndex;
if (typeof currAcc.Intensity !== 'undefined') {
context.brightness = currAcc.Intensity;
context.isOn = currAcc.Intensity > 0;
}
if (typeof currAcc.type !== 'undefined') context.type = currAcc.type;
if (typeof currAcc.isOn !== 'undefined') context.isOn = currAcc.isOn;
if (typeof currAcc.Name !== 'undefined') cachedAcc.displayName = currAcc.Name;
cachedAcc.context = context;
this.api.updatePlatformAccessories([cachedAcc]);
LightFactory.createLight(this, cachedAcc);
this.currGroupsAndThemes.splice(j, 1);
remove = false;
break;
}
}
// remove the cachedAcc that can't be matched
if (remove) {
this.log.info(`Removing cached accessory ${cachedAcc.displayName} with UUID ${cachedAcc.UUID}.`);
this.api.unregisterPlatformAccessories("homebridge-luxor", "Luxor", [cachedAcc]);
}
}
// add any new accessories that were not previously matched
if (this.currGroupsAndThemes.length > 0) {
for (let j = 0; j < this.currGroupsAndThemes.length; j++) {
let currAcc = this.currGroupsAndThemes[j];
this.log.info(`Adding new accessory ${currAcc.Name} with UUID ${currAcc.UUID}.`);
if (currAcc.type === ILightType.THEME)
this.addThemeAccessory(currAcc);
else
this.addGroupAccessory(currAcc);
}
}
}
async didFinishLaunchingAsync() {
if (!this.config.ipAddr) {
this.log.error(this.Name + " needs an IP Address in the config file. Please see sample_config.json.");
}
try {
while (await this.getControllerAsync() == false) {
this.log.info(`Unable to connect to Luxor controller. Waiting 60s and will retry.`)
await this.sleep(60*1000);
}
//this.retrieveCachedAccessories();
await this.getControllerGroupListAsync();
await this.getControllerThemeListAsync();
await this.processAccessories();
// this.removeOphanedAccessories();
this.log.info('Finished initializing');
}
catch (err) {
this.log.error('Error in didFinishLaunching', err);
};
}
}
export interface IContext {
lastDateAdded: number;
groupNumber?: number;
brightness?: number;
type: ILightType
color?: number;
status?: any;
isOn: boolean;
hue?: number;
saturation?: number;
themeIndex?: number;
OnOff?: 0 | 1;
independentColors?: boolean;
commandTimeout: number;
}