@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.
385 lines • 18.6 kB
JavaScript
import { PLATFORM_NAME, PLUGIN_NAME } from './settings.js';
import { TvAccessory } from './tvAccessory.js';
import { SmartThingsClient, RefreshTokenAuthenticator, BearerTokenAuthenticator, } from '@smartthings/core-sdk';
import { SwitchAccessory } from './switchAccessory.js';
import { SliderAccessory } from './sliderAccessory.js';
import { SoundbarAccessory } from './soundbarAccessory.js';
import path from 'path';
import fs from 'fs';
import axios from 'axios';
/**
* Class implements the configured Device to mac and ip address mappings.
*/
class DeviceMapping {
deviceId;
nameOverride;
macAddress;
ipAddress;
inputSources;
applications;
validateApplications;
infoKey;
category;
constructor(deviceId, nameOverride, macAddress, ipAddress, inputSources, applications, validateApplications, infoKey, category) {
this.deviceId = deviceId;
this.nameOverride = nameOverride;
this.macAddress = macAddress;
this.ipAddress = ipAddress;
this.inputSources = inputSources;
this.applications = applications;
this.validateApplications = validateApplications;
this.infoKey = infoKey;
this.category = category;
}
}
/**
* Class implements the plugin platform.
*/
export class SmartThingsPlatform {
log;
config;
api;
Service;
Characteristic;
configPath = process.env.UIX_CONFIG_PATH || path.join('./', 'config.json');
authData;
// this is used to track restored cached accessories
accessories = [];
constructor(log, config, api) {
this.log = log;
this.config = config;
this.api = api;
this.log.debug('Finished initializing platform: %s', this.config.name);
this.Service = this.api.hap.Service;
this.Characteristic = this.api.hap.Characteristic;
if (!this.validateConfig(config)) {
return;
}
this.api.on('didFinishLaunching', () => {
this.log.debug('Executed didFinishLaunching callback');
let deviceBlocklist = config.deviceBlocklist;
if (this.config.deviceBlacklist) {
deviceBlocklist = config.deviceBlacklist;
this.log.warn('Config property deviceBlacklist has been renamed to deviceBlocklist \
- adjust your configuration because deviceBlacklist will be removed in future versions');
}
let authenticator = undefined;
switch (this.config.tokenType) {
case 'oauth':
authenticator = new RefreshTokenAuthenticator(config.oauthRefreshToken, this);
break;
case 'pat':
default:
authenticator = new BearerTokenAuthenticator(config.token);
}
this.discoverDevices(authenticator, deviceBlocklist ?? [], config.deviceMappings ?? [], config.tvDeviceTypes ?? ['oic.d.tv', 'x.com.st.d.monitor'], config.soundbarDeviceTypes ?? ['oic.d.networkaudio']);
});
}
/**
* @inheritdoc
*/
async getRefreshData() {
const config = this.getPlatformConfig();
return {
refreshToken: this.authData?.refreshToken ?? config.oauthRefreshToken,
clientId: config.oauthClientId,
clientSecret: config.oauthClientSecret,
};
}
/**
* @inheritdoc
*/
async putAuthData(data) {
this.log.debug('Updating auth data: %s', JSON.stringify(data, null, 4));
this.authData = data;
const config = this.getPlatformConfig();
config.oauthRefreshToken = data.refreshToken;
this.savePlatformConfig(config);
}
/**
* @inheritdoc
*/
configureAccessory(accessory) {
this.log.info('Loading accessory from cache: %s', accessory.displayName);
// add the restored accessory to the accessories cache so we can track if it has already been registered
this.accessories.push(accessory);
}
/**
* Reads the platform configuration from the file system.
*
* @returns the platform configuration
*/
getPlatformConfig() {
try {
const readConfig = JSON.parse(fs.readFileSync(this.configPath, 'utf8'));
return readConfig.platforms.find((p) => p.platform === this.config.platform);
}
catch (error) {
console.error('Error when reading configuration:', error);
return null;
}
}
/**
* Writes the platform configuration passed in to the file system.
*
* @param platformConfig the new platform configuration
*/
savePlatformConfig(platformConfig) {
try {
const newConfig = JSON.parse(fs.readFileSync(this.configPath, 'utf8'));
const platform = newConfig.platforms.find((p) => p.platform === this.config.platform);
newConfig.platforms[newConfig.platforms.indexOf(platform)] = platformConfig;
fs.writeFileSync(this.configPath, JSON.stringify(newConfig, null, 4), 'utf8');
}
catch (error) {
console.error('Error when writing configuration:', error);
}
}
/**
* Uses the SmartThings API to discover and register the available devices.
*
* @param token the SmartThings API token
* @param deviceBlocklist the device ids to be ignored
* @param deviceMappings the array of configured DeviceMapping
* @param tvDeviceTypes the array of configured TV device types
* @param soundbarDeviceTypes the array of configured SoundBar device types
*/
async discoverDevices(authenticator, deviceBlocklist, deviceMappings, tvDeviceTypes, soundbarDeviceTypes) {
const client = new SmartThingsClient(authenticator);
let externalAccessories = [];
try {
const devices = await client.devices.list();
this.log.debug('SmartThings API returned %i devices', devices.length);
for (const device of devices) {
if (deviceBlocklist.includes(device.deviceId)) {
this.log.debug('Ignoring SmartThings device %s because it is on the blocklist', device.name ? device.name + ' (' + device.deviceId + ')' : device.deviceId);
}
else {
externalAccessories = externalAccessories.concat(await this.registerDevice(client, device, deviceMappings, tvDeviceTypes, soundbarDeviceTypes));
}
}
}
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.log.error('Error when getting devices: [%s] %s', statusCode, errorMessage);
}
this.log.debug('Publishing %s external accessories', externalAccessories.length);
this.api.publishExternalAccessories(PLUGIN_NAME, externalAccessories);
}
/**
* Registers a SmartThings Device for Homebridge.
*
* @param client the SmartThingsClient used to send API calls
* @param device the SmartThings Device
* @param deviceMappings the array of configured DeviceMapping
* @param tvDeviceTypes the array of configured TV device types
* @param soundbarDeviceTypes the array of configured SoundBar device types
* @returns the PlatformAccessory that must be published as external accessory or undefined
* if accessory must not be published as external accessory
*/
async registerDevice(client, device, deviceMappings, tvDeviceTypes, soundbarDeviceTypes) {
const deviceType = device.ocf?.ocfDeviceType;
if (!deviceType) {
this.log.error('Ignoring SmartThings device %s because it has no device type', device.name ? device.name + ' (' + device.deviceId + ')' : device.deviceId);
return [];
}
const deviceMapping = deviceMappings.find(mapping => mapping.deviceId === device.deviceId);
if (tvDeviceTypes.includes(deviceType)) {
return await this.registerTvDevice(client, device, deviceMapping);
}
else if (soundbarDeviceTypes.includes(deviceType)) {
return await this.registerSoundbarDevice(client, device, deviceMapping);
}
this.log.debug('Ignoring SmartThings device %s because device type %s is not in list of implemented/configured types (%s): %s', device.name ? device.name + ' (' + device.deviceId + ')' : device.deviceId, device.ocf?.ocfDeviceType, tvDeviceTypes.concat(soundbarDeviceTypes).join(', '), JSON.stringify(device, null, 2));
return [];
}
/**
* Registers a SmartThings TV Device for Homebridge.
*
* @param client the SmartThingsClient used to send API calls
* @param device the SmartThings Device
* @param accessory the cached PlatformAccessory or undefined if no cached PlatformAccessory exists
* @param deviceMappings the array of configured DeviceMapping
* @returns the PlatformAccessory that must be published as external accessory or undefined
* if device could not be registered
*/
async registerTvDevice(client, device, deviceMapping) {
this.log.info('Adding new TV accessory: %s', device.name ? device.name + ' (' + device.deviceId + ')' : device.deviceId);
this.log.debug('New TV accessory\'s properties: %s', JSON.stringify(device, null, 4));
const component = device.components?.at(0);
if (!component) {
this.log.info('Can\'t register TV accessory because (main) component does not exist');
return [];
}
let displayName = device.name ?? device.deviceId;
if (deviceMapping?.nameOverride) {
this.log.info('Overriding device default name \'%s\' with configured display name \'%s\'', device.name, deviceMapping.nameOverride);
displayName = deviceMapping.nameOverride;
}
const accessory = new this.api.platformAccessory(displayName, device.deviceId);
accessory.context.device = device;
accessory.category = deviceMapping?.category ?? 31 /* this.api.hap.Categories.TELEVISION */;
const tv = new TvAccessory(displayName, device, component, client, this.log, this, accessory, this.config.capabilityLogging ?? false, this.config.registerApplications ?? false, deviceMapping?.validateApplications ?? true, this.config.pollInterval ?? undefined, this.config.cyclicCallsLogging ?? false, deviceMapping?.macAddress, deviceMapping?.ipAddress, deviceMapping?.inputSources, deviceMapping?.applications, deviceMapping?.infoKey);
await tv.registerCapabilities();
if (this.config.registerPictureModes) {
const modes = await tv.getPictureModes();
if (modes) {
this.registerModeSwitches(client, device, component, false, modes);
}
}
if (this.config.registerSoundModes) {
const modes = await tv.getSoundModes();
if (modes) {
this.registerModeSwitches(client, device, component, false, modes);
}
}
if (this.config.registerInputSwitches) {
const sources = await tv.getInputSources();
if (sources) {
this.registerModeSwitches(client, device, component, true, sources);
}
}
if (this.config.registerVolumeSlider) {
if (tv.hasSpeakerService()) {
this.registerVolumeSlider(client, device, component);
}
else {
this.log.warn('Volume slider can not be registered because TV has no volume capabilities');
}
}
return [accessory];
}
/**
* Registers a SmartThings Soundbar Device for Homebridge.
*
* @param client the SmartThingsClient used to send API calls
* @param device the SmartThings Device
* @param accessory the cached PlatformAccessory or undefined if no cached PlatformAccessory exists
* @param deviceMappings the array of configured DeviceMapping
* @returns the PlatformAccessory that must be published as external accessory or undefined
* if device could not be registered
*/
async registerSoundbarDevice(client, device, deviceMapping) {
this.log.info('Adding new soundbar accessory: %s', device.name ? device.name + ' (' + device.deviceId + ')' : device.deviceId);
this.log.debug('New Soundbar accessory\'s properties: %s', JSON.stringify(device, null, 4));
const component = device.components?.at(0);
if (!component) {
this.log.info('Can\'t register soundbar accessory because (main) component does not exist');
return [];
}
let displayName = device.name ?? device.deviceId;
if (deviceMapping?.nameOverride) {
this.log.info('Overriding device default name \'%s\' with configured display name \'%s\'', device.name, deviceMapping.nameOverride);
displayName = deviceMapping.nameOverride;
}
const accessory = new this.api.platformAccessory(displayName, device.deviceId);
accessory.context.device = device;
accessory.category = deviceMapping?.category ?? 35 /* this.api.hap.Categories.TV_SET_TOP_BOX */;
const soundbar = new SoundbarAccessory(displayName, device, component, client, this.log, this, accessory, this.config.capabilityLogging ?? false, this.config.pollInterval ?? undefined, this.config.cyclicCallsLogging ?? false, deviceMapping?.macAddress, deviceMapping?.ipAddress, deviceMapping?.inputSources);
await soundbar.registerCapabilities();
if (this.config.registerVolumeSlider) {
this.registerVolumeSlider(client, device, component);
}
return [accessory];
}
/**
* Register the modes of the device passed in as platform accessories.
* Handles caching of accessories as well.
*
* @param client the SmartThingsClient used to send API calls
* @param device the SmartThings Device
* @param component the SmartThings Device's Component
* @param stateful flag if switch will be stateful (set to TRUE to get capability status and reflect changes in switch)
* @param modes the modes to register
*/
registerModeSwitches(client, device, component, stateful, modes) {
for (const mode of modes.values) {
const id = this.api.hap.uuid.generate(`${device.deviceId}${modes.prefix}${mode.id}`);
const name = `${modes.prefix} ${mode.name}`;
const existingAccessory = this.accessories.find(accessory => accessory.UUID === id);
if (existingAccessory) {
this.log.info('Restoring existing accessory from cache: %s', existingAccessory.displayName);
new SwitchAccessory(device, component, client, this.log, this, existingAccessory, modes.capability, modes.command, mode.value, stateful);
}
else {
const accessory = new this.api.platformAccessory(name, id);
accessory.context.device = device;
accessory.category = 8 /* this.api.hap.Categories.SWITCH */;
new SwitchAccessory(device, component, client, this.log, this, accessory, modes.capability, modes.command, mode.value, stateful);
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
}
}
}
/**
* Register the volume of the device passed in as platform accessory.
* Handles caching of accessories as well.
*
* @param client the SmartThingsClient used to send API calls
* @param device the SmartThings Device
* @param component the SmartThings Device's Component
*/
registerVolumeSlider(client, device, component) {
const id = this.api.hap.uuid.generate(`${device.deviceId}volume`);
const name = 'Volume';
const existingAccessory = this.accessories.find(accessory => accessory.UUID === id);
if (existingAccessory) {
this.log.info('Restoring existing accessory from cache: %s', existingAccessory.displayName);
new SliderAccessory(device, component, client, this.log, this, existingAccessory, 'audioVolume', 'setVolume', (value) => {
return value?.volume.value ?? 0;
}, (value) => {
return [value ?? 0];
}, this.config.pollInterval ?? undefined, this.config.cyclicCallsLogging ?? false);
}
else {
const accessory = new this.api.platformAccessory(name, id);
accessory.context.device = device;
accessory.category = 5 /* this.api.hap.Categories.LIGHTBULB */;
new SliderAccessory(device, component, client, this.log, this, accessory, 'audioVolume', 'setVolume', (value) => {
return value?.volume.value ?? 0;
}, (value) => {
return [value ?? 0];
}, this.config.pollInterval ?? undefined, this.config.cyclicCallsLogging ?? false);
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
}
}
/**
* Validates the config and returns validation result.
* Basically checks that authentication token setup is correct.
*
* @param config the PlatformConfig
* @returns TRUE in case config is valid - FALSE otherwise
*/
validateConfig(config) {
switch (this.config.tokenType) {
case 'oauth':
if (!config.oauthClientId) {
this.log.error('OAuth client id must be set for OAuth token type');
return false;
}
if (!config.oauthClientSecret) {
this.log.error('OAuth client secret must be set for OAuth token type');
return false;
}
if (!config.oauthRefreshToken) {
this.log.error('OAuth refresh token must be set for OAuth token type');
return false;
}
break;
case 'pat':
default:
if (!config.token) {
this.log.error('SmartThings personal access token must be set for PAT token type');
return false;
}
}
return true;
}
}
//# sourceMappingURL=smartThingsPlatform.js.map