homebridge-bravia-tvos-hv2
Version:
Custom version of Bravia plugin for Homebridge v2
608 lines (519 loc) • 22.7 kB
JavaScript
'use strict';
const logger = require('../utils/logger');
class Handler {
constructor(api, accessory, inputs, bravia) {
this.api = api;
this.accessory = accessory;
this.inputs = inputs;
this.polling = accessory.context.config.polling;
this.bravia = bravia;
this.warningMuted = false;
}
async getTelevisionState() {
try {
const response = await this.bravia.exec('avContent', 'getPlayingContentInfo');
logger.debug(response, this.accessory.displayName);
this.accessory
.getService(this.api.hap.Service.Television)
.getCharacteristic(this.api.hap.Characteristic.Active)
.updateValue(response.turnedOff ? 0 : 1);
this.accessory
.getService(this.api.hap.Service.Television)
.getCharacteristic(this.api.hap.Characteristic.CurrentMediaState)
.updateValue(response.turnedOff ? 2 : 0);
this.accessory
.getService(this.api.hap.Service.Television)
.getCharacteristic(this.api.hap.Characteristic.TargetMediaState)
.updateValue(response.turnedOff ? 2 : 0);
if (!response.turnedOff) {
const uri = response.result[0].uri;
let identifier = 0;
if (uri) {
const foundUri = this.inputs.findIndex((input) => input.uri === uri);
if (foundUri > -1) {
identifier = foundUri + 1;
}
}
this.accessory
.getService(this.api.hap.Service.Television)
.getCharacteristic(this.api.hap.Characteristic.ActiveIdentifier)
.updateValue(identifier);
}
} catch (err) {
this.handleError(this.accessory.displayName, err, 'An error occured during getting television state!');
}
}
async getSpeakerState() {
try {
let mute = true;
let volume = 0;
const target =
this.accessory.context.config.speaker.output === 'other'
? 'speaker'
: this.accessory.context.config.speaker.output;
const tvState = this.accessory
.getService(this.api.hap.Service.Television)
.getCharacteristic(this.api.hap.Characteristic.Active).value;
if (tvState) {
const response = await this.bravia.exec('audio', 'getVolumeInformation');
logger.debug(response, `${this.accessory.displayName} Speaker`);
const speakers = (response.result && response.result.length) ? response.result[0] : [];
const targetSpeaker = speakers.find((speaker) => speaker.target === target);
const availableTargets = speakers.map((speaker) => {
return speaker.target;
});
if (targetSpeaker) {
mute = targetSpeaker.mute;
volume = targetSpeaker.volume;
this.accessory.context.speakerMute = mute;
this.accessory.context.speakerVolume = volume;
} else {
logger.warn(
`Defined speaker output "${target}" not found! Can not update speaker state/volume!`,
`${this.accessory.displayName} Speaker`
);
logger.warn(
`Available speaker outputs: ${availableTargets.toString()}`,
`${this.accessory.displayName} Speaker`
);
}
}
this.changeSpeakerAccessory(mute, volume);
} catch (err) {
this.handleError(`${this.accessory.displayName} Speaker`, err, 'An error occured during getting speaker state!');
}
}
changeSpeakerAccessory(mute, volume) {
const speakerType = this.accessory.context.config.speaker.accType;
if (speakerType === 'switch') {
this.accessory
.getService(this.api.hap.Service.Switch)
.getCharacteristic(this.api.hap.Characteristic.On)
.updateValue(!mute);
} else if (speakerType === 'fan') {
this.accessory
.getService(this.api.hap.Service.Fanv2)
.getCharacteristic(this.api.hap.Characteristic.Active)
.updateValue(mute ? 0 : 1);
this.accessory
.getService(this.api.hap.Service.Fanv2)
.getCharacteristic(this.api.hap.Characteristic.RotationSpeed)
.updateValue(volume);
} else {
this.accessory
.getService(this.api.hap.Service.Lightbulb)
.getCharacteristic(this.api.hap.Characteristic.On)
.updateValue(!mute);
this.accessory
.getService(this.api.hap.Service.Lightbulb)
.getCharacteristic(this.api.hap.Characteristic.Brightness)
.updateValue(volume);
}
}
async setActive(state) {
try {
this.accessory.context.busy = true;
logger.info(state ? 'ON' : 'OFF', this.accessory.displayName);
if (state) {
await this.bravia.wake();
} else {
await this.bravia.exec('system', 'setPowerStatus', '1.0', {
status: false,
});
}
if (this.accessory.context.config.speaker.active) {
const mute = state ? this.accessory.context.speakerMute : true;
const volume = state ? this.accessory.context.speakerVolume : 0;
this.changeSpeakerAccessory(mute, volume);
}
} catch (err) {
this.handleError(this.accessory.displayName, err, 'An error occured during setting televion state!');
} finally {
this.accessory.context.busy = false;
}
}
async setActiveIdentifier(value) {
try {
this.accessory.context.busy = true;
const index = value - 1;
const input = this.inputs[index];
if (input.type === 'apps') {
//apps
logger.info(`Open: ${input.inputName}`, this.accessory.displayName);
logger.debug(input, this.accessory.displayName);
await this.bravia.exec('appControl', 'setActiveApp', '1.0', {
uri: input.uri,
});
//Reset ActiveIdentifier, because "Applications" does not support state control
setTimeout(
() =>
this.accessory
.getService(this.api.hap.Service.Television)
.getCharacteristic(this.api.hap.Characteristic.ActiveIdentifier)
.updateValue(0),
500
);
} else if (input.type === 'channels') {
//channels
logger.info(`Open: ${input.inputName}`, this.accessory.displayName);
logger.debug(input, this.accessory.displayName);
await this.bravia.exec('avContent', 'setPlayContent', '1.0', {
uri: input.uri,
});
} else if (input.type === 'commands') {
//commands
logger.info(`Send command: ${input.inputName} (${input.value})`, this.accessory.displayName);
logger.debug(input, this.accessory.displayName);
await this.bravia.execCommand(input.value);
//Reset ActiveIdentifier, because "Commands" does not support state control
setTimeout(
() =>
this.accessory
.getService(this.api.hap.Service.Television)
.getCharacteristic(this.api.hap.Characteristic.ActiveIdentifier)
.updateValue(0),
500
);
} else if (input.type === 'inputs') {
//inputs
logger.info(`Open: ${input.inputName}`, this.accessory.displayName);
logger.debug(input, this.accessory.displayName);
await this.bravia.exec('avContent', 'setPlayContent', '1.0', {
uri: input.uri,
});
} else if (input.type === 'macros') {
//macros
logger.info(
`Send Command(s): ${JSON.stringify(input.commands)} - Delay ${input.delay}ms`,
this.accessory.displayName
);
logger.debug(input, this.accessory.displayName);
/*
* Due to the fact that "macros" have a delay
* and with a high number of "commands" it can come to a long waiting time,
* the Promise is resolved immediately and the function is processed afterwards.
* Otherwise it can come to problems with HomeKit.
*/
setTimeout(async () => {
try {
await this.bravia.execCommand(input.commands, input.delay);
} catch (err) {
this.handleError(this.accessory.displayName, err, 'An error occured during setting active identifier!');
}
}, 1);
//Reset ActiveIdentifier, because "Macros" does not support state control
setTimeout(
() =>
this.accessory
.getService(this.api.hap.Service.Television)
.getCharacteristic(this.api.hap.Characteristic.ActiveIdentifier)
.updateValue(0),
500
);
} else {
//unknown
logger.warn(`Unknown identifier: ${value}`, this.accessory.displayName);
}
} catch (err) {
this.handleError(this.accessory.displayName, err, 'An error occured during setting active identifier');
} finally {
this.accessory.context.busy = false;
}
}
async setRemoteKey(state, target) {
try {
this.accessory.context.busy = true;
if (target) {
state = target;
}
switch (state) {
case this.api.hap.Characteristic.RemoteKey.REWIND:
logger.info('Rewind', this.accessory.displayName);
logger.debug(`Execute Command: ${this.accessory.context.config.remote.REWIND}`, this.accessory.displayName);
await this.bravia.execCommand(this.accessory.context.config.remote.REWIND);
break;
case this.api.hap.Characteristic.RemoteKey.FAST_FORWARD:
logger.info('Fast Forward', this.accessory.displayName);
logger.debug(
`Execute Command: ${this.accessory.context.config.remote.FAST_FORWARD}`,
this.accessory.displayName
);
await this.bravia.execCommand(this.accessory.context.config.remote.FAST_FORWARD);
break;
case this.api.hap.Characteristic.RemoteKey.NEXT_TRACK:
logger.info('Next Track', this.accessory.displayName);
logger.debug(
`Execute Command: ${this.accessory.context.config.remote.NEXT_TRACK}`,
this.accessory.displayName
);
await this.bravia.execCommand(this.accessory.context.config.remote.NEXT_TRACK);
break;
case this.api.hap.Characteristic.RemoteKey.PREVIOUS_TRACK:
logger.info('Previous Track', this.accessory.displayName);
logger.debug(
`Execute Command: ${this.accessory.context.config.remote.PREVIOUS_TRACK}`,
this.accessory.displayName
);
await this.bravia.execCommand(this.accessory.context.config.remote.PREVIOUS_TRACK);
break;
case this.api.hap.Characteristic.RemoteKey.ARROW_UP:
logger.info('Up', this.accessory.displayName);
logger.debug(`Execute Command: ${this.accessory.context.config.remote.ARROW_UP}`, this.accessory.displayName);
await this.bravia.execCommand(this.accessory.context.config.remote.ARROW_UP);
break;
case this.api.hap.Characteristic.RemoteKey.ARROW_DOWN:
logger.info('Down', this.accessory.displayName);
logger.debug(
`Execute Command: ${this.accessory.context.config.remote.ARROW_DOWN}`,
this.accessory.displayName
);
await this.bravia.execCommand(this.accessory.context.config.remote.ARROW_DOWN);
break;
case this.api.hap.Characteristic.RemoteKey.ARROW_LEFT:
logger.info('Left', this.accessory.displayName);
logger.debug(
`Execute Command: ${this.accessory.context.config.remote.ARROW_LEFT}`,
this.accessory.displayName
);
await this.bravia.execCommand(this.accessory.context.config.remote.ARROW_LEFT);
break;
case this.api.hap.Characteristic.RemoteKey.ARROW_RIGHT:
logger.info('Right', this.accessory.displayName);
logger.debug(
`Execute Command: ${this.accessory.context.config.remote.ARROW_RIGHT}`,
this.accessory.displayName
);
await this.bravia.execCommand(this.accessory.context.config.remote.ARROW_RIGHT);
break;
case this.api.hap.Characteristic.RemoteKey.SELECT:
logger.info('Select', this.accessory.displayName);
logger.debug(`Execute Command: ${this.accessory.context.config.remote.SELECT}`, this.accessory.displayName);
await this.bravia.execCommand(this.accessory.context.config.remote.SELECT);
break;
case this.api.hap.Characteristic.RemoteKey.BACK:
logger.info('Back', this.accessory.displayName);
logger.debug(`Execute Command: ${this.accessory.context.config.remote.BACK}`, this.accessory.displayName);
await this.bravia.execCommand(this.accessory.context.config.remote.BACK);
break;
case this.api.hap.Characteristic.RemoteKey.EXIT:
logger.info('Exit', this.accessory.displayName);
logger.debug(`Execute Command: ${this.accessory.context.config.remote.EXIT}`, this.accessory.displayName);
await this.bravia.execCommand(this.accessory.context.config.remote.EXIT);
break;
case this.api.hap.Characteristic.RemoteKey.PLAY_PAUSE:
if (this.pause) {
this.pause = false;
logger.info('Pause', this.accessory.displayName);
logger.debug(`Execute Command: ${this.accessory.context.config.remote.PAUSE}`, this.accessory.displayName);
await this.bravia.execCommand(this.accessory.context.config.remote.PAUSE);
} else {
this.pause = true;
logger.info('Play', this.accessory.displayName);
logger.debug(`Execute Command: ${this.accessory.context.config.remote.PLAY}`, this.accessory.displayName);
await this.bravia.execCommand(this.accessory.context.config.remote.PLAY);
}
break;
case this.api.hap.Characteristic.RemoteKey.INFORMATION:
logger.info('Information', this.accessory.displayName);
logger.debug(
`Execute Command: ${this.accessory.context.config.remote.INFORMATION}`,
this.accessory.displayName
);
await this.bravia.execCommand(this.accessory.context.config.remote.INFORMATION);
break;
case 'MEDIA': {
const targetState = state === 0 ? 'Stop' : state === 1 ? 'Pause' : 'Play';
const command =
state === 0
? this.accessory.context.config.remote.STOP
: state === 1
? this.accessory.context.config.remote.PAUSE
: this.accessory.context.config.remote.PLAY;
logger.info(`Media state: ${targetState}`, this.accessory.displayName);
const tvState = this.accessory
.getService(this.api.hap.Service.Television)
.getCharacteristic(this.api.hap.Characteristic.Active).value;
if (!tvState) {
logger.warn('Can not change media state, Television is not active!', this.accessory.displayName);
return;
}
logger.debug(`Execute Command: ${command}`, this.accessory.displayName);
await this.bravia.execCommand(command);
break;
}
case 'SETTINGS': {
logger.info('TV Settings', this.accessory.displayName);
const tvState = this.accessory
.getService(this.api.hap.Service.Television)
.getCharacteristic(this.api.hap.Characteristic.Active).value;
if (!tvState) {
logger.warn('Can not open TV Settings, Television is not active!', this.accessory.displayName);
return;
}
logger.debug(`Execute Command: ${this.accessory.context.config.remote.SETTINGS}`, this.accessory.displayName);
await this.bravia.execCommand(this.accessory.context.config.remote.SETTINGS);
break;
}
default:
logger.warn(`Unknown remote key: ${state}`, this.accessory.displayName);
break;
}
} catch (err) {
this.handleError(this.accessory.displayName, err, 'An error occured during setting remote key!');
} finally {
this.accessory.context.busy = false;
}
}
async setMute(state, reverse) {
try {
const tvState = this.accessory
.getService(this.api.hap.Service.Television)
.getCharacteristic(this.api.hap.Characteristic.Active).value;
if (!tvState) {
logger.warn('Can not change Mute, Television is not active!', `${this.accessory.displayName} Speaker`);
return;
}
if (reverse) {
state = !state;
}
this.accessory.context.busy = true;
logger.info(`Mute ${state ? 'ON' : 'OFF'}`, `${this.accessory.displayName} Speaker`);
await this.bravia.exec('audio', 'setAudioMute', '1.0', { status: state });
this.accessory.context.speakerMute = state;
} catch (err) {
this.handleError(`${this.accessory.displayName} Speaker`, err, 'An error occured during setting speaker state!');
} finally {
this.accessory.context.busy = false;
}
}
async setVolume(state) {
try {
const tvState = this.accessory
.getService(this.api.hap.Service.Television)
.getCharacteristic(this.api.hap.Characteristic.Active).value;
if (!tvState) {
logger.warn('Can not change Volume, Television is not active!', `${this.accessory.displayName} Speaker`);
return;
}
const target =
this.accessory.context.config.speaker.output === 'other' ? '' : this.accessory.context.config.speaker.output;
logger.info(
`Volume: ${state} (${this.accessory.context.config.speaker.output})`,
`${this.accessory.displayName} Speaker`
);
this.accessory.context.busy = true;
await this.bravia.exec('audio', 'setAudioVolume', '1.0', {
target: target,
volume: state.toString(),
});
this.accessory.context.speakerVolume = state;
} catch (err) {
this.handleError(`${this.accessory.displayName} Speaker`, err, 'An error occured during setting speaker volume!');
} finally {
this.accessory.context.busy = false;
}
}
async setVolumeSelector(state) {
try {
const tvState = this.accessory
.getService(this.api.hap.Service.Television)
.getCharacteristic(this.api.hap.Characteristic.Active).value;
if (!tvState) {
logger.warn(
'Can not change Volume (selector), Television is not active!',
`${this.accessory.displayName} Speaker`
);
return;
}
// Volume Control through IRCC
const target =
this.accessory.context.config.speaker.output === 'other'
? 'speaker'
: this.accessory.context.config.speaker.output;
const volumeLevel = state
? this.accessory.context.config.speaker.reduceBy
: this.accessory.context.config.speaker.increaseBy;
const volumeCommand = state
? this.accessory.context.config.remote.VOLUME_DOWN
: this.accessory.context.config.remote.VOLUME_UP;
const irccCommands = [];
for (let i = 0; i < volumeLevel; i++) {
irccCommands.push(volumeCommand);
}
logger.info(
`${state ? 'Reducing' : 'Increasing'} volume by ${volumeLevel} (${target})`,
`${this.accessory.displayName} Speaker`
);
logger.debug(`Execute Command: ${volumeCommand}`, `${this.accessory.displayName} Speaker`);
this.accessory.context.busy = true;
await this.bravia.execCommand(irccCommands);
/*
//Volume control through API
const target =
this.accessory.context.config.speaker.output === 'other'
? 'speaker'
: this.accessory.context.config.speaker.output;
const volumeLevel = state
? `-${this.accessory.context.config.speaker.reduceBy}`
: `+${this.accessory.context.config.speaker.increaseBy}`;
this.accessory.context.busy = true;
await this.bravia.exec('audio', 'setAudioVolume', '1.0', {
target: '',
volume: volumeLevel,
});
*/
} catch (err) {
this.handleError(
`${this.accessory.displayName} Speaker`,
err,
'An error occured during setting speaker volume (selector)!'
);
} finally {
this.accessory.context.busy = false;
}
}
async poll() {
if (!this.accessory.context.busy) {
logger.debug('Polling API...', this.accessory.displayName);
await this.getTelevisionState();
if (this.accessory.context.config.speaker.active) {
await this.getSpeakerState();
}
} else {
logger.debug('Skip polling API, Television is busy', this.accessory.displayName);
}
setTimeout(() => this.poll(), this.polling * 1000);
}
handleError(accessoryName, err, message) {
//err.code = APIERROR means that protocol/service/method could not be found
if (err && err.code === 'APIERROR') {
logger.debug(err.message, accessoryName);
} else {
if (err.title === 'No Response' || err.code === 'EHOSTUNREACH') {
if (this.accessory.context.config.oldModel) {
logger.debug('Old tv model - Warnings are ignored.');
} else {
const msg1 = 'API not reachable - It seems the TV can not handle API calls if its not turned on.';
const msg2 =
'To continue using the plugin without errors, please enable "wol" in your config and enter the MAC address of the TV.';
const msg3 =
'This warning is displayed only once. To mute the warnings, please enable also "oldModel" in your config.json';
if (this.warningMuted) {
logger.debug(msg1);
logger.debug(msg2);
logger.debug(msg3);
} else {
logger.warn(msg1);
logger.warn(msg2);
logger.warn(msg3);
}
this.warningMuted = true;
}
} else {
logger.warn(message, accessoryName);
logger.error(err, accessoryName);
}
}
}
}
module.exports = Handler;