UNPKG

homebridge-harmony

Version:

Publish your harmony activities as homekit accessories

1,459 lines (1,278 loc) 47.3 kB
var Service, Characteristic, Accessory, Categories, AccessControlManagement, AccessControlEvent; const HarmonyBase = require('./harmonyBase').HarmonyBase; const HarmonyConst = require('./harmonyConst'); const HarmonyTools = require('./harmonyTools.js'); const HarmonyAsTVKeysTools = require('./harmonyAsTVKeysTools.js'); const fs = require('fs'); module.exports = { HarmonySubPlatform: HarmonySubPlatform, }; function HarmonySubPlatform(log, config, api, mainPlatform) { Service = api.hap.Service; Characteristic = api.hap.Characteristic; Accessory = api.hap.Accessory; Categories = api.hap.Categories; AccessControlManagement = api.hap.AccessControlManagement; AccessControlEvent = api.hap.AccessControlEvent; this.api = api; this.mainPlatform = mainPlatform; this.harmonyBase = new HarmonyBase(api); this.harmonyBase.configCommonProperties(log, config, this); this.TVAccessory = HarmonyTools.checkParameter(config['TVAccessory'], true); this.receiverIcon = HarmonyTools.checkParameter(config['ReceiverIcon'], false); this.sortInput = HarmonyTools.checkParameter(config['sortInput'], 0); this.publishGeneralMuteSwitch = HarmonyTools.checkParameter( config['publishGeneralMuteSwitch'], false ); this.publishGeneralVolumeSlider = HarmonyTools.checkParameter( config['publishGeneralVolumeSlider'], false ); this.publishGeneralVolumeSwitches = HarmonyTools.checkParameter( config['publishGeneralVolumeSwitches'], false ); this.linkVolumeControlToTV = HarmonyTools.checkParameter(config['linkVolumeControlToTV'], false); this.numberOfCommandsSentForVolumeControl = HarmonyTools.checkParameter( config['numberOfCommandsSentForVolumeControl'], 1 ); if (this.TVAccessory) { this.mainActivity = (this.devMode ? 'DEV' : '') + config['mainActivity']; this.playPauseBehavior = HarmonyTools.checkParameter(config['playPauseBehavior'], false); this.activitiesToPublishAsInputForTVMode = config['activitiesToPublishAsInputForTVMode']; this.remoteOverrideCommandsList = config['remoteOverrideCommandsList']; this.configureAccesscontrol = HarmonyTools.checkParameter( config['configureAccesscontrol'], false ); if (Array.isArray(this.remoteOverrideCommandsList)) { this.log.debug('(' + this.name + ')' + 'INFO - remoteOverrideCommandsList is in new format'); const NewRemoteOverrideCommandsList = {}; this.remoteOverrideCommandsList.forEach((x) => { var commands = {}; x.CommandsList.forEach((y) => (commands[y.CommandName] = y.NewCommand)); NewRemoteOverrideCommandsList[x.ActivityName] = commands; }); this.remoteOverrideCommandsList = NewRemoteOverrideCommandsList; } this.log.debug( '(' + this.name + ')' + 'INFO - remoteOverrideCommandsList is : ' + JSON.stringify(this.remoteOverrideCommandsList) ); if ( !this.addAllActivitiesToSkippedIfSameStateActivitiesList && !this.skippedIfSameStateActivities ) { this.skippedIfSameStateActivities = ['PowerOff']; } this.log.debug( '(' + this.name + ')' + '(' + this.name + ')' + 'INFO - playPause option set to ' + this.playPauseBehavior ); this.playStatus = {}; this.volumesLevel = {}; this.prefsDir = api.user.storagePath(); // check if prefs directory ends with a /, if not then add it if (this.prefsDir.endsWith('/') === false) { this.prefsDir = this.prefsDir + '/'; } this.savedNamesFile = this.prefsDir + 'harmonyPluginNames_' + this.name + '_' + (this.hubIP == undefined ? this.name : this.hubIP.split('.').join('')); this.savedVisibilityFile = this.prefsDir + 'harmonyPluginVisibility_' + this.name + '_' + (this.hubIP == undefined ? this.name : this.hubIP.split('.').join('')); this.savedNames = {}; try { this.savedNames = JSON.parse(fs.readFileSync(this.savedNamesFile)); } catch (err) { this.log.debug('(' + this.name + ')' + 'INFO - input names file does not exist'); } this.savedVisibility = {}; try { this.savedVisibility = JSON.parse(fs.readFileSync(this.savedVisibilityFile)); } catch (err) { this.log.debug('(' + this.name + ')' + 'INFO - input visibility file does not exist'); } } this.isPlatformWithSwitch = HarmonyTools.isPlatformWithSwitch(config); if (this.isPlatformWithSwitch) { this.showTurnOffActivity = HarmonyTools.checkTurnOffActivityOption( config['showTurnOffActivity'] ); this.publishSwitchActivitiesAsIndividualAccessories = HarmonyTools.checkParameter( config['publishSwitchActivitiesAsIndividualAccessories'], true ); this.activitiesToPublishAsAccessoriesSwitch = config['activitiesToPublishAsAccessoriesSwitch']; this.switchAccessories = config['switchAccessories']; if (!this.switchAccessories && !this.activitiesToPublishAsAccessoriesSwitch) { this.activitiesToPublishAsAccessoriesSwitch = []; } } this._confirmedAccessories = []; this._confirmedServices = []; } HarmonySubPlatform.prototype = { //MAIN METHODS onMessage(newActivity) { this.refreshCurrentActivityOnSubPlatform(newActivity); }, readAccessories: function (data, homedata) { let accessoriesToAdd = []; if (this.TVAccessory) accessoriesToAdd.push.apply(accessoriesToAdd, this.readTVAccessories(data)); if (this.isPlatformWithSwitch) accessoriesToAdd.push.apply(accessoriesToAdd, this.readSwitchAccessories(data)); this.harmonyBase.setupFoundAccessories(this, accessoriesToAdd, data, homedata); }, readSwitchAccessories: function (data) { let activities = data.data.activity; let accessoriesToAdd = []; var myHarmonyAccessory; let name = (this.devMode ? 'DEV' : '') + 'Switch'; if (!this.publishSwitchActivitiesAsIndividualAccessories) { myHarmonyAccessory = this.harmonyBase.checkAccessory(this, name); if (!myHarmonyAccessory) { myHarmonyAccessory = this.harmonyBase.createAccessory(this, name); accessoriesToAdd.push(myHarmonyAccessory); } myHarmonyAccessory.category = Categories.SWITCH; this._confirmedAccessories.push(myHarmonyAccessory); } for (let i = 0, len = activities.length; i < len; i++) { if (this.showActivity(activities[i])) { let switchName = this.devMode ? 'DEV' + activities[i].label : activities[i].label; if (this.publishSwitchActivitiesAsIndividualAccessories) { //Handle special case if (switchName === 'TV') switchName = 'TV-Switch'; myHarmonyAccessory = this.harmonyBase.checkAccessory(this, switchName); if (!myHarmonyAccessory) { myHarmonyAccessory = this.harmonyBase.createAccessory(this, switchName); accessoriesToAdd.push(myHarmonyAccessory); } myHarmonyAccessory.category = Categories.SWITCH; this._confirmedAccessories.push(myHarmonyAccessory); } this.log('(' + this.name + ')' + 'INFO - Discovered Activity : ' + switchName); let subType = switchName; let service = this.harmonyBase.getSwitchService( this, myHarmonyAccessory, switchName, subType ); service.activityId = activities[i].id; service.type = HarmonyConst.ACTIVITY_TYPE; this._confirmedServices.push(service); this.bindCharacteristicEventsForSwitch(myHarmonyAccessory, service); } } return accessoriesToAdd; }, readTVAccessories: function (data) { let activities = data.data.activity; let accessoriesToAdd = []; let name = (this.devMode ? 'DEV' : '') + 'TV'; myHarmonyAccessory = this.harmonyBase.checkAccessory(this, name); if (!myHarmonyAccessory) { myHarmonyAccessory = this.harmonyBase.createAccessory(this, name); accessoriesToAdd.push(myHarmonyAccessory); } if (this.receiverIcon) myHarmonyAccessory.category = Categories.AUDIO_RECEIVER; else myHarmonyAccessory.category = Categories.TELEVISION; this._confirmedAccessories.push(myHarmonyAccessory); this.log('(' + this.name + ')' + 'INFO - configuring Main TV Service'); this.configureMainService(myHarmonyAccessory); let mainActivityConfigured = false; let defaultActivity = undefined; //Pre-sort so the input sorces are set alphabetically or by activityOrder // "sort input list in TV accessory : 0-default,1:Alpha,2:activityOrder property of hub, 3:activitiesToPublishAsInputForTVMode order (defaults to 0).", this.log.debug('(' + this.name + ')' + 'INFO - accessories : Sort Order : ' + this.sortInput); if (this.sortInput == 1) activities.sort((a, b) => a.label.localeCompare(b.label)); else if (this.sortInput == 2) activities.sort((a, b) => a.activityOrder - b.activityOrder); else if (this.sortInput > 2 && this.activitiesToPublishAsInputForTVMode) { var refArray = this.activitiesToPublishAsInputForTVMode; activities.sort(function (a, b) { return refArray.indexOf(a.label) - refArray.indexOf(b.label); }); } for (let i = 0, len = activities.length; i < len; i++) { if (this.showInput(activities[i])) { let inputName = this.devMode ? 'DEV' + activities[i].label : activities[i].label; let inputId = activities[i].id; this.log.debug( '(' + this.name + ')' + 'INFO - accessories : activity to configure : ' + inputName ); if (this.mainActivity == inputName) { this.configureMainActivity(myHarmonyAccessory, activities[i]); mainActivityConfigured = true; } else if (defaultActivity == undefined) { defaultActivity = activities[i]; } let inputSourceService = this.configureInputSourceService( myHarmonyAccessory, inputName, inputId, activities[i], this.inputServices.length + 1 ); this.mainService.addLinkedService(inputSourceService); this.inputServices.push(inputSourceService); } } if (!mainActivityConfigured) { this.log( '(' + this.name + ')' + 'WARNING - No main Activity that match config file found, default to first one' ); if (defaultActivity == undefined) this.log( '(' + this.name + ')' + 'ERROR - No Activity at all was found for this TV accessory' ); else this.configureMainActivity(myHarmonyAccessory, defaultActivity); } //AccessControl if (this.configureAccesscontrol) this.configureAccessControlService(myHarmonyAccessory); this.bindCharacteristicEventsForInputs(myHarmonyAccessory); this.TVFoundAccessory = myHarmonyAccessory; return accessoriesToAdd; }, //TV METHODS configureAccessControlService: function (myHarmonyAccessory) { //// try { //acces control this.log('(' + this.name + ')' + 'INFO - configuring Access Control Service'); let subType = this.name + ' AccessControlService'; this.accessControlService = myHarmonyAccessory.getServiceById(this.name, subType); var accessControl; if (!this.accessControlService) { accessControl = new AccessControlManagement(true); this.accessControlService = accessControl.getService(); this.accessControlService.subtype = subType; this.accessControlService.displayName = this.name; myHarmonyAccessory.addService(this.accessControlService); } else { accessControl = new AccessControlManagement(true, this.accessControlService); } this._confirmedServices.push(this.accessControlService); accessControl.on(AccessControlEvent.ACCESS_LEVEL_UPDATED, (level) => { this.log( '(' + this.name + ')' + 'INFO - New access control level of ' + level + '/' + myHarmonyAccessory.displayName ); }); accessControl.on( AccessControlEvent.PASSWORD_SETTING_UPDATED, (password, passwordRequired) => { if (passwordRequired) { this.log( '(' + this.name + ')' + 'INFO - New access control - Required password is: ' + password + '/' + myHarmonyAccessory.displayName ); } else { this.log( '(' + this.name + ')' + 'INFO - access control - No password set! ' + '/' + myHarmonyAccessory.displayName ); } } ); } catch { this.log( '(' + this.name + ')' + 'ERROR - error adding control service, you should try to restart (possible cache problem because of previous bug) or you are not on correct homebridge version' + '/' + myHarmonyAccessory.displayName ); } }, configureMainService: function (accessory) { let subType = this.name + ' TV'; this.mainService = accessory.getServiceById(this.name, subType); if (!this.mainService) { this.log('(' + this.name + ')' + 'INFO - Creating TV Service'); this.mainService = new Service.Television(this.name, 'tvService' + this.name); this.mainService.subtype = subType; accessory.addService(this.mainService); } this._confirmedServices.push(this.mainService); if (this.savedNames && this.savedNames[0]) { mainServiceName = this.savedNames[0]; } else { mainServiceName = this.name; } this.mainService .setCharacteristic(Characteristic.ConfiguredName, mainServiceName) .setCharacteristic(Characteristic.Name, this.name) .setCharacteristic( Characteristic.SleepDiscoveryMode, Characteristic.SleepDiscoveryMode.ALWAYS_DISCOVERABLE ) .setCharacteristic(Characteristic.Active, false); this.bindCharacteristicEventsForTV(accessory); this.inputServices = []; this.log.debug( '(' + this.name + ')' + 'INFO - accessories : main activity name : ' + this.mainActivity ); }, configureMainActivity: function (accessory, activity) { let inputName = activity.label; if (this.devMode) { inputName = 'DEV' + inputName; } this.log('(' + this.name + ')' + 'INFO - Configuring Main Activity ' + inputName); this.mainActivityId = activity.id; this.mainService.activityName = inputName; this.mainService.activityId = activity.id; let subType = this.name + ' Volume'; this.tvSpeakerService = accessory.getServiceById(this.name, subType); if (!this.tvSpeakerService) { this.log('(' + this.name + ')' + 'INFO - Creating TV Speaker Service'); this.tvSpeakerService = new Service.TelevisionSpeaker(this.name, 'TVSpeaker' + this.name); this.tvSpeakerService.subtype = subType; accessory.addService(this.tvSpeakerService); } this._confirmedServices.push(this.tvSpeakerService); this.tvSpeakerService .setCharacteristic(Characteristic.Name, this.name) .setCharacteristic(Characteristic.Active, Characteristic.Active.ACTIVE) .setCharacteristic( Characteristic.VolumeControlType, Characteristic.VolumeControlType.ABSOLUTE ); this.bindCharacteristicEventsForSpeaker(this.tvSpeakerService); this.mainService.addLinkedService(this.tvSpeakerService); }, configureInputSourceService: function (accessory, inputName, inputId, activity, order) { let subType = inputName + ' Activity'; let inputSourceService = accessory.getServiceById(this.name, subType); if (!inputSourceService) { this.log( '(' + this.name + ')' + 'INFO - Creating Input Service - ' + inputName + ' in position ' + order ); inputSourceService = new Service.InputSource(this.name, 'Input' + this.name + inputName); inputSourceService.subtype = subType; accessory.addService(inputSourceService); } this._confirmedServices.push(inputSourceService); inputSourceService.activityName = inputName; inputSourceService.activityId = inputId; let controlGroup = activity.controlGroup; HarmonyAsTVKeysTools.mapKeys(this, controlGroup, inputName, inputSourceService); if (this.savedNames && this.savedNames[inputId]) { this.log( '(' + this.name + ')' + 'INFO - Input Service name saved - ' + this.savedNames[inputId] ); inputServiceName = this.savedNames[inputId]; } else { this.log('(' + this.name + ')' + 'INFO - Input Service name - ' + inputName); inputServiceName = inputName; } inputSourceService .setCharacteristic(Characteristic.Identifier, order) .setCharacteristic(Characteristic.Name, inputName) .setCharacteristic(Characteristic.ConfiguredName, inputServiceName) .setCharacteristic(Characteristic.InputSourceType, Characteristic.InputSourceType.APPLICATION) .setCharacteristic(Characteristic.IsConfigured, Characteristic.IsConfigured.CONFIGURED); return inputSourceService; }, showInput: function (activity) { if ( activity.id != -1 && this.activitiesToPublishAsInputForTVMode && !this.activitiesToPublishAsInputForTVMode.includes(activity.label) ) return false; else return activity.id != -1; }, ///REFRESHING TOOLS handleRefreshOfCharacteristic() { this.updateCurrentInputService(); this.harmonyBase.handleCharacteristicUpdate( this, this.mainService.getCharacteristic(Characteristic.Active), this._currentInputService !== undefined ); this.harmonyBase.handleCharacteristicUpdate( this, this.mainService.getCharacteristic(Characteristic.ActiveIdentifier), HarmonyTools.transformActivityIdToActiveIdentifier( this._currentInputService, this.inputServices ) ); }, localRefresh: function () { //TV if (this.TVAccessory) this.handleRefreshOfCharacteristic(); //SWITCH ACTIVITIEs if (this.isPlatformWithSwitch) { for (let a = 0; a < this._foundAccessories.length; a++) { let myHarmonyAccessory = this._foundAccessories[a]; for (let s = 0; s < myHarmonyAccessory.services.length; s++) { let service = myHarmonyAccessory.services[s]; if ( service.type == HarmonyConst.ACTIVITY_TYPE || service.type == HarmonyConst.GENERALVOLUME_TYPE || service.type == HarmonyConst.GENERALVOLUMEUP_TYPE || service.type == HarmonyConst.GENERALVOLUMEDOWN_TYPE ) this.refreshService(service, false); } } } }, refreshPlatform: function () { this.harmonyBase.refreshCurrentActivity(this, () => { this.harmonyBase.refreshHomeAccessory(this); }); }, updateCurrentInputService: function () { if (this._currentActivity > 0) { let inputFound = false; for (let i = 0, len = this.inputServices.length; i < len; i++) { if (this.inputServices[i].activityId == this._currentActivity) { this._currentInputService = this.inputServices[i]; inputFound = true; break; } } if (!inputFound) { this._currentInputService = undefined; } } else { this._currentInputService = undefined; } this.keysMap = HarmonyAsTVKeysTools.mapKeysForActivity(this); }, refreshCurrentActivityOnSubPlatform: function (response) { this._currentActivityLastUpdate = Date.now(); if (response === undefined) return; this._currentActivity = response; this.localRefresh(); }, refreshService: function (service, callback) { var characteristic = service.getCharacteristic(Characteristic.On); //return immediately if (callback) { callback(undefined, service.getCharacteristic(Characteristic.On).value); } this.harmonyBase.refreshCurrentActivity(this, () => { if (this._currentActivity > HarmonyConst.CURRENT_ACTIVITY_NOT_SET_VALUE) { let characteristicIsOn = this.checkOn(service); this.log.debug( '(' + this.name + ')' + 'Got status for ' + service.displayName + ' - was ' + characteristic.value + ' set to ' + characteristicIsOn ); this.harmonyBase.handleCharacteristicUpdate(this, characteristic, characteristicIsOn); } else { this.log.debug('(' + this.name + ')' + 'WARNING : no current Activity'); this.harmonyBase.handleCharacteristicUpdate(this, characteristic, characteristic.value); } }); }, ///COMANDS sendInputCommand: function (homebridgeAccessory, value) { let doCommand = true; let commandToSend = value; let inputName = commandToSend == -1 ? 'PowerOff' : ''; for (let i = 0, len = this.inputServices.length; i < len; i++) { if (this.inputServices[i].activityId == commandToSend) { inputName = this.inputServices[i].activityName; break; } } if (HarmonyTools.isActivtyToBeSkipped(this, inputName)) { //GLOBAL OFF SWITCH : do command only if we are not off if (commandToSend == -1) { doCommand = this._currentActivity > 0; } //ELSE, we do the command only if state is different. else { doCommand = this._currentActivity !== value; } } if (doCommand) { this.log.debug( '(' + this.name + ')' + 'INFO - sendInputCommand : Activty ' + inputName + ' will be activated ' ); } else { this.log.debug( '(' + this.name + ')' + 'INFO - sendInputCommand : Activty ' + inputName + ' will not be activated ' ); } if (doCommand) { this.activityCommand(homebridgeAccessory, commandToSend); } else { setTimeout(() => { this.refreshPlatform(); }, HarmonyConst.DELAY_TO_UPDATE_STATUS); } }, handlePlayPause: function () { this.log.debug( '(' + this.name + ')' + 'INFO - current play status is : ' + this.playStatus[this._currentActivity] + ' with playPause option set to :' + this.playPauseBehavior ); this.log.debug( '(' + this.name + ')' + 'INFO - pauseCommand defined for : ' + this._currentActivity + ' is ' + this._currentInputService.PauseCommand ); if ( !this.playPauseBehavior || this._currentInputService.PauseCommand === undefined || this.playStatus[this._currentActivity] === undefined || this.playStatus[this._currentActivity] === 'PAUSED' ) { this.log.debug('(' + this.name + ')' + 'INFO - sending PlayCommand for PLAY_PAUSE'); //this.harmonyBase.sendCommand(this, this.keysMap[Characteristic.RemoteKey.PLAY_PAUSE]); HarmonyTools.processCommands( this.harmonyBase, this, this.keysMap[Characteristic.RemoteKey.PLAY_PAUSE] ); this.playStatus[this._currentActivity] = ''; } else { this.log.debug('(' + this.name + ')' + 'INFO - sending PauseCommand for PLAY_PAUSE'); let overridePAUSE = HarmonyAsTVKeysTools.getOverrideCommand(this, 'PAUSE'); if (!overridePAUSE) { this.harmonyBase.sendCommand(this, this._currentInputService.PauseCommand); } else { HarmonyTools.processCommands(this.harmonyBase, this, overridePAUSE); } this.playStatus[this._currentActivity] = 'PAUSED'; } }, //HOMEKIT CHARACTERISTICS EVENTS refreshCharacteristic: function (characteristic, callback) { if (callback) { //return immediately callback(undefined, characteristic.value); } this.harmonyBase.refreshCurrentActivity(this, () => { if (this._currentActivity > HarmonyConst.CURRENT_ACTIVITY_NOT_SET_VALUE) { if (characteristic.UUID == Characteristic.Active.UUID) { this.log.debug( '(' + this.name + ')' + 'INFO - refreshCharacteristic : updating Characteristic.Active to ' + (this._currentInputService !== undefined) ); this.harmonyBase.handleCharacteristicUpdate( this, characteristic, this._currentInputService !== undefined ); } else if (characteristic.UUID == Characteristic.ActiveIdentifier.UUID) { this.log.debug( '(' + this.name + ')' + 'INFO - refreshCharacteristic : updating Characteristic.ActiveIdentifier to ' + HarmonyTools.transformActivityIdToActiveIdentifier( this._currentInputService, this.inputServices ) ); this.harmonyBase.handleCharacteristicUpdate( this, characteristic, HarmonyTools.transformActivityIdToActiveIdentifier( this._currentInputService, this.inputServices ) ); } } else { this.log.debug( '(' + this.name + ')' + 'WARNING - refreshCharacteristic : no current Activity' ); if (characteristic.UUID == Characteristic.Active.UUID) { this.harmonyBase.handleCharacteristicUpdate(this, characteristic, false); } else if (characteristic.UUID == Characteristic.ActiveIdentifier.UUID) { this.harmonyBase.handleCharacteristicUpdate(this, characteristic, 0); } } }); }, bindActiveCharacteristic(characteristic, service, homebridgeAccessory) { //set to main activity / activeIdentifier or off characteristic.on( 'set', function (value, callback) { this.log.debug('(' + this.name + ')' + 'INFO - SET Characteristic.Active ' + value); this.log.debug( '(' + this.name + ')' + 'INFO - value of Characteristic.ActiveIdentifier ' + service.getCharacteristic(Characteristic.ActiveIdentifier).value ); if (value == 0) { this.log.debug('(' + this.name + ')' + 'INFO - switching off'); this.sendInputCommand(homebridgeAccessory, '-1'); callback(null); } else { //we push back the execution to let the second event be taken care of in case of switching on with a dedicated input. setTimeout(() => { if (this._currentInputService == undefined) { var currentActivity = HarmonyTools.transformActiveIdentifierToActivityId( service.getCharacteristic(Characteristic.ActiveIdentifier).value, this.inputServices ); if (currentActivity <= 0) { this.log.debug( '(' + this.name + ')' + 'INFO - launching with default Activity - ' + this.mainActivityId ); this.sendInputCommand(homebridgeAccessory, '' + this.mainActivityId); } else { this.log.debug( '(' + this.name + ')' + 'INFO - not launching, an activeinput is set - ' + currentActivity ); } } else { this.log.debug( '(' + this.name + ')' + 'INFO - not launching, there is a current Activity defined -' + this.mainActivityId ); } }, HarmonyConst.DELAY_TO_UPDATE_STATUS); callback(null); } }.bind(this) ); characteristic.on( 'get', function (callback) { this.log.debug('(' + this.name + ')' + 'INFO - GET Characteristic.Active '); this.refreshCharacteristic(characteristic, callback); }.bind(this) ); }, bindActiveIdentifierCharacteristic: function (characteristic, homebridgeAccessory) { //set the current Activity characteristic.on( 'set', function (value, callback) { this.log.debug( '(' + this.name + ')' + 'INFO - SET Characteristic.ActiveIdentifier ' + value ); this.sendInputCommand( homebridgeAccessory, '' + HarmonyTools.transformActiveIdentifierToActivityId(value, this.inputServices) ); callback(null); }.bind(this) ); characteristic.on( 'get', function (callback) { this.log.debug('(' + this.name + ')' + 'INFO - GET Characteristic.ActiveIdentifier'); this.refreshCharacteristic(characteristic, callback); }.bind(this) ); }, bindRemoteKeyCharacteristic: function (characteristic) { characteristic.on( 'set', function (newValue, callback) { this.log.debug( '(' + this.name + ')' + 'INFO - SET Characteristic.RemoteKey : ' + newValue + ' with currentActivity ' + this._currentActivity ); if (this._currentActivity > 0) { if (newValue === Characteristic.RemoteKey.PLAY_PAUSE) { this.handlePlayPause(); } else if (this.keysMap[newValue]) { this.log.debug( '(' + this.name + ')' + 'INFO - sending command ' + this.keysMap[newValue] + 'for ' + newValue ); if (Array.isArray(this.keysMap[newValue])) HarmonyTools.processCommands(this.harmonyBase, this, this.keysMap[newValue]); else this.harmonyBase.sendCommand(this, this.keysMap[newValue]); } else { this.log.debug('(' + this.name + ')' + 'INFO - no command to send for ' + newValue); } } callback(null); }.bind(this) ); }, bindMuteCharacteristic(characteristic) { characteristic.on( 'set', function (value, callback) { if (this._currentInputService !== undefined) { this.log.debug('(' + this.name + ')' + 'INFO - SET Characteristic.Mute : ' + value); let overrideMUTE = HarmonyAsTVKeysTools.getOverrideCommand(this, 'MUTE'); if (!overrideMUTE) { this.harmonyBase.sendCommand(this, this._currentInputService.MuteCommand); } else { HarmonyTools.processCommands(this.harmonyBase, this, overrideMUTE); } } callback(null); }.bind(this) ); characteristic.on( 'get', function (callback) { this.log.debug('(' + this.name + ')' + 'INFO - GET Characteristic.Mute'); callback(null, false); }.bind(this) ); }, bindVolumeSelectorCharacteristic(characteristic) { characteristic.on( 'set', function (value, callback) { if (this._currentInputService !== undefined) { this.log.debug( '(' + this.name + ')' + 'INFO - SET Characteristic.VolumeSelector : ' + value ); if (value === Characteristic.VolumeSelector.DECREMENT) { let overrideVOLUMEDOWN = HarmonyAsTVKeysTools.getOverrideCommand(this, 'VOLUME_DOWN'); if (!overrideVOLUMEDOWN) { this.harmonyBase.sendCommand( this, this._currentInputService.VolumeDownCommand + '|' + this.numberOfCommandsSentForVolumeControl ); } else { HarmonyTools.processCommands(this.harmonyBase, this, overrideVOLUMEDOWN); } } else { let overrideVOLUMEUP = HarmonyAsTVKeysTools.getOverrideCommand(this, 'VOLUME_UP'); if (!overrideVOLUMEUP) { this.harmonyBase.sendCommand( this, this._currentInputService.VolumeUpCommand + '|' + this.numberOfCommandsSentForVolumeControl ); } else { HarmonyTools.processCommands(this.harmonyBase, this, overrideVOLUMEUP); } } } callback(null); }.bind(this) ); }, bindVolumeCharacteristic(characteristic) { characteristic.on( 'set', function (value, callback) { if (this._currentActivity > 0) { this.log.debug('(' + this.name + ')' + 'INFO - SET Characteristic.Volume : ' + value); this.volumesLevel[this._currentActivity] = value; } callback(null); }.bind(this) ); characteristic.on( 'get', function (callback) { this.log.debug('(' + this.name + ')' + 'INFO - GET Characteristic.Volume'); if (this.volumesLevel[this._currentActivity]) callback(null, this.volumesLevel[this._currentActivity]); else callback(null, HarmonyConst.DEFAULT_VOLUME); }.bind(this) ); }, bindConfiguredNameCharacteristic: function (characteristic, service) { characteristic.on( 'set', function (value, callback) { this.log.debug( '(' + this.name + ')' + 'INFO - SET Characteristic.ConfiguredName : ' + value ); var idConf = 0; if (service.UUID == Service.InputSource.UUID) idConf = service.activityId; this.savedNames[idConf] = value; fs.writeFile(this.savedNamesFile, JSON.stringify(this.savedNames), (err) => { if (err) { this.log( '(' + this.name + ')' + 'ERROR - error occured could not write configured name %s', err ); } else { this.log.debug( '(' + this.name + ')' + 'INFO - configured name successfully saved! New name: %s ID: %s', value, idConf ); } }); callback(null); }.bind(this) ); }, bindCurrentVisibilityStateCharacteristic: function (characteristic, service) { characteristic.on( 'get', function (callback) { let idConf = service.activityId; this.log.debug( '(' + this.name + ')' + 'INFO - GET Characteristic.CurrentVisibilityState : ' + (this.savedVisibility[idConf] ? this.savedVisibility[idConf] : 'DEFAULT - ' + Characteristic.TargetVisibilityState.SHOWN) ); if (this.savedVisibility[idConf]) callback(null, this.savedVisibility[idConf]); else callback(null, Characteristic.CurrentVisibilityState.SHOWN); }.bind(this) ); }, bindTargetVisibilityStateCharacteristic(characteristic, service) { characteristic.on( 'get', function (callback) { let idConf = service.activityId; this.log.debug( '(' + this.name + ')' + 'INFO - GET Characteristic.TargetVisibilityState : ' + (this.savedVisibility[idConf] ? this.savedVisibility[idConf] : 'DEFAULT - ' + Characteristic.TargetVisibilityState.SHOWN) ); if (this.savedVisibility[idConf]) callback(null, this.savedVisibility[idConf]); else callback(null, Characteristic.TargetVisibilityState.SHOWN); }.bind(this) ); characteristic.on( 'set', function (value, callback) { this.log.debug( '(' + this.name + ')' + 'INFO - SET Characteristic.TargetVisibilityState : ' + value ); let idConf = service.activityId; let oldValue = this.savedVisibility[idConf] ? this.savedVisibility[idConf] : Characteristic.CurrentVisibilityState.SHOWN; this.savedVisibility[idConf] = value; fs.writeFile(this.savedVisibilityFile, JSON.stringify(this.savedVisibility), (err) => { if (err) { this.savedVisibility[idConf] = oldValue; this.log( '(' + this.name + ')' + 'ERROR - error occured could not write visibility state %s', err ); } else { this.log.debug( '(' + this.name + ')' + 'INFO - configured visibility successfully saved! New visibility: %s ID: %s', value, idConf ); } service .getCharacteristic(Characteristic.CurrentVisibilityState) .updateValue(this.savedVisibility[idConf]); callback(null); }); }.bind(this) ); }, bindPowerModeSelectionCharacteristic(characteristic) { characteristic.on( 'set', function (value, callback) { if (this._currentInputService !== undefined) { this.log.debug( '(' + this.name + ')' + 'INFO - SET Characteristic.PowerModeSelection : ' + value ); let overrideSETUP = HarmonyAsTVKeysTools.getOverrideCommand(this, 'SETUP'); if (!overrideSETUP) { this.harmonyBase.sendCommand(this, this._currentInputService.SetupCommand); } else { HarmonyTools.processCommands(this.harmonyBase, this, overrideSETUP); } } callback(null); }.bind(this) ); }, bindCharacteristicEventsForTV: function (homebridgeAccessory) { this.bindActiveCharacteristic( this.mainService.getCharacteristic(Characteristic.Active), this.mainService, homebridgeAccessory ); this.bindActiveIdentifierCharacteristic( this.mainService.getCharacteristic(Characteristic.ActiveIdentifier), homebridgeAccessory ); this.bindRemoteKeyCharacteristic(this.mainService.getCharacteristic(Characteristic.RemoteKey)); this.bindPowerModeSelectionCharacteristic( this.mainService.getCharacteristic(Characteristic.PowerModeSelection) ); this.bindConfiguredNameCharacteristic( this.mainService.getCharacteristic(Characteristic.ConfiguredName), this.mainService ); }, bindCharacteristicEventsForSpeaker: function () { this.bindMuteCharacteristic(this.tvSpeakerService.getCharacteristic(Characteristic.Mute)); this.bindVolumeSelectorCharacteristic( this.tvSpeakerService.getCharacteristic(Characteristic.VolumeSelector) ); this.bindVolumeCharacteristic(this.tvSpeakerService.getCharacteristic(Characteristic.Volume)); }, bindCharacteristicEventsForInputs: function () { for (let i = 0, len = this.inputServices.length; i < len; i++) { this.bindConfiguredNameCharacteristic( this.inputServices[i].getCharacteristic(Characteristic.ConfiguredName), this.inputServices[i] ); this.bindCurrentVisibilityStateCharacteristic( this.inputServices[i].getCharacteristic(Characteristic.CurrentVisibilityState), this.inputServices[i] ); this.bindTargetVisibilityStateCharacteristic( this.inputServices[i].getCharacteristic(Characteristic.TargetVisibilityState), this.inputServices[i] ); } }, //SWITCHES METHODS showActivity: function (activity) { if ( activity.id != -1 && this.activitiesToPublishAsAccessoriesSwitch && !this.activitiesToPublishAsAccessoriesSwitch.includes(activity.label) ) return false; else return activity.id != -1 || this.showTurnOffActivity; }, checkOn(service) { this.log.debug( '(' + this.name + ')' + 'checkOn : ' + this._currentActivity + '/' + service.type + '/' + service.activityId + '/' + (this.showTurnOffActivity == 'inverted') + '/' + (this.showTurnOffActivity == 'stateless') ); if (service.type == HarmonyConst.GENERALVOLUME_TYPE) { return ( this._currentActivity > -1 && service.volumeDownCommands[this._currentActivity] !== undefined && service.volumeUpCommands[this._currentActivity] !== undefined ); } else if (service.type == HarmonyConst.GENERALVOLUMEUP_TYPE) { return ( this._currentActivity > -1 && service.volumeUpCommands[this._currentActivity] !== undefined ); } else if (service.type == HarmonyConst.GENERALVOLUMEDOWN_TYPE) { return ( this._currentActivity > -1 && service.volumeDownCommands[this._currentActivity] !== undefined ); } else if (service.activityId == -1) { if ( this._currentActivity == -1 && (this.showTurnOffActivity == 'inverted' || this.showTurnOffActivity == 'stateless') ) { return false; } if (this._currentActivity != -1 && this.showTurnOffActivity == 'inverted') { return true; } } return this._currentActivity == service.activityId; }, handleActivityOk: function (commandToSend) { this._currentSetAttemps = 0; this._currentActivity = commandToSend; this._currentActivityLastUpdate = Date.now(); for (let a = 0; a < this._foundAccessories.length; a++) { let foundHarmonyAccessory = this._foundAccessories[a]; for (let s = 0; s < foundHarmonyAccessory.services.length; s++) { let otherService = foundHarmonyAccessory.services[s]; if (otherService.type == HarmonyConst.ACTIVITY_TYPE) { let characteristic = otherService.getCharacteristic(Characteristic.On); HarmonyTools.disablePreviousActivity( this, characteristic, otherService, commandToSend, characteristic.value ); HarmonyTools.handleOffActivity(this, characteristic, otherService, commandToSend); } } } }, getService: function (homebridgeAccessory, idToFind) { var service; for (let a = 0; a < homebridgeAccessory.services.length; a++) { if (homebridgeAccessory.services[a].ActivityId == idToFind) { service = homebridgeAccessory.services[a]; this.log.debug('(' + this.name + ')' + 'INFO - ' + service.displayName + ' activated'); break; } } return service; }, handleActivityInProgress: function (homebridgeAccessory, commandToSend) { this._currentSetAttemps = this._currentSetAttemps + 1; //we try again with a delay of 1sec since an activity is in progress and we couldn't update the one. setTimeout(() => { if (this._currentSetAttemps < HarmonyConst.MAX_ATTEMPS_STATUS_UPDATE) { this.log.debug( '(' + this.name + ')' + 'INFO - activityCommand : RETRY to send command ' + commandToSend ); this.activityCommand(homebridgeAccessory, commandToSend); } else { this.log( '(' + this.name + ')' + 'ERROR - activityCommand : could not SET status, no more RETRY : ' + commandToSend ); this.refreshPlatform(); } }, HarmonyConst.DELAY_BETWEEN_ATTEMPS_STATUS_UPDATE); }, activityCommand: function (homebridgeAccessory, commandToSend) { this.harmonyBase.harmony .startActivity(commandToSend) .then((data) => { this.log.debug( '(' + this.name + ')' + 'INFO - activityCommand : Returned from hub ' + JSON.stringify(data) ); if (HarmonyTools.isCommandOk(data)) { this.handleActivityOk(commandToSend); if (this.TVAccessory) this.handleRefreshOfCharacteristic(); } else if (HarmonyTools.isCommandInProgress(data)) { this.log.debug( '(' + this.name + ')' + 'WARNING - activityCommand : could not SET status : ' + JSON.stringify(data) ); this.handleActivityInProgress(homebridgeAccessory, commandToSend); } else { this.log( '(' + this.name + ')' + 'ERROR - activityCommand : could not SET status, no data' ); } }) .catch((e) => { this.log('(' + this.name + ')' + 'ERROR - activityCommand : ' + e); }); }, setSwitchOnCharacteristic: function ( homebridgeAccessory, characteristic, service, value, callback ) { let doCommand = true; let commandToSend = value ? service.activityId : '-1'; let currentValue = characteristic.value; //Actitiy in skippedIfSameState if (HarmonyTools.isActivtyToBeSkipped(this, service.subtype)) { this.log.debug( '(' + this.name + ')' + 'INFO : SET on an activty in skippedIfsameState list ' + service.subtype ); this.log.debug( '(' + this.name + ')' + 'INFO : Activty ' + service.subtype + ' is ' + currentValue + ', wants to set to ' + value ); //GLOBAL OFF SWITCH : do command only if it is off and we want to set it on since on state can't be reversed //ELSE, we do the command only if state is different. doCommand = service.activityId == -1 ? (this.showTurnOffActivity == 'inverted' && currentValue && !value) || (this.showTurnOffActivity != 'inverted' && !currentValue && value) : currentValue !== value; } else { this.log.debug( '(' + this.name + ')' + 'INFO : SET on an activty not in skippedIfSameStateActivities list ' + service.subtype ); } if (doCommand) { this.log.debug( '(' + this.name + ')' + 'INFO : Activty ' + service.subtype + ' will be sent command ' + commandToSend ); this.activityCommand(homebridgeAccessory, commandToSend); callback(); } else { this.log.debug( '(' + this.name + ')' + 'INFO : Activty ' + service.subtype + ' will not be sent any command ' ); callback(); setTimeout(() => { characteristic.updateValue(currentValue); }, HarmonyConst.DELAY_TO_UPDATE_STATUS); } }, bindCharacteristicEventsForSwitch: function (homebridgeAccessory, service) { service .getCharacteristic(Characteristic.On) .on( 'set', function (value, callback) { this.setSwitchOnCharacteristic( homebridgeAccessory, service.getCharacteristic(Characteristic.On), service, value, callback ); }.bind(this) ) .on( 'get', function (callback) { this.refreshService(service, callback); }.bind(this) ); }, };