Last active
August 17, 2019 19:50
-
-
Save MohammadAG/b5355503ece2be98a431aadf82046319 to your computer and use it in GitHub Desktop.
webOS 3 accessory plugin for HAP-NodeJS
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var Accessory = require('../').Accessory; | |
var Service = require('../').Service; | |
var Characteristic = require('../').Characteristic; | |
var uuid = require('../').uuid; | |
var ping = require ("net-ping"); | |
var http = require('http'); | |
var url = require("url"); | |
var util = require("util"); | |
var wsUrl = "ws://IP_ADDRESS_HERE:3000"; | |
var tvMacAddress = "MAC_ADDRESS_HERE"; | |
var lgtv; | |
var wol = require('wake_on_lan'); | |
var on = false; | |
var connected = false; | |
var volume = 0; | |
var muted = false; | |
// Generate a consistent UUID for TV that will remain the same even when | |
// restarting our server. We use the `uuid.generate` helper function to create a deterministic | |
// UUID based on an arbitrary "namespace" and the word "tv". | |
var tvUUID = uuid.generate('hap-nodejs:accessories:tv'); | |
// This is the Accessory that we'll return to HAP-NodeJS. | |
var tv = exports.accessory = new Accessory('TV', tvUUID); | |
// Add properties for publishing (in case we're using Core.js and not BridgedCore.js) | |
tv.username = "A3:FB:3D:4D:2E:AC"; | |
tv.pincode = "031-45-154"; | |
var idList = [ | |
"com.webos.app.livetv", | |
"com.webos.app.hdmi1", | |
"com.webos.app.hdmi2", | |
"netflix", | |
"spotify-beehive", | |
"youtube.leanback.v4" | |
]; | |
var mouseSocket = null; | |
var powerOnCallback = null; | |
lgtv = require('lgtv2')({ | |
url: wsUrl, | |
timeout: 5000, | |
reconnect: 3000 | |
}); | |
lgtv.on('connect', function() { | |
console.log('Connected to webOS TV'); | |
connected = true; | |
on = 1 | |
tv.getService(Service.Television).getCharacteristic(Characteristic.Active).updateValue(on); | |
lgtv.subscribe('ssap://audio/getVolume', function (err, res) { | |
if (res.changed.indexOf('volume') !== -1) { | |
console.log('volume changed', res.volume); | |
volume = res.volume; | |
} | |
if (res.changed.indexOf('muted') !== -1) { | |
console.log('mute changed', res.muted); | |
muted = res.muted; | |
} | |
tv.getService(Service.TelevisionSpeaker).getCharacteristic(Characteristic.Volume).updateValue(volume); | |
tv.getService(Service.TelevisionSpeaker).getCharacteristic(Characteristic.Mute).updateValue(muted); | |
}); | |
lgtv.getSocket('ssap://com.webos.service.networkinput/getPointerInputSocket', function(err, sock) { | |
if (!err) { | |
mouseSocket = sock; | |
} | |
}); | |
lgtv.subscribe('ssap://com.webos.applicationManager/getForegroundAppInfo', function (err, res) { | |
var index = idList.indexOf(res.appId); | |
console.log("App changed to " + res.appId); | |
if (index != -1) { | |
tv.getService(Service.Television).getCharacteristic(Characteristic.ActiveIdentifier).updateValue(index); | |
} | |
}); | |
lgtv.request('ssap://com.webos.applicationManager/listLaunchPoints', function (err, res) { | |
var launchPoints = res.launchPoints; | |
console.log(util.inspect(launchPoints)); | |
}); | |
lgtv.request('api/getServiceList', function (err, res) { | |
console.log(util.inspect(res)); | |
}); | |
if (powerOnCallback) { | |
powerOnCallback(); | |
powerOnCallback = true; | |
} | |
}); | |
lgtv.on('close', function() { | |
console.log('Disconnected from webOS TV'); | |
connected = false; | |
on = 0 | |
tv.getService(Service.Television).getCharacteristic(Characteristic.Active).updateValue(on); | |
mouseSocket = null; | |
}); | |
lgtv.on('error', function(error) { | |
console.log('webOS error %s', error); | |
connected = false; | |
//setTimeout(lgtv.connect(this.url), 5000); | |
}); | |
lgtv.on('prompt', function() { | |
console.log('webOS prompt for confirmation'); | |
connected = false; | |
}); | |
lgtv.on('connecting', function() { | |
console.log('webOS connecting to TV'); | |
connected = false; | |
}); | |
var retryCount = 0; | |
function adjustVolume(increment, callback) { | |
if (!connected) { | |
callback(null); | |
return; | |
} | |
var url = increment ? "ssap://audio/volumeUp" : "ssap://audio/volumeDown"; | |
lgtv.request(url, function(err, res) { | |
if (err) { | |
console.log("Error adjusting volume"); | |
return; | |
}; | |
console.log("Volume adjusted to " + util.inspect(res)); | |
callback(true); | |
}); | |
} | |
function setMute(value, callback) { | |
if (!connected) { | |
callback(null); | |
return; | |
} | |
lgtv.request('ssap://audio/setMute', {mute: value}, function(err, res) { | |
if (err) { | |
console.log("Error changing mute: " + err); | |
return; | |
} | |
console.log("Volume mute set to " + res); | |
console.log(util.inspect(res)); | |
callback(null); | |
}); | |
} | |
function setVolume(value, callback) { | |
if (!connected) { | |
callback(null); | |
return; | |
} | |
lgtv.request("ssap://audio/setVolume", {volume: value}, function(err, res) { | |
if (err) { | |
console.log("Error changing volume: " + err); | |
return; | |
} | |
console.log("Volume changed to " + res); | |
console.log(util.inspect(res)); | |
callback(null); | |
}); | |
} | |
function checkWakeOnLan(callback) { | |
if (connected) { | |
console.log("TV now connected, notifying HomeKit"); | |
callback(1); | |
retryCount = 0; | |
} else { | |
if (retryCount < 10) { | |
console.log("TV not connected, trying again in 2 seconds"); | |
lgtv.connect(wsUrl); | |
setTimeout(checkWakeOnLan.bind(this, callback), 2000); | |
retryCount ++; | |
} | |
} | |
} | |
function powerOnTv(callback) { | |
wol.wake(tvMacAddress, function(error) { | |
if (error) return callback(new Error('webOS wake on lan error')); | |
retryCount = 0; | |
powerOnCallback = callback; | |
lgtv.connect(wsUrl); | |
//setTimeout(checkWakeOnLan.bind(this, callback), 3000); | |
}) | |
} | |
function powerOffTv(callback) { | |
lgtv.request('ssap://system/turnOff', function(err, res) { | |
if (err) return callback(null); | |
lgtv.disconnect(); | |
connected = false ; | |
on = false; | |
return callback(null); | |
}) | |
} | |
function ensureOnAndThen(callback) { | |
if (on) { | |
callback(); | |
} else { | |
powerOnTv(callback); | |
} | |
} | |
// Add the actual TV Service and listen for change events from iOS. | |
// We can see the complete list of Services and Characteristics in `lib/gen/HomeKitTypes.js` | |
var televisionService = tv.addService(Service.Television, "Television", "Television"); | |
tv | |
.getService(Service.AccessoryInformation) | |
.setCharacteristic(Characteristic.Manufacturer, "LG") | |
.setCharacteristic(Characteristic.Model, "60UH651Y-TE") | |
.setCharacteristic(Characteristic.FirmwareRevision, "3.3.3-4205"); | |
televisionService | |
.setCharacteristic(Characteristic.ConfiguredName, "TV"); | |
televisionService | |
.setCharacteristic( | |
Characteristic.SleepDiscoveryMode, | |
Characteristic.SleepDiscoveryMode.ALWAYS_DISCOVERABLE | |
); | |
televisionService | |
.getCharacteristic(Characteristic.Active) | |
.on('set', function(newValue, callback) { | |
console.log("set Active => setNewValue: " + newValue); | |
if (newValue && !on) { | |
powerOnTv(callback); | |
} else if (!newValue && on) { | |
powerOffTv(callback); | |
} else { | |
callback(null); | |
} | |
}); | |
televisionService | |
.setCharacteristic(Characteristic.ActiveIdentifier, 1); | |
televisionService | |
.getCharacteristic(Characteristic.ActiveIdentifier) | |
.on('set', function(newValue, callback) { | |
console.log("set Active Identifier => setNewValue: " + newValue); | |
ensureOnAndThen(function() { | |
lgtv.request('ssap://system.launcher/launch', {id: idList[newValue]}, function(err, res) { | |
if (err) { | |
console.log("Error launching app: " + idList[newValue]); | |
callback(null); | |
return; | |
} | |
callback(); | |
}); | |
}); | |
}); | |
televisionService | |
.getCharacteristic(Characteristic.RemoteKey) | |
.on('set', function(newValue, callback) { | |
console.log("set Remote Key => setNewValue: " + newValue); | |
if (mouseSocket != null) { | |
var keyName = null; | |
if (newValue == Characteristic.RemoteKey.ARROW_UP) { | |
keyName = "UP"; | |
} else if (newValue == Characteristic.RemoteKey.ARROW_LEFT) { | |
keyName = "LEFT"; | |
} else if (newValue == Characteristic.RemoteKey.ARROW_RIGHT) { | |
keyName = "RIGHT"; | |
} else if (newValue == Characteristic.RemoteKey.ARROW_DOWN) { | |
keyName = "DOWN"; | |
} else if (newValue == Characteristic.RemoteKey.BACK) { | |
keyName = "BACK"; | |
} else if (newValue == Characteristic.RemoteKey.EXIT) { | |
keyName = "EXIT"; | |
} else if (newValue == Characteristic.RemoteKey.INFORMATION) { | |
keyName = "HOME"; | |
} else if (newValue == Characteristic.RemoteKey.SELECT) { | |
mouseSocket.send('click'); | |
} | |
if (keyName != null) { | |
mouseSocket.send("button\n" + "name:" + keyName + "\n" + "\n"); | |
} | |
} | |
if (newValue == Characteristic.RemoteKey.PLAY_PAUSE) { | |
lgtv.request('ssap://media.controls/pause'); | |
} | |
callback(null); | |
}); | |
televisionService | |
.getCharacteristic(Characteristic.PictureMode) | |
.on('set', function(newValue, callback) { | |
console.log("set PictureMode => setNewValue: " + newValue); | |
callback(null); | |
}); | |
televisionService | |
.getCharacteristic(Characteristic.PowerModeSelection) | |
.on('set', function(newValue, callback) { | |
console.log("set PowerModeSelection => setNewValue: " + newValue); | |
if (newValue == Characteristic.PowerModeSelection.SHOW) { | |
if (mouseSocket != null) { | |
mouseSocket.send("button\n" + "name:" + "SETTINGS" + "\n" + "\n"); | |
} | |
} | |
callback(null); | |
}); | |
// Speaker | |
var speakerService = tv.addService(Service.TelevisionSpeaker) | |
speakerService | |
.setCharacteristic(Characteristic.Active, Characteristic.Active.ACTIVE) | |
.setCharacteristic(Characteristic.VolumeControlType, Characteristic.VolumeControlType.ABSOLUTE); | |
// TV widget sends a duplicate set here, which causes volume increments to be +/- 2 all the time. | |
// This ignores one of the values. | |
var ignoreVolumeDuplicate = false; | |
speakerService.getCharacteristic(Characteristic.VolumeSelector) | |
.on('set', function(newValue, callback) { | |
console.log("set VolumeSelector => setNewValue: " + newValue); | |
if (ignoreVolumeDuplicate) { | |
callback(null); | |
} else { | |
adjustVolume(newValue == Characteristic.VolumeSelector.INCREMENT, callback); | |
} | |
ignoreVolumeDuplicate = !ignoreVolumeDuplicate; | |
}); | |
speakerService.getCharacteristic(Characteristic.Volume) | |
.on('get', function(callback) { | |
callback(null, volume); | |
}) | |
.on('set', function(newValue, callback) { | |
setVolume(newValue, callback); | |
}); | |
speakerService.getCharacteristic(Characteristic.Mute) | |
.on('get', function(callback) { | |
callback(null, muted); | |
}) | |
.on('set', function(newValue, callback) { | |
setMute(newValue, callback); | |
}); | |
// Live TV | |
var inputLiveTV = tv.addService(Service.InputSource, "livetv", "Live TV"); | |
inputLiveTV | |
.setCharacteristic(Characteristic.Identifier, 0) | |
.setCharacteristic(Characteristic.ConfiguredName, "Live TV") | |
.setCharacteristic(Characteristic.IsConfigured, Characteristic.IsConfigured.CONFIGURED) | |
.setCharacteristic(Characteristic.InputSourceType, Characteristic.InputSourceType.TUNER); | |
// HDMI 1 | |
var inputHDMI1 = tv.addService(Service.InputSource, "hdmi1", "HDMI 1"); | |
inputHDMI1 | |
.setCharacteristic(Characteristic.Identifier, 1) | |
.setCharacteristic(Characteristic.ConfiguredName, "HDMI 1") | |
.setCharacteristic(Characteristic.IsConfigured, Characteristic.IsConfigured.CONFIGURED) | |
.setCharacteristic(Characteristic.InputSourceType, Characteristic.InputSourceType.HDMI); | |
// HDMI 2 | |
var inputHDMI2 = tv.addService(Service.InputSource, "hdmi2", "HDMI 2"); | |
inputHDMI2 | |
.setCharacteristic(Characteristic.Identifier, 2) | |
.setCharacteristic(Characteristic.ConfiguredName, "HDMI 2") | |
.setCharacteristic(Characteristic.IsConfigured, Characteristic.IsConfigured.CONFIGURED) | |
.setCharacteristic(Characteristic.InputSourceType, Characteristic.InputSourceType.HDMI); | |
// Netflix | |
var inputNetflix = tv.addService(Service.InputSource, "netflix", "Netflix"); | |
inputNetflix | |
.setCharacteristic(Characteristic.Identifier, 3) | |
.setCharacteristic(Characteristic.ConfiguredName, "Netflix") | |
.setCharacteristic(Characteristic.IsConfigured, Characteristic.IsConfigured.CONFIGURED) | |
.setCharacteristic(Characteristic.InputSourceType, Characteristic.InputSourceType.APPLICATION); | |
var inputSpotify = tv.addService(Service.InputSource, "spotify", "Spotify"); | |
inputSpotify | |
.setCharacteristic(Characteristic.Identifier, 4) | |
.setCharacteristic(Characteristic.ConfiguredName, "Spotify") | |
.setCharacteristic(Characteristic.IsConfigured, Characteristic.IsConfigured.CONFIGURED) | |
.setCharacteristic(Characteristic.InputSourceType, Characteristic.InputSourceType.APPLICATION); | |
var inputYouTube = tv.addService(Service.InputSource, "youtube", "YouTube"); | |
inputYouTube | |
.setCharacteristic(Characteristic.Identifier, 5) | |
.setCharacteristic(Characteristic.ConfiguredName, "YouTube") | |
.setCharacteristic(Characteristic.IsConfigured, Characteristic.IsConfigured.CONFIGURED) | |
.setCharacteristic(Characteristic.InputSourceType, Characteristic.InputSourceType.APPLICATION); | |
televisionService.addLinkedService(inputLiveTV); | |
televisionService.addLinkedService(inputHDMI1); | |
televisionService.addLinkedService(inputHDMI2); | |
televisionService.addLinkedService(inputNetflix); | |
televisionService.addLinkedService(inputSpotify); | |
televisionService.addLinkedService(inputYouTube); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment