homebridge-lgwebos-tv
Version:
Homebridge plugin to control LG webOS TV.
857 lines (806 loc) • 51.8 kB
JavaScript
import WebSocket from 'ws';
import EventEmitter from 'events';
import ImpulseGenerator from './impulsegenerator.js';
import Functions from './functions.js';
import { ApiUrls, PairingOld, PairingNew } from './constants.js';
class LgWebOsSocket extends EventEmitter {
constructor(config, keyFile, devInfoFile, inputsFile, channelsFile, restFulEnabled, mqttEnabled) {
super();
this.host = config.host;
this.getInputsFromDevice = config.inputs?.getFromDevice || false;
this.inputs = (config.inputs?.data || []).filter(input => input.name && input.reference);
this.logWarn = config.log?.warn || true;
this.logError = config.log?.error || true;
this.logDebug = config.log?.debug || false;
this.sslWebSocket = config.sslWebSocket || false;
this.keyFile = keyFile;
this.devInfoFile = devInfoFile;
this.inputsFile = inputsFile;
this.channelsFile = channelsFile;
this.webSocketPort = this.sslWebSocket ? 3001 : 3000;
this.restFulEnabled = restFulEnabled;
this.mqttEnabled = mqttEnabled;
this.functions = new Functions();
this.socket = null;
this.specializedSocket = null;
this.socketOpen = false;
this.heartbeat = null;
this.externalInputsArr = [];
this.inputsArr = [];
this.socketConnected = false;
this.specializedSocketConnected = false;
this.cidCount = 0;
this.power = false;
this.screenState = 'Suspend';
this.appId = '';
this.channelId = '';
this.appType = '';
this.volume = 0;
this.mute = false;
this.playState = false;
this.brightness = 0;
this.backlight = 0;
this.contrast = 0;
this.color = 0;
this.pictureMode = '';
this.soundMode = '';
this.soundOutput = '';
this.tvInfo = {
manufacturer: 'LG Electronics',
modelName: 'LG TV',
productName: '',
deviceId: '',
firmwareRevision: '',
webOS: 2.0
};
//create impulse generator
this.impulseGenerator = new ImpulseGenerator()
.on('heartBeat', async () => {
try {
await this.connect();
} catch (error) {
if (this.logError) this.emit('error', `Impulse generator error: ${error}, trying again`);
}
})
.on('state', (state) => {
this.emit(state ? 'success' : 'warn', `Heartbeat ${state ? 'started' : 'stopped'}`);
});
}
cleanupSocket = () => {
if (this.heartbeat) {
clearInterval(this.heartbeat);
this.heartbeat = null;
}
this.socket = null;
this.socketOpen = false;
this.socketConnected = false;
this.specializedSocketConnected = false;
this.cidCount = 0;
this.power = false;
this.screenState = 'Suspend';
this.updateTvState();
};
updateTvState() {
this.emit('powerState', this.power, this.screenState);
this.emit('currentApp', this.appId, this.power);
this.emit('audioState', this.volume, this.mute, this.power);
this.emit('pictureSettings', this.brightness, this.backlight, this.contrast, this.color, this.power);
this.emit('pictureMode', this.pictureMode, this.power);
this.emit('soundMode', this.soundMode, this.power);
this.emit('mediaInfo', this.appId, this.playState, this.appType, this.power);
}
updateSensors() {
this.emit('updateSensors', this.power, this.screenState, this.appId, this.volume, this.mute, this.soundMode, this.soundOutput, this.pictureMode, this.playState, this.channelId);
}
async getCid(type) {
try {
switch (type) {
case 'Power': return this.powerStateId;
case 'App': return this.currentAppId;
case 'Channel': return this.currentChannelId;
case 'Audio': return this.audioStateId;
case 'PictureSettings': return this.pictureSettingsId;
case 'PictureMode': return this.pictureModeId;
case 'SoundMode': return this.soundModeId;
case 'SoundOutput': return this.soundOutputId;
case 'ExternalInputList': return this.externalInputListId;
case 'MediaInfo': return this.mediaInfoId;
default: {
this.cidCount++;
const randomPart = (`0000000${Math.floor(Math.random() * 0xFFFFFFFF).toString(16)}`).slice(-8);
const counterPart = (`000${(this.cidCount).toString(16)}`).slice(-4);
return randomPart + counterPart;
}
}
} catch (error) {
throw new Error(`Get Cid error: ${error}`);
}
}
async getSystemInfo() {
try {
this.systemInfoId = await this.getCid();
await this.send('request', ApiUrls.GetSystemInfo, undefined, this.systemInfoId);
this.systemInfoReceived = true;
} catch (err) {
if (this.logError) this.emit('error', `Get system info error: ${err}`);
}
}
async registerToTv(PairingData) {
try {
const data = { ...PairingData, 'client-key': this.pairingKey };
this.registerId = await this.getCid();
await this.send('register', undefined, data, this.registerId);
return;
} catch (err) {
if (this.logError) this.emit('error', `Socket register error: ${err}`);
}
}
async getSoftwareInfo() {
try {
this.softwareInfoId = await this.getCid();
await this.send('request', ApiUrls.GetSoftwareInfo, undefined, this.softwareInfoId);
return;
} catch (err) {
if (this.logError) this.emit('error', `Get software info error: ${err}`);
}
}
async getSpecjalizedSocket() {
try {
this.specializedSocketId = await this.getCid();
await this.send('request', ApiUrls.SocketUrl, undefined, this.specializedSocketId);
return;
} catch (err) {
if (this.logError) this.emit('error', `Get specjalized socket error: ${err}`);
}
}
async applySoftwareInfoFallback() {
// Called when getCurrentSWInformation returns 401 (webOS 26+).
this.tvInfo.webOS = 26.0;
this.tvInfo.productName = 'webOSTV 26.0';
this.tvInfo.firmwareRevision = this.tvInfo.firmwareRevision || '40.xx.xx';
// deviceId was already filled from serialNumber in the systemInfo handler
if (this.logDebug) this.emit('debug', `Software info fallback (webOS 26+): deviceId=${this.tvInfo.deviceId}, model=${this.tvInfo.modelName}`);
await this.functions.saveData(this.devInfoFile, this.tvInfo);
this.emit('deviceInfo', this.tvInfo);
await this.subscribeTvStatus();
}
async subscribeTvStatus() {
if (this.logDebug) this.emit('debug', `Subscribe tv status`);
try {
this.channelsId = await this.getCid();
await this.send('subscribe', ApiUrls.GetChannelList, undefined, this.channelsId);
this.externalInputListId = await this.getCid();
await this.send('subscribe', ApiUrls.GetExternalInputList, undefined, this.externalInputListId);
this.appsId = await this.getCid();
await this.send('subscribe', ApiUrls.GetInstalledApps, undefined, this.appsId);
await new Promise(resolve => setTimeout(resolve, 500));
this.powerStateId = await this.getCid();
await this.send('subscribe', ApiUrls.GetPowerState, undefined, this.powerStateId);
await new Promise(resolve => setTimeout(resolve, 1500));
this.currentAppId = await this.getCid();
await this.send('subscribe', ApiUrls.GetForegroundAppInfo, undefined, this.currentAppId);
this.currentChannelId = await this.getCid();
await this.send('subscribe', ApiUrls.GetCurrentChannel, undefined, this.currentChannelId);
this.audioStateId = await this.getCid();
await this.send('subscribe', ApiUrls.GetAudioStatus, undefined, this.audioStateId);
this.soundOutputId = await this.getCid();
await this.send('subscribe', ApiUrls.GetSoundOutput, undefined, this.soundOutputId);
//picture
if (this.tvInfo.webOS >= 4.0) {
let payload = {
category: 'picture',
keys: ['brightness', 'backlight', 'contrast', 'color']
};
this.pictureSettingsId = await this.getCid();
await this.send('subscribe', ApiUrls.GetSystemSettings, payload, this.pictureSettingsId);
//mode
payload = {
category: 'picture',
keys: ['pictureMode']
};
this.pictureModeId = await this.getCid();
await this.send('subscribe', ApiUrls.GetSystemSettings, payload, this.pictureModeId);
}
//sound mode
if (this.tvInfo.webOS >= 6.0) {
const payload = {
category: 'sound',
keys: ['soundMode']
};
this.soundModeId = await this.getCid();
await this.send('subscribe', ApiUrls.GetSystemSettings, payload, this.soundModeId);
}
//media info
if (this.tvInfo.webOS >= 7.0) {
this.mediaInfoId = await this.getCid();
await this.send('subscribe', ApiUrls.GetForegroundAppMediaInfo, undefined, this.mediaInfoId);
}
if (this.logDebug) this.emit('debug', `Subscribe tv status successful`);
return true;
} catch (error) {
throw new Error(`Subscribe TV status error: ${error}`);
}
}
async send(type, uri, payload, cid, title, message) {
if (!this.socketOpen) {
if (this.logDebug) this.emit('debug', 'Socket not connected');
return;
}
try {
payload = payload ?? {};
cid = cid ?? await this.getCid();
title = title ?? 'Unknown Title';
message = message ?? 'Unknown Message';
let data;
let messageContent;
const sendAsync = (socket, content) =>
new Promise((resolve, reject) => {
socket.send(content, (err) => err ? reject(err) : resolve(true));
});
switch (type) {
case 'button': {
if (!this.specializedSocketConnected) {
if (this.logWarn) this.emit('warn', 'Specialized socket not connected');
return;
}
const keyValuePairs = Object.entries(payload).map(([k, v]) => `${k}:${v}`);
keyValuePairs.unshift(`type:${type}`);
const array = keyValuePairs.join('\n') + '\n\n';
await sendAsync(this.specializedSocket, array);
return true;
}
case 'alert': {
this.alertCid = cid;
const buttons = [{ label: 'Ok', focus: true, buttonType: 'ok', onClick: uri, params: payload }];
const onClose = { uri, params: payload };
const onFail = { uri, params: payload };
const alertPayload = { title, message, modal: true, buttons, onclose: onClose, onfail: onFail, type: 'confirm', timeout: 0 };
data = { id: cid, type: 'request', uri: ApiUrls.CreateAlert, payload: alertPayload };
messageContent = JSON.stringify(data);
await sendAsync(this.socket, messageContent);
if (this.logDebug) this.emit('debug', `Alert send: ${messageContent}`);
return true;
}
case 'toast': {
this.toastCid = cid;
const toastPayload = { message, iconData: null, iconExtension: null, onClick: payload };
data = { id: cid, type: 'request', uri: ApiUrls.CreateToast, payload: toastPayload };
messageContent = JSON.stringify(data);
await sendAsync(this.socket, messageContent);
if (this.logDebug) this.emit('debug', `Toast send: ${messageContent}`);
return true;
}
default: {
data = { id: cid, type, uri, payload };
messageContent = JSON.stringify(data);
await sendAsync(this.socket, messageContent);
if (this.logDebug) this.emit('debug', `Socket send: ${messageContent}`);
return true;
}
}
} catch (error) {
throw new Error(`Send data error: ${error}`);
}
}
async connectSocket() {
try {
if (this.logDebug) this.emit('debug', `Connect to TV`);
const pairingKey = await this.functions.readData(this.keyFile);
this.pairingKey = (pairingKey ?? '').length > 10 ? pairingKey.toString() : '0';
const url = this.sslWebSocket ? ApiUrls.WssUrl.replace('lgwebostv', this.host) : ApiUrls.WsUrl.replace('lgwebostv', this.host);
const options = this.sslWebSocket ? { rejectUnauthorized: false } : {};
const socket = new WebSocket(url, options)
.on('error', (error) => {
if (this.logDebug) this.emit('debug', `Socket error: ${error}`);
socket.close();
})
.on('close', () => {
if (this.logDebug) this.emit('debug', `Socket closed`);
this.cleanupSocket();
})
.on('open', async () => {
if (this.logDebug) this.emit('debug', `Socket connected to TV`);
if (this.socketConnected) return;
this.socket = socket;
this.emit('success', `Socket Connect Success`);
this.socketConnected = true;
this.heartbeat = setInterval(() => {
if (socket.readyState === socket.OPEN) {
if (this.logDebug) this.emit('debug', `Socket send heartbeat`);
socket.ping();
}
}, 5000);
})
.on('pong', async () => {
if (this.logDebug) this.emit('debug', `Socket received heartbeat`);
if (!this.socketOpen) {
this.socketOpen = true;
await this.getSystemInfo();
await this.registerToTv(PairingOld);
}
})
.on('message', async (message) => {
let parsedMessage;
try {
parsedMessage = JSON.parse(message);
} catch (error) {
if (this.logDebug) this.emit('debug', `Socket received malformed message: ${error}`);
return;
}
const messageId = parsedMessage.id;
const messageType = parsedMessage.type;
const messageData = parsedMessage.payload;
const stringifyMessage = JSON.stringify(messageData, null, 2);
const stringifyData = JSON.stringify(parsedMessage, null, 2);
switch (messageId) {
case this.registerId:
switch (messageType) {
case 'response':
if (this.logDebug) this.emit('debug', `Start registering to TV: ${stringifyMessage}`);
if (this.logWarn) this.emit('warn', 'Please accept authorization on TV');
break;
case 'registered': {
if (this.logDebug) this.emit('debug', `Registered to TV with key: ${messageData['client-key']}`);
const pairingKey = messageData['client-key'];
if (pairingKey !== this.pairingKey) {
await this.functions.saveData(this.keyFile, pairingKey, false);
this.emit('success', 'Pairing key saved');
}
await this.getSpecjalizedSocket();
await this.getSoftwareInfo();
break;
}
case 'error':
if (this.logDebug) this.emit('debug', `Register to TV error: ${stringifyData}, trying new pairing method`);
await new Promise(resolve => setTimeout(resolve, 5000));
await this.registerToTv(PairingNew);
break;
default:
if (this.logDebug) this.emit('debug', `Register to TV unknown message, type: ${messageType}, id: ${messageId}, data: ${stringifyData}`);
return;
}
break;
case this.specializedSocketId:
switch (messageType) {
case 'response': {
if (this.logDebug) this.emit('debug', `Connecting to specialized socket`);
const socketUrl = messageData.socketPath;
const specializedSocket = new WebSocket(socketUrl, options)
.on('error', (error) => {
if (this.logDebug) this.emit('debug', `Specialized socket connect error: ${error}`);
specializedSocket.close();
})
.on('close', async () => {
if (this.logDebug) this.emit('debug', 'Specialized socket closed');
this.specializedSocketConnected = false;
this.specializedSocket = null;
await new Promise(resolve => setTimeout(resolve, 5000));
if (this.logDebug) this.emit('debug', 'Retrying connect to specialized socket...');
try {
await this.getSpecjalizedSocket();
} catch (error) {
if (this.logError) this.emit('error', `Specialized socket retry connect error: ${error}`);
}
})
.on('open', () => {
if (this.logDebug) this.emit('debug', `Specialized socket connected, path: ${socketUrl}`);
this.specializedSocket = specializedSocket;
this.specializedSocketConnected = true;
this.emit('success', `Specialized Socket Connect Success`);
});
break;
}
case 'error':
if (this.logDebug) this.emit('debug', `Specialized socket error: ${stringifyData}`);
break;
default:
if (this.logDebug) this.emit('debug', `Specialized socket unknown message: type=${messageType}, id=${messageId}, data=${stringifyData}`);
return;
}
break;
case this.systemInfoId:
switch (messageType) {
case 'response':
if (this.logDebug) this.emit('debug', `System info: ${stringifyMessage}`);
this.tvInfo.modelName = messageData.modelName ?? 'LG TV';
// webOS 26+: getSystemInfo returns serialNumber instead of deviceId;
this.tvInfo.deviceId = messageData.deviceId ?? messageData.serialNumber ?? this.tvInfo.deviceId;
if (this.restFulEnabled) this.emit('restFul', 'systeminfo', messageData);
if (this.mqttEnabled) this.emit('mqtt', 'System info', messageData);
break;
case 'error':
if (this.logDebug) this.emit('debug', `System info error: ${stringifyData}`);
break;
default:
if (this.logDebug) this.emit('debug', `System info received message, type: ${messageType}, id: ${messageId}, data: ${stringifyData}`);
return;
}
break;
case this.softwareInfoId:
switch (messageType) {
case 'response':
if (this.logDebug) this.emit('debug', `Software Info: ${stringifyMessage}`);
this.tvInfo.productName = messageData.product_name;
this.tvInfo.deviceId = messageData.device_id;
this.tvInfo.firmwareRevision = `${messageData.major_ver}.${messageData.minor_ver}`;
this.tvInfo.webOS = Number.isFinite(Number(messageData.product_name?.match(/\d+(\.\d+)?/)?.[0])) ? Number(messageData.product_name.match(/\d+(\.\d+)?/)[0]) : this.tvInfo.webOS;
await this.functions.saveData(this.devInfoFile, this.tvInfo);
this.emit('deviceInfo', this.tvInfo);
await this.subscribeTvStatus();
if (this.restFulEnabled) this.emit('restFul', 'softwareinfo', messageData);
if (this.mqttEnabled) this.emit('mqtt', 'Software info', messageData);
break;
case 'error':
// webOS 26+ blocks getCurrentSWInformation with 401 insufficient permissions.
// Fall back to data already harvested from getSystemInfo and derive
if (this.logDebug) this.emit('debug', `Software info blocked (likely webOS 26+ 401), applying fallback: ${stringifyData}`);
await this.applySoftwareInfoFallback();
break;
default:
if (this.logDebug) this.emit('debug', `Software info received message, type: ${messageType}, id: ${messageId}, data: ${stringifyData}`);
return;
}
break;
case this.channelsId:
switch (messageType) {
case 'response': {
if (this.logDebug) this.emit('debug', `Channels list: ${stringifyMessage}`);
const channelsList = messageData.channelList;
const channelListExist = Array.isArray(channelsList) ? channelsList.length > 0 : false;
if (!channelListExist) return;
const channelsArr = [];
for (const channel of channelsList) {
channelsArr.push({
name: channel.channelName,
reference: channel.channelId,
number: channel.channelNumber,
mode: 1
});
}
await this.functions.saveData(this.channelsFile, channelsArr);
if (this.restFulEnabled) this.emit('restFul', 'channels', messageData);
if (this.mqttEnabled) this.emit('mqtt', 'Channels', messageData);
break;
}
case 'error':
if (this.logDebug) this.emit('debug', `Channels error: ${stringifyData}`);
break;
default:
if (this.logDebug) this.emit('debug', `Channels list received message, type: ${messageType}, id: ${messageId}, data: ${stringifyData}`);
return;
}
break;
case this.externalInputListId:
switch (messageType) {
case 'response': {
if (this.logDebug) this.emit('debug', `External input list: ${stringifyMessage}`);
const externalInputList = messageData.devices;
const externalInputListExist = this.getInputsFromDevice && Array.isArray(externalInputList) ? externalInputList.length > 0 : false;
if (!externalInputListExist) return;
const arr = [];
for (const input of externalInputList) {
arr.push({
name: input.label,
reference: input.appId,
mode: 0,
visible: input.visible ?? true
});
}
this.externalInputsArr = arr;
if (this.restFulEnabled) this.emit('restFul', 'externalinputlist', messageData);
if (this.mqttEnabled) this.emit('mqtt', 'External Input List', messageData);
break;
}
case 'error':
if (this.logDebug) this.emit('debug', `External input list error: ${stringifyData}`);
break;
default:
if (this.logDebug) this.emit('debug', `External input list received message, type: ${messageType}, id: ${messageId}, data: ${stringifyData}`);
return;
}
break;
case this.appsId:
switch (messageType) {
case 'response': {
if (this.logDebug) this.emit('debug', `Apps list: ${stringifyMessage}`);
if (this.getInputsFromDevice) {
const appInstalled = messageData.changeReason === 'appInstalled';
const appUninstalled = messageData.changeReason === 'appUninstalled';
const appUpdated = !appInstalled && !appUninstalled;
if (appUninstalled && messageData.app) {
this.inputs = this.inputs.filter(input => input.reference !== messageData.app.id);
const inputs = [{ name: messageData.app.title, reference: messageData.app.id }];
await this.functions.saveData(this.inputsFile, this.inputs);
this.emit('installedApps', inputs, true);
return;
}
const appsList = appInstalled ? [messageData.app] : messageData.apps;
if (!Array.isArray(appsList) || appsList.length === 0) return;
const arr = [];
for (const app of appsList) {
if (app?.id && app?.title) {
arr.push({
name: app.title,
reference: app.id,
mode: 0,
visible: app.visible
});
}
}
if (appUpdated && this.tvInfo.webOS >= 4.0) arr.push({ name: 'Screen Off', reference: 'com.webos.app.screenoff', mode: 0 });
this.inputs = [...this.externalInputsArr, ...arr];
}
await this.functions.saveData(this.inputsFile, this.inputs);
this.emit('installedApps', this.inputs, false);
if (this.restFulEnabled) this.emit('restFul', 'apps', messageData);
if (this.mqttEnabled) this.emit('mqtt', 'Apps', messageData);
break;
}
case 'error':
if (this.logDebug) this.emit('debug', `Apps list error: ${stringifyData}`);
break;
default:
if (this.logDebug) this.emit('debug', `Apps list received message, type: ${messageType}, id: ${messageId}, data: ${stringifyData}`);
return;
}
break;
case this.powerStateId:
switch (messageType) {
case 'response':
if (this.logDebug) this.emit('debug', `Power: ${stringifyMessage}`);
switch (messageData.state) {
case 'Active':
this.power = true;
this.screenState = 'Active';
this.emit('currentApp', this.appId);
break;
case 'Active Standby':
this.power = false;
this.screenState = 'Active Standby';
break;
case 'Screen Saver':
this.power = true;
this.screenState = 'Screen Saver';
this.emit('currentApp', 'com.webos.app.screensaver');
break;
case 'Screen Off':
this.power = true;
this.screenState = 'Screen Off';
this.emit('currentApp', 'com.webos.app.screenoff');
break;
case 'Suspend':
this.power = false;
this.screenState = 'Suspend';
break;
default:
if (this.logDebug) this.emit('debug', `Unknown power state: ${stringifyMessage}`);
return;
}
this.emit('powerState', this.power, this.screenState);
this.updateSensors();
if (!this.power) this.updateTvState();
if (this.restFulEnabled) this.emit('restFul', 'powerstate', this.power);
if (this.restFulEnabled) this.emit('restFul', 'power', messageData);
if (this.mqttEnabled) this.emit('mqtt', 'Power State', this.power);
if (this.mqttEnabled) this.emit('mqtt', 'Power', messageData);
break;
case 'error':
if (this.tvInfo.webOS < 3.0) {
this.power = this.socketConnected;
this.screenState = this.socketConnected ? 'Active' : 'Suspend';
this.emit('powerState', this.power, this.screenState);
if (!this.power) this.updateTvState();
if (this.logDebug) this.emit('debug', `Installed system webOS: ${this.tvInfo.webOS}`);
} else {
if (this.logDebug) this.emit('debug', `Power error: ${stringifyData}`);
}
break;
default:
if (this.logDebug) this.emit('debug', `Power received message, type: ${messageType}, id: ${messageId}, data: ${stringifyData}`);
return;
}
break;
case this.currentAppId:
switch (messageType) {
case 'response': {
if (this.logDebug) this.emit('debug', `App: ${stringifyMessage}`);
const appId = messageData.appId;
if (!appId) return;
this.appId = appId;
this.emit('currentApp', appId, this.power);
this.updateSensors();
if (this.restFulEnabled) this.emit('restFul', 'currentapp', messageData);
if (this.mqttEnabled) this.emit('mqtt', 'Current App', messageData);
break;
}
case 'error':
if (this.logDebug) this.emit('debug', `App error: ${stringifyData}`);
break;
default:
if (this.logDebug) this.emit('debug', `App received message, type: ${messageType}, id: ${messageId}, data: ${stringifyData}`);
return;
}
break;
case this.audioStateId:
switch (messageType) {
case 'response': {
if (this.logDebug) this.emit('debug', `Audio: ${stringifyMessage}`);
const volume = messageData.volume >= 0 ? messageData.volume : this.volume;
const mute = (messageData.mute ?? messageData.muteStatus) !== undefined ? !!(messageData.mute ?? messageData.muteStatus) : this.mute;
this.volume = volume;
this.mute = mute;
this.emit('audioState', volume, mute, this.power);
this.updateSensors();
break;
}
case 'error':
if (this.logDebug) this.emit('debug', `Audio error: ${stringifyData}`);
break;
default:
if (this.logDebug) this.emit('debug', `Audio received message, type: ${messageType}, id: ${messageId}, data: ${stringifyData}`);
return;
}
if (this.restFulEnabled) this.emit('restFul', 'audio', messageData);
if (this.mqttEnabled) this.emit('mqtt', 'Audio', messageData);
break;
case this.currentChannelId:
switch (messageType) {
case 'response': {
if (this.logDebug) this.emit('debug', `Channel: ${stringifyMessage}`);
const channelId = messageData.channelId;
const channelName = messageData.channelName;
const channelNumber = messageData.channelNumber;
if (!channelId) return;
this.channelId = channelId;
this.emit('currentChannel', channelId, channelName, channelNumber, this.power);
this.updateSensors();
if (this.restFulEnabled) this.emit('restFul', 'currentchannel', messageData);
if (this.mqttEnabled) this.emit('mqtt', 'Current Channel', messageData);
break;
}
case 'error':
if (this.logDebug) this.emit('debug', `Channel error: ${stringifyData}`);
break;
default:
if (this.logDebug) this.emit('debug', `Channel received message, type: ${messageType}, id: ${messageId}, data: ${stringifyData}`);
return;
}
break;
case this.pictureSettingsId:
switch (messageType) {
case 'response': {
if (this.logDebug) this.emit('debug', `Picture settings: ${stringifyMessage}`);
const settings = messageData.settings;
if (!settings) return;
const brightness = settings.brightness ?? this.brightness;
const backlight = settings.backlight ?? this.backlight;
const contrast = settings.contrast ?? this.contrast;
const color = settings.color ?? this.color;
this.brightness = brightness;
this.backlight = backlight;
this.contrast = contrast;
this.color = color;
this.emit('pictureSettings', brightness, backlight, contrast, color, this.power);
this.updateSensors();
if (this.restFulEnabled) this.emit('restFul', 'picturesettings', messageData);
if (this.mqttEnabled) this.emit('mqtt', 'Picture Settings', messageData);
break;
}
case 'error':
if (this.logDebug) this.emit('debug', `Picture settings error: ${stringifyData}`);
break;
default:
if (this.logDebug) this.emit('debug', `Picture settings received message, type: ${messageType}, id: ${messageId}, data: ${stringifyData}`);
return;
}
break;
case this.pictureModeId:
switch (messageType) {
case 'response': {
if (this.logDebug) this.emit('debug', `Picture mode: ${stringifyMessage}`);
const pictureMode = messageData.settings?.pictureMode;
if (!pictureMode) return;
this.pictureMode = pictureMode;
this.emit('pictureMode', pictureMode, this.power);
this.updateSensors();
if (this.restFulEnabled) this.emit('restFul', 'picturemode', messageData);
if (this.mqttEnabled) this.emit('mqtt', 'Picture Mode', messageData);
break;
}
case 'error':
if (this.logDebug) this.emit('debug', `Picture mode error: ${stringifyData}`);
break;
default:
if (this.logDebug) this.emit('debug', `Picture mode received message, type: ${messageType}, id: ${messageId}, data: ${stringifyData}`);
return;
}
break;
case this.soundModeId:
switch (messageType) {
case 'response': {
if (this.logDebug) this.emit('debug', `Sound mode: ${stringifyMessage}`);
const settings = messageData.settings;
if (!settings) return;
const soundMode = settings.soundMode ?? this.soundMode;
this.soundMode = soundMode;
this.emit('soundMode', soundMode, this.power);
this.updateSensors();
if (this.restFulEnabled) this.emit('restFul', 'soundmode', messageData);
if (this.mqttEnabled) this.emit('mqtt', 'Sound Mode', messageData);
break;
}
case 'error':
if (this.logDebug) this.emit('debug', `Sound mode error: ${stringifyData}`);
break;
default:
if (this.logDebug) this.emit('debug', `Sound mode received message, type: ${messageType}, id: ${messageId}, data: ${stringifyData}`);
return;
}
break;
case this.soundOutputId:
switch (messageType) {
case 'response': {
if (this.logDebug) this.emit('debug', `Sound output: ${stringifyMessage}`);
const soundOutput = messageData.soundOutput ?? this.soundOutput;
if (!soundOutput) return;
this.soundOutput = soundOutput;
this.emit('soundOutput', soundOutput, this.power);
this.updateSensors();
if (this.restFulEnabled) this.emit('restFul', 'soundoutput', messageData);
if (this.mqttEnabled) this.emit('mqtt', 'Sound Output', messageData);
break;
}
case 'error':
if (this.logDebug) this.emit('debug', `Sound output error: ${stringifyData}`);
break;
default:
if (this.logDebug) this.emit('debug', `Sound output received message, type: ${messageType}, id: ${messageId}, data: ${stringifyData}`);
return;
}
break;
case this.mediaInfoId:
switch (messageType) {
case 'response': {
// {"subscribed":true,"returnValue":true,"foregroundAppInfo":[{"appId":"netflix","playState":"loaded","type":"media","mediaId":"_fdTzfnKXXXXX","windowId":"_Window_Id_1"}]}
// reported playState values: starting, loaded, playing, paused
if (this.logDebug) this.emit('debug', `Media info: ${stringifyMessage}`);
const foregroundAppInfo = messageData.foregroundAppInfo ?? [];
const mediaAppOpen = foregroundAppInfo.length > 0;
if (!mediaAppOpen) return;
const appId = foregroundAppInfo[0].appId ?? this.appId;
const playState = foregroundAppInfo[0].playState === 'playing';
const appType = foregroundAppInfo[0].type ?? this.appType;
this.appId = appId;
this.playState = playState;
this.appType = appType;
this.emit('mediaInfo', appId, playState, appType, this.power);
this.updateSensors();
if (this.restFulEnabled) this.emit('restFul', 'mediainfo', messageData);
if (this.mqttEnabled) this.emit('mqtt', 'Media Info', messageData);
break;
}
case 'error':
if (this.logDebug) this.emit('debug', `Media info error: ${stringifyData}`);
break;
default:
if (this.logDebug) this.emit('debug', `Media info received message, type: ${messageType}, id: ${messageId}, data: ${stringifyData}`);
return;
}
break;
case this.alertCid: {
if (this.logDebug) this.emit('debug', `Alert: ${stringifyMessage}`);
const alertId = messageData.alertId ?? false;
if (!alertId) return;
if (this.tvInfo.webOS >= 4.0 && this.tvInfo.webOS < 24.0) {
await this.send('request', ApiUrls.CloseAlert, { alertId });
} else {
await new Promise(resolve => setTimeout(resolve, 20));
await this.send('button', undefined, { name: 'ENTER' });
}
break;
}
case this.toastCid: {
if (this.logDebug) this.emit('debug', `Toast: ${stringifyMessage}`);
const toastId = messageData.toastId ?? false;
if (!toastId) return;
if (this.tvInf