@o-lukas/homebridge-smartthings-tv
Version:
This is a plugin for Homebridge. It offers some basic functions to control Samsung TVs using the SmartThings API.
371 lines • 17 kB
JavaScript
import { wake } from 'wol';
import ping from 'ping';
import { SmartThingsAccessory } from './smartThingsAccessory.js';
import axios from 'axios';
/**
* Class implements a SmartThings soundbar accessory.
*/
export class SoundbarAccessory extends SmartThingsAccessory {
logCapabilities;
pollingInterval;
cyclicCallsLogging;
macAddress;
ipAddress;
inputSources;
service;
speakerService;
inputSourceServices = [];
capabilities = [];
activeIdentifierChangeTime = 0;
activeIdentifierChangeValue = 0;
constructor(name, device, component, client, log, platform, accessory, logCapabilities, pollingInterval, cyclicCallsLogging, macAddress = undefined, ipAddress = undefined, inputSources = undefined) {
super(device, component, client, platform, accessory, log);
this.logCapabilities = logCapabilities;
this.pollingInterval = pollingInterval;
this.cyclicCallsLogging = cyclicCallsLogging;
this.macAddress = macAddress;
this.ipAddress = ipAddress;
this.inputSources = inputSources;
this.accessory.getService(this.platform.Service.AccessoryInformation)
.setCharacteristic(this.platform.Characteristic.Name, name);
this.service = this.accessory.getService(this.platform.Service.Television)
?? this.accessory.addService(this.platform.Service.Television);
this.service
.setCharacteristic(this.platform.Characteristic.SleepDiscoveryMode, this.platform.Characteristic.SleepDiscoveryMode.ALWAYS_DISCOVERABLE)
.setCharacteristic(this.platform.Characteristic.ConfiguredName, name);
this.service.getCharacteristic(this.platform.Characteristic.RemoteKey)
.onSet(this.setRemoteKey.bind(this));
this.speakerService = this.accessory.getService(this.platform.Service.TelevisionSpeaker)
?? this.accessory.addService(this.platform.Service.TelevisionSpeaker);
this.service.addLinkedService(this.speakerService);
}
/**
* Registers all available capabilities of the SmartThings Component.
*/
async registerCapabilities() {
this.logInfo('Registering capabilities for component %s', this.component.id);
for (const reference of this.component.capabilities) {
try {
await this.registerCapability(await this.client.capabilities.get(reference.id, reference.version ?? 0));
}
catch (error) {
let errorMessage = 'unknown';
if (error instanceof Error) {
errorMessage = error.message;
}
let statusCode = -1;
if (axios.isAxiosError(error)) {
statusCode = error.response?.status ?? -1;
}
this.logError('Registering capability \'%s\' failed: [%s] %s', reference.id, statusCode, errorMessage);
}
}
if (this.capabilities.find(s => s === 'samsungvd.audioInputSource') &&
!this.capabilities.find(s => s === 'mediaInputSource')) {
this.logWarn('Capability %s might not work as expected because capability %s is missing which is needed to set input sources', 'samsungvd.audioInputSource', 'mediaInputSource');
}
}
/**
* Registers the SmartThings Capablity if it's functionality is implemented.
*
* @param capability the Capability
*/
async registerCapability(capability) {
let inputSourcePollingStarted = false;
if (this.logCapabilities) {
this.logDebug('Available capability: %s', JSON.stringify(capability, null, 2));
}
if (capability.id && !this.capabilities.includes(capability.id)) {
this.capabilities.push(capability.id);
}
switch (capability.id) {
case 'switch':
this.logCapabilityRegistration(capability);
this.service.getCharacteristic(this.platform.Characteristic.Active)
.onSet(this.setActive.bind(this))
.onGet(this.getActive.bind(this));
this.startStatusPolling(capability.name, this.service, this.platform.Characteristic.Active, this.getActive.bind(this, this.cyclicCallsLogging), this.pollingInterval);
break;
case 'audioVolume':
this.logCapabilityRegistration(capability);
this.speakerService
.setCharacteristic(this.platform.Characteristic.Active, this.platform.Characteristic.Active.ACTIVE);
this.speakerService
.setCharacteristic(this.platform.Characteristic.VolumeControlType, this.platform.Characteristic.VolumeControlType.ABSOLUTE);
this.speakerService
.getCharacteristic(this.platform.Characteristic.Volume)
.onGet(this.getVolume.bind(this))
.onSet(this.setVolume.bind(this));
this.speakerService.getCharacteristic(this.platform.Characteristic.VolumeSelector)
.onSet(this.setVolumeSelector.bind(this));
this.startStatusPolling(capability.name, this.speakerService, this.platform.Characteristic.Volume, this.getVolume.bind(this, this.cyclicCallsLogging), this.pollingInterval);
break;
case 'audioMute':
this.logCapabilityRegistration(capability);
this.speakerService.getCharacteristic(this.platform.Characteristic.Mute)
.onSet(this.setMute.bind(this))
.onGet(this.getMute.bind(this));
this.startStatusPolling(capability.name, this.speakerService, this.platform.Characteristic.Mute, this.getMute.bind(this, this.cyclicCallsLogging), this.pollingInterval);
break;
case 'samsungvd.audioInputSource':
this.logCapabilityRegistration(capability);
await this.registerAvailableInputSources();
if (this.inputSourceServices.length > 0) {
this.service.getCharacteristic(this.platform.Characteristic.ActiveIdentifier)
.onSet(this.setActiveIdentifier.bind(this))
.onGet(this.getActiveIdentifier.bind(this));
if (!inputSourcePollingStarted) {
inputSourcePollingStarted = true;
this.startStatusPolling('activeIdentifier', this.service, this.platform.Characteristic.ActiveIdentifier, this.getActiveIdentifier.bind(this, this.cyclicCallsLogging), this.pollingInterval);
}
}
break;
}
}
/**
* Setter for Homebridge accessory Active property.
*
* @param value the CharacteristicValue
*/
async setActive(value) {
this.logDebug('Set active to: %s', value);
if (value) {
if (this.macAddress) {
this.logDebug('Use wake-on-lan functionality because mac-address has been configured');
if (await wake(this.macAddress)) {
this.logDebug('Successfully woke device');
}
else {
this.logError('Could not wake device - if this error keeps occuring try to disable wake-on-lan functionality');
}
}
else {
await this.executeCommand('switch', 'on');
}
}
else {
await this.executeCommand('switch', 'off');
}
}
/**
* Getter for Homebridge accessory Active property.
*
* @param log flag to turn logging on/off
* @returns the CharacteristicValue
*/
async getActive(log = true) {
if (this.ipAddress) {
try {
const status = await ping.promise.probe(this.ipAddress);
if (log) {
this.logDebug('ping status: %s', status);
}
return status?.alive;
}
catch (exc) {
this.logError('error when pinging device: %s\n\
ping command fails mostly because of permission issues - falling back to SmartThings API for getting active state', exc);
return false;
}
}
const status = await this.getCapabilityStatus('switch', log);
return status?.switch.value === 'on' ? true : false;
}
/**
* Setter for Homebridge accessory VolumeSelector property.
*
* @param value the CharacteristicValue
*/
async setVolumeSelector(value) {
const increment = value === this.platform.Characteristic.VolumeSelector.INCREMENT;
this.logDebug('%s volume', increment ? 'Increasing' : 'Decreasing');
await this.executeCommand('audioVolume', increment ? 'volumeUp' : 'volumeDown');
}
/**
* Setter for Homebridge accessory Volume property.
*
* @param value the CharacteristicValue
*/
async setVolume(value) {
this.logDebug('Set volume to: %s', value);
await this.executeCommand('audioVolume', 'setVolume', [value ?? 0]);
}
/**
* Getter for Homebridge accessory Volume property.
*
* @param log flag to turn logging on/off
* @returns the CharacteristicValue
*/
async getVolume(log = true) {
const status = await this.getCapabilityStatus('audioVolume', log);
return status?.volume.value ?? 0;
}
/**
* Setter for Homebridge accessory Mute property.
*
* @param value the CharacteristicValue
*/
async setMute(value) {
this.logDebug('Set mute to: %s', value);
await this.executeCommand('audioMute', value ? 'mute' : 'unmute');
}
/**
* Getter for Homebridge accessory Mute property.
*
* @param log flag to turn logging on/off
* @returns the CharacteristicValue
*/
async getMute(log = true) {
const status = await this.getCapabilityStatus('audioMute', log);
return status?.mute.value === 'muted' ? true : false;
}
/**
* Setter for Homebridge accessory ActiveIdentifier property.
*
* @param value the CharacteristicValue
*/
async setActiveIdentifier(value) {
this.logDebug('Set active identifier to: %s', value);
const inputSource = this.inputSourceServices[value];
this.activeIdentifierChangeTime = Date.now();
this.activeIdentifierChangeValue = value;
// HACK: because samsungvd.audioInputSource does not support setting input source use
// mediaInputSource samsungvd.audioInputSource to get list of supported sources
await this.executeCommand('mediaInputSource', 'setInputSource', [inputSource.name ?? '']);
}
/**
* Getter for Homebridge accessory ActiveIdentifier property.
*
* @param log flag to turn logging on/off
* @returns the CharacteristicValue
*/
async getActiveIdentifier(log = true) {
const status = await this.getCapabilityStatus('samsungvd.audioInputSource', log);
if (Date.parse(status?.inputSource.timestamp ?? '') > this.activeIdentifierChangeTime) {
const id = this.inputSourceServices.findIndex(inputSource => inputSource.name === status?.inputSource.value);
if (log) {
this.logDebug('ActiveIdentifier has been changed on the device - using API result: %s', id);
}
if (id < 0) {
this.logWarn('Could not find input source for name \'%s\' - using first input source \'%s\' as active identifier', status?.inputSource.value, this.inputSourceServices[0].name);
return 0;
}
return id;
}
else {
if (log) {
this.logDebug('ActiveIdentifier has not been changed on the device - using temporary result: %s', this.activeIdentifierChangeValue);
}
return this.activeIdentifierChangeValue;
}
}
/**
* Setter for Homebridge accessory RemoteKey property.
*
* @param value the CharacteristicValue
*/
async setRemoteKey(value) {
switch (value) {
case this.platform.Characteristic.RemoteKey.REWIND:
if (this.validateRemoteKeyCapability('mediaPlayback', 'REWIND')) {
await this.executeCommand('mediaPlayback', 'rewind');
}
break;
case this.platform.Characteristic.RemoteKey.FAST_FORWARD:
if (this.validateRemoteKeyCapability('mediaPlayback', 'FAST_FORWARD')) {
await this.executeCommand('mediaPlayback', 'fastForward');
}
break;
case this.platform.Characteristic.RemoteKey.PLAY_PAUSE:
if (this.validateRemoteKeyCapability('mediaPlayback', 'PLAY_PAUSE')) {
await this.executeCommand('mediaPlayback', 'play');
}
break;
case this.platform.Characteristic.RemoteKey.EXIT:
if (this.validateRemoteKeyCapability('mediaPlayback', 'EXIT')) {
await this.executeCommand('mediaPlayback', 'stop');
}
break;
case this.platform.Characteristic.RemoteKey.BACK:
if (this.validateRemoteKeyCapability('mediaPlayback', 'BACK')) {
await this.executeCommand('mediaPlayback', 'stop');
}
break;
}
}
/**
* Validates that the SmartThings Capability needed to execute the remote key is available.
*
* @param capabilityId the identifier of the SmartThings Capablity
* @param remoteKey the remote key
* @returns TRUE in case capability is available - FALSE otherwise
*/
validateRemoteKeyCapability(capabilityId, remoteKey) {
if (this.capabilities.includes(capabilityId)) {
return true;
}
else {
this.logError('can\'t handle RemoteKey %s because %s capability is not available', remoteKey, capabilityId);
return false;
}
}
/**
* Registers all available media input sources (e.g. HDMI inputs).
*/
async registerAvailableInputSources() {
const status = await this.client.devices.getCapabilityStatus(this.device.deviceId, this.component.id, 'samsungvd.audioInputSource');
const supportedInputSources = [...new Set(status.supportedInputSources.value)].map((s) => {
return {
name: s,
id: s,
};
});
if (this.inputSources) {
this.logInfo('Overriding default input sources map "%s" with custom map "%s"', JSON.stringify(supportedInputSources, null, 2), JSON.stringify(this.inputSources, null, 2));
}
for (const inputSource of this.inputSources ?? supportedInputSources) {
this.registerInputSource(inputSource.id, inputSource.name);
}
}
/**
* Registers a Homebridge input source.
*
* @param id the input source id
* @param name the input source display name
* @param inputSource the InputSourceType or @code undefined @endcode to use @link guessInputSourceType @endlink
* to determine InputSourceType
*/
registerInputSource(id, name, inputSource = undefined) {
this.logInfo('Registering input source: %s (%s)', name, id);
let inputSourceType = inputSource;
if (inputSourceType === undefined) {
inputSourceType = this.guessInputSourceType(id);
this.logDebug('Guessed input source type for %s is: %i', name, inputSourceType);
}
const inputSourceService = this.accessory.getService(id)
?? this.accessory.addService(this.platform.Service.InputSource, id, id);
inputSourceService.name = id;
inputSourceService
.setCharacteristic(this.platform.Characteristic.Identifier, this.inputSourceServices.length)
.setCharacteristic(this.platform.Characteristic.ConfiguredName, name)
.setCharacteristic(this.platform.Characteristic.IsConfigured, this.platform.Characteristic.IsConfigured.CONFIGURED)
.setCharacteristic(this.platform.Characteristic.InputSourceType, inputSourceType);
this.service.addLinkedService(inputSourceService);
this.inputSourceServices.push(inputSourceService);
}
/**
* Guesses the InputSourceType from the identifier of the input source.
*
* @param inputSourceId the identifier of the input source
* @returns the InputSourceType (HDMI|TUNER|OTHER)
*/
guessInputSourceType(inputSourceId) {
if (inputSourceId.startsWith('HDMI')) {
return this.platform.Characteristic.InputSourceType.HDMI;
}
else {
return this.platform.Characteristic.InputSourceType.OTHER;
}
}
}
//# sourceMappingURL=soundbarAccessory.js.map