Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
233 changes: 129 additions & 104 deletions lib/lg_module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
*
* sends system/turnOn
**/
import wol from 'wol'
import LGTV from 'lgtv2';
import { LGTV, Inputs, DefaultSettings } from 'lgtv-ip-control';
import express from 'express';

/**
* Listening to a wildcard topic, anything after the topic is the command to control the TV
Expand All @@ -17,149 +17,171 @@ import LGTV from 'lgtv2';
**/
export class LgWebOSModule {


static readonly INPUT_LIVETV = "dtv";
static readonly INPUT_HDMI1 = "hdmi1";
static readonly INPUT_HDMI2 = "hdmi2";

config: LGConfig;
lgtv!: LGTV;
connected = false;
reties: 0;

constructor(config: LGConfig) {
this.config = config;

console.log("Subscribing to " + this.config.topic);
global.SmartHub.mqttClient.subscribe(this.config.topic + "/#");
global.SmartHub.mqttClient.on('message', this.onMessage.bind(this));
}

connect() {
this.lgtv = LGTV({
url: `ws://${this.config.address}:3000`,
timeout: 30000,
reconnect: 5000,
});

return new Promise((resolve, reject) => {
DefaultSettings.networkWolAddress = "192.168.1.255";
DefaultSettings.networkTimeout = 1000;

this.lgtv.on('error', (err: Error) => {
console.log('Connecting to TV failed!');
});

this.lgtv.on('connecting', () => {
this.reties++;
if (this.reties > 4) {
console.log("Max connection attempts exceeded, discounting");
this.lgtv.disconnect();
this.lgtv = undefined;
this.reties = 0;
return;
}
console.log('Connecting to TV...');
});

this.lgtv.on('connect', () => {
console.log('Successfully connected!');
this.connected = true;
this.reties = 0;
resolve(true);
});

this.lgtv.on('prompt', () => {
console.log('Please authorize on TV');
});

this.lgtv.on('close', () => {
this.connected = false;
console.log('Connection to TV lost');
});
});
this.lgtv = new LGTV(
this.config.address,
this.config.macAddress,
this.config.key,
DefaultSettings
);
}

async sendCommand(command: string, message: any, powerOn: boolean = false) {
console.log("Sending " + command);
async connect() {
let retries = 0;
try {
await this.lgtv.connect();
retries = 0;
console.log('Successfully connected!');
return true;
} catch (err) {
retries++;
if (retries > 4) {
console.log("Max connection attempts exceeded, disconnecting");
this.lgtv.disconnect();
retries = 0;
return false;
}
console.log('Connecting to TV failed!');
throw err;
}
}

if (powerOn && !this.lgtv) {
await this.powerOn();
private async sendCommand(command: any, powerOn: boolean = false) {
if (powerOn && !this.lgtv.connected) {
await this.powerOnAndConnect();
} else if (!this.lgtv.connected) {
await this.connect();
}

if (!this.lgtv && !this.connected) {
if (!this.lgtv.connected) {
console.log("Ignoring request, TV is not ready.");
return;
}

return new Promise((resolve, reject) => {
this.lgtv.request("ssap://" + command, JSON.stringify(message), (err: Error | null, res: any) => {
if (err) {
reject(err);
return;
}

resolve(res);
});
});
}

powerOn() {
if (this.lgtv) {
console.log("TV already on");
return;
try {
await command();
} catch (err) {
console.log("Error sending command: " + err);
}
}

console.log("Sending WOL to " + this.config.macAddress);
return new Promise(async (resolve, reject) => {
wol.wake(this.config.macAddress, {}, async (err: any, res: any) => {
if (err) {
reject(err);
return
}

try {
await this.connect();
console.log("TV ready...")
resolve(true);
} catch (error: any) {
reject(err);
}
});
});
async powerOnAndConnect() {
await this.lgtv.powerOn();
await this.lgtv.connect({ maxRetries: 25, retryTimeout: 5000 });
}

async powerOff() {
await this.sendCommand("system/turnOff", {}, true);
this.lgtv.disconnect();
this.lgtv = undefined;
await this.lgtv.disconnect();
await this.lgtv.powerOff();
}

volumeUp() {
return this.sendCommand("audio/volumeUp", {});
async volumeUp() {
await this.sendCommand(async () => {
let currentVolume = await this.lgtv.getCurrentVolume() + 1;
await this.lgtv.setVolume();
});
}

volumeDown() {
return this.sendCommand("audio/volumeDown", {});
async volumeDown() {
await this.sendCommand(async () => {
let currentVolume = await this.lgtv.getCurrentVolume() - 1;
await this.lgtv.setVolume(currentVolume);
});
}

volume(level: any) {
return this.sendCommand("audio/setVolume", { volume: level });
async volume(level: any) {
await this.sendCommand(async () => {
await this.lgtv.setVolume(level);
});
}

channel(channelNum: any) {
return this.sendCommand("tv/openChannel", { channelNumber: channelNum }, true);
async channel(channelNum: any) {
await this.sendCommand(async () => {
await this.lgtv.setDigitalChannel(channelNum);
}, true);
}

/**
* com.webos.app.hdmi1
* com.webos.app.livetv
**/
input(input: any) {
return this.sendCommand("system.launcher/launch", { id: input }, true);
async input(input: any) {
if (Inputs[input]) {
await this.sendCommand(async () => {
await this.lgtv.setInput(Inputs[input]);
}, true);
} else {
console.log("Unknown input: " + input);
}
}

onMessage(topic: any, message: any) {
//{topic_prefix}/audio/setVolume with payload { "volume": 15 }
onMessage(topic: any, payload: any) {
if (topic.startsWith(this.config.topic)) {
console.log("Message recieved on " + topic);
var command = topic.replace(this.config.topic + "/", "");
this.sendCommand(command, JSON.parse(message));
console.log("Message received on " + topic);
const command = topic.replace(this.config.topic + "/", "");

const message = payload.toString();
switch (command) {
case "audio/volumeUp":
return this.volumeUp();
case "audio/volumeDown":
return this.volumeDown();
case "audio/setVolume":
return this.volume(message);
case "system/turnOff":
return this.lgtv.powerOff();
case "tv/openChannel":
return this.channel(message);
case "system.launcher/launch":
return this.input(Inputs[message]);
default:
console.log("Unknown command: " + command);
return;
}
}
}

static registerApis() {
let router = express.Router();

console.log("Registering /watchTV")
router.post("/watchTV/:id", async (req, res) => {

let chanNum = req.query.chanNum;

try {
//Switch TV input to LiveTV
await global.SmartHub.modules[req.params.id].input(LgWebOSModule.INPUT_LIVETV);

//Change change
await global.SmartHub.modules[req.params.id].channel(chanNum);

console.log("watchTV completed");
res.sendStatus(200);
} catch (err) {
console.log(err);

res.sendStatus(500);
}
});

global.SmartHub.express.use('/api', router);
}

static init() {
if (!global.SmartHub.config.lg || (global.SmartHub.config.lg as LGConfig[]).length === 0) {
console.log("No LG TV configuration found");
Expand All @@ -169,6 +191,8 @@ export class LgWebOSModule {
(global.SmartHub.config.lg as LGConfig[]).forEach((config) => {
global.SmartHub.modules[config.id] = new LgWebOSModule(config);
});

LgWebOSModule.registerApis();
}
}

Expand All @@ -177,4 +201,5 @@ interface LGConfig {
topic: string;
address: string;
macAddress: string;
key: string; // lgtv-ip-control pairing key
}
25 changes: 2 additions & 23 deletions lib/mythtv_module.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { InputSource } from './onkyo_module';
import { LgWebOSModule } from './lg_module';

const http = require('http');
const express = require('express');
Expand Down Expand Up @@ -100,7 +101,7 @@ export class MythTvModule {

try {
//Switch TV input to HDMI1
await SmartHub.modules.livingRoomTV.input("com.webos.app.hdmi2");
await SmartHub.modules.livingRoomTV.input(LgWebOSModule.INPUT_HDMI1);

//Play Recording
await SmartHub.modules.mythTvModule.play(chanId, startTime);
Expand All @@ -117,28 +118,6 @@ export class MythTvModule {

});

console.log("Registering /watchTV")
router.post("/watchTV", async (req, res) => {

let chanNum = req.query.chanNum;

try {
//Switch TV input to LiveTV
await SmartHub.modules.lgTvModule.input("com.webos.app.livetv");

//Change change
await SmartHub.modules.lgTvModule.channel(chanNum);

//Switch Reciever input to TV
await SmartHub.modules["TX-NR656"].input(InputSource.TV);

console.log("watchTV completed");
res.sendStatus(200);
} catch (err) {
res.sendStatus(500);
}
});

SmartHub.express.use('/api', router);
}
}
Expand Down
2 changes: 1 addition & 1 deletion lib/onkyo_module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export class OnkyoModule {
console.log("eISCP Scanning....")

// Discover receviers on network, stop after 2 receviers or 5 seconds
eiscp.discover({ devices: this.config.length, timeout: 10 }, (err, result) => {
eiscp.discover({ devices: this.config.length, timeout: 10, address: "192.168.1.255" }, (err, result) => {

if (err) {
console.log("Error message: " + result);
Expand Down
2 changes: 1 addition & 1 deletion lib/tplink_module.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ TPLinkModule.init = function () {
let tpLinkModule = new TPLinkModule(SmartHub.config.tplink, SmartHub.mqttClient);
tpLinkModule.subscribeHouseTopic();
tpLinkModule.discoverDevices();

SmartHub.modules["tpLinkModule"] = tpLinkModule;
};

Expand Down
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,18 @@
"scripts": {
"start": "node dist/server.js",
"tsc": "tsc",
"watch": "tsc --watch",
"postinstall": "npm run tsc"
},
"dependencies": {
"cookie-parser": "^1.4.4",
"cron": "^1.4.1",
"debug": "^4.1.0",
"didyoumean2": "^2.0.4",
"eiscp": "file:/home/john/Development/node-eiscp",
"eiscp": "file:/home/john/Development/personal/node-eiscp",
"express": "^4.16.4",
"firebase-admin": "^10.0.1",
"lgtv2": "file:/home/john/Development/lgtv2",
"lgtv-ip-control": "file:/home/john/Development/personal/lgtv-ip-control/packages/lgtv-ip-control",
"mqtt": "^4.3.0",
"mysql": "^2.18.1",
"plivo": "^4.12.0",
Expand All @@ -29,9 +30,9 @@
},
"license": "MIT",
"devDependencies": {
"@types/lgtv2": "^1.4.5",
"@types/cookie-parser": "^1.4.9",
"@types/express": "^5.0.3",
"@types/lgtv2": "^1.4.5",
"@types/mqtt": "^0.0.34",
"@types/node": "^24.5.2",
"@types/request": "^2.48.13",
Expand Down