UNPKG

homebridge-luxor

Version:

Homebridge Plug-in for the FX Luminaire (Luxor) lighting controller

285 lines (267 loc) 13.1 kB
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; }