LG WebOS Thin Q

It looks like this question gets asked every few years. I want to change my lighting scene when the TV is on and revert when it's off and maybe turn the TV off on no motion.
I've tried this

It discovers the TV but I can't figure out how to "pair" it.

This can't be that complicated, what am I missing??

Thanks for your help

Just read another thread, apparently you need the pairing key, which seems unavailable. Funny I'm looking for a deal on a 65" C3.
How do you like your LG TV?

PS- someone smarter than me maybe able to sniff out the pairing key using wireshark

I bought a 55" C2 a couple months ago. It has a beautiful picture. The WebOS and Remote take some getting used to.
I did have some weird issues at first, like no sound when I turned it on, but if I turned it off and back on it would be ok.
The software over the next two weeks updated itself 3 or 4 times and it straightened out and works fine now.

I do wonder why no one has dug into to WebOS? I would think managing your lighting scenes in relation to the TV would be a no brainer.

I don't have any LG TVs, but I do have a Thinq-capable washer and dryer. Not sure if they use the same platform, but I've found the Hubitat Thinq integration to be lacking. The Home Assistant integration works much better. If you use Home Assistant, maybe look there for a start and then share the devices back to Hubitat with the Device Bridge App.

1 Like

I've already have too many hubs, hubitat, hue, ecowitt, blink, etc. Seems crazy to have another just to tell hubitat the TV is on.

This can't be that hard

Try this to get the code How to add an LG TV to Control4 using their secret installer menu #con... | TikTok

I need a translator!!

@rob17 Have you checked if the Devices were created (I just set this up on a "new" second Hubitat and it did say it had to be paired (I couldn't find any way to do that either), but the tv device was created and able to be controlled by Hubitat (volume up and down, sending notifications all worked) I checked this with the Hubitat I already had setup and the pairing key's are different and both work)

Sorry I didn't see your post earlier
The devices are setup but the commands do not work

Here's a snip from the after I execute a volume down

For whatever reason, I came home today and it now works

Please forgive my poor and lack of instructions below,i promise i would post later how i make it to work,i need to remember exactly how,here is my experience

i have an Lg from 2018 (i think),with Lg WebOs and a newer Lg tv from 2021 (LG UP 8000 Series) and both of them work with the Driver ¨¨LG WEBOS TV¨¨ From ASJ drivers
(I Found it here,somewhere in hubitat community forum)
without need nothing special,but i remember the fist time you are setting it up,its a little bit tricky, but it can be done

i cant make ot wokr

I gave up

figured it out........
first add this code in a new APP CODE..

/**

  • LG Smart TV Discovery
  • Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
  • in compliance with the License. You may obtain a copy of the License at:
  •  http://www.apache.org/licenses/LICENSE-2.0
    
  • Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
  • on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
  • for the specific language governing permissions and limitations under the License.
  • Original Author: Sam Lalor
  • Ported to Hubitat by: Mike Magrann, 3/27/2019
  • Modified to support WebOS SSAP protocol: Cybrmage, 7/18/2019
  • portions of the websocket code modified from the Logitech Harmony plugin by Dan G Ogorchock

See Release Notes at the bottom
***********************************************************************************************************************/
public static String version() { return "v0.2.4" }

definition(
name: "LG WebOS TV Discovery",
namespace: "asj",
author: "Sam Lalor, Andrew SJ",
description: "Discovers an LG WebOS TV",
category: "My Apps",
iconUrl: "",
iconX2Url: "",
iconX3Url: ""
)

preferences
{
page(name:"televisionDiscovery", title:"LG TV Setup", content:"televisionDiscovery", refreshTimeout:5)
page(name:"televisionAuthenticate", title:"LG TV Pairing", content:"televisionAuthenticate", refreshTimeout:5)
}

def televisionDiscovery()
{
int tvRefreshCount = !state.bridgeRefreshCount ? 0 : state.bridgeRefreshCount as int
state.bridgeRefreshCount = tvRefreshCount + 1
def refreshInterval = 10

def options = televisionsDiscovered() ?: []
def numFound = options.size() ?: 0

if(!state.subscribe) {
    subscribe(location, null, deviceLocationHandler, [filterEvents:false])    
    state.subscribe = true
}

// Television discovery request every 15 seconds
if((tvRefreshCount % 5) == 0) {
    findTv()
}

return dynamicPage(name:"televisionDiscovery", title:"LG TV Search Started!", nextPage:"televisionAuthenticate", refreshInterval:refreshInterval, uninstall: true){
    section("Please wait while we discover your LG TV. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered."){
        input "selectedTv", "enum", required:false, title:"Select LG TV (${numFound} found)", multiple:false, options:options
    }
}

}

def televisionAuthenticate()
{
if (!selectedTv?.trim()) {
log.warn("No TV selected")
return televisionDiscovery()
}
def settingsInfo = selectedTv.split("!")
if (settingsInfo[3] == "NETCAST") {
tvRequestPairingKey()

    return dynamicPage(name:"televisionAuthenticate", title:"LG TV Pairing Started!", nextPage:"", install:true){
        section("We sent a pairing request to your TV. Please enter the pairing key and click Done."){
    	    input "pairingKey", "string", defaultValue:"DDTYGF", required:true, title:"Pairing Key", multiple:false
        }
    }
} else {
    return dynamicPage(name:"televisionAuthenticate", title:"LG TV Search Started!", nextPage:"", install:true){
        section("WebOS TVs can not be paired from the application. The driver will attempt to pair with the TV when it is initialized. Please authorize the pairing from the TV using the TV remote control. Please click Done."){}
    }
}

}

Map televisionsDiscovered()
{
def vbridges = getLGTvs()
def map = [:]
vbridges.each {
log.debug "Discovered List: $it"
def value = "$it"
def key = it.value

    if (key.contains("!")) {
        def settingsInfo = key.split("!")
        def deviceIp = convertHexToIP(settingsInfo[1])
		value = "LG TV (${deviceIp} - ${settingsInfo[3]})"
    }
    
    map["${key}"] = value
}
map

}

def installed()
{
log.debug "Installed with settings: ${settings}"

initialize()

}

def updated()
{
log.debug "Updated with settings: ${settings}"

initialize()

}

def initialize()
{
// Remove UPNP Subscription
unsubscribe()
state.subscribe = false

addDevice()

log.debug "Application Initialized"
log.debug "Selected TV: $selectedTv"

}

def addDevice()
{
def deviceSettings = selectedTv.split("!")
def macAddress = deviceSettings[0]
def ipAddressHex = deviceSettings[1]
def ipAddress = convertHexToIP(ipAddressHex)
def pairKey = "$pairingKey"
if (pairKey == null) { pairKey = "x"}
def tvType = deviceSettings[3]

def dni = "${ipAddressHex}_${macAddress}_${tvType}_${convertPortToHex(8080)}"

if (tvType == "WEBOS") { dni = "${ipAddressHex}${macAddress}${tvType}_${convertPortToHex(3000)}" }

log.debug("LG Smart TV Discovery - addDevice - ip: ${ipAddress} mac: ${macAddress} type: ${tvType} pairKey: ${pairKey} dni: ${dni}")

def d = getChildDevice(dni)
if(!d)
{
log.debug "Hub: " + location.hubs[0].id
log.debug "Ekim: " + ipAddress + pairKey
addChildDevice("asj", "LG WebOS TV", dni, null, [name: "LG WebOS TV", isComponent: true, label: "LG WebOS TV - $ipAddress"])
d = getChildDevice(dni)
d.updateSetting("televisionIp",[type:"text", value:ipAddress])
d.updateSetting("televisionMac",[type:"text", value:macAddress])
d.updateSetting("televisionType",[type:"text", value:tvType])
if (tvType == "NETCAST") {
d.updateSetting("pairingKey",[type:"text", value:"${pairKey}"])
}
// d.setParameters(ipAddress,macAddress,tvType,pairkey)

log.debug "created ${d.displayName} with id ${d.deviceNetworkId}"

}
else
{
log.debug "Device with id ${dni} already created - Updating"
d.updateSetting("televisionIp",[type:"text", value:ipAddress])
d.updateSetting("televisionMac",[type:"text", value:macAddress])
d.updateSetting("televisionType",[type:"text", value:tvType])
if (tvType == "NETCAST") {
d.updateSetting("pairingKey",[type:"text", value:"${pairKey}"])
}
// d.setParameters(ipAddress,macAddress,tvType,pairkey)
log.debug "updated ${d.displayName} with id ${d.deviceNetworkId}"
}
getLgDevice().updated()
}

def getLgDevice(){
log.debug("getLgDevice")
def childDevices = getChildDevices()
log.debug("childDevices: ${childDevices}")
def LgDevice = childDevices[0]
log.debug("childDevices: ${LgDevice}")
return LgDevice
}

def castLgDeviceStates(){
log.debug("Casting to State Variables")
state.televisionType = televisionType
log.debug("Setting state.televisionType ${state.televisionType}")
state.televisionIp = televisionIp
log.debug("Setting state.televisionIp ${state.televisionIp}")
state.televisionMac = televisionMac
log.debug("Setting state.televisionMac ${state.televisionMac}")
state.pairingKey = pairingKey ?: ""
log.debug("Setting state.pairingKey ${state.pairingKey}")
if (getLgDevice()){
log.debug("Found a Child LG ${getLgDevice().label}")
}
else{
log.debug("Did not find a Parent LG")
}
}

// Returns a list of the found LG TVs from UPNP discovery
def getLGTvs()
{
state.televisions = state.televisions ?: [:]
}

// Sends out a UPNP request, looking for the LG TV. Results are sent to [deviceLocationHandler]
private findTv()
{
// send ssdp search for NetCast TVs (2012 - 2015 models)
sendHubCommand(new hubitat.device.HubAction("lan discovery urn:schemas-udap:service:netrcu:1", hubitat.device.Protocol.LAN))
// send ssdp search for WebOS TVs (2016 and newer models)
sendHubCommand(new hubitat.device.HubAction("lan discovery urn:lge-com:service:webos-second-screen:1", hubitat.device.Protocol.LAN))
log.debug "Looking for TV's"
}

// Parses results from [findTv], looking for the specific UPNP result that clearly identifies the TV we can use
def deviceLocationHandler(evt)
{
log.debug "Device Location Event: " + evt.inspect()
def upnpResult = parseEventMessage(evt.description)
log.debug "upnp: $upnpResult"

def hub = evt?.hubId
log.debug "hub: $hub"

if (upnpResult?.ssdpUSN?.contains("urn:lge-com:service:webos-second-screen:1")) {
    // found a WebOS TV
    log.debug "Found WebOS TV: ${upnpResult}"
    state.televisions << [device:"${upnpResult.mac}!${upnpResult.ip}!${hub}!WEBOS"]
}
if (upnpResult?.ssdpPath?.contains("udap/api/data")) {
    // found a NetCast TV
    log.debug "Found TV: ${upnpResult}"
    state.televisions << [device:"${upnpResult.mac}!${upnpResult.ip}!${hub}!NETCAST"]
}

}

// Display pairing key on TV
private tvRequestPairingKey()
{
log.debug "Display pairing key"

def deviceSettings = selectedTv.split("!")
def ipAddressHex = deviceSettings[1]
def ipAddress = convertHexToIP(ipAddressHex)

if (deviceSettings[3] == "NETCAST") {
    // Netcast TV pairing
    def reqKey = "<?xml version=\"1.0\" encoding=\"utf-8\"?><auth><type>AuthKeyReq</type></auth>"

    def httpRequest = [
  	    method:		"POST",
        path: 		"/roap/api/auth",
        body:		"$reqKey",
        headers:	[
    	    HOST:			"$ipAddress:8080",
            "Content-Type":	"application/atom+xml",
        ]
    ]

    log.debug "HTTP REQUEST"
    log.debug "${shttpRequest}"

    def hubAction = new hubitat.device.HubAction(httpRequest)
    sendHubCommand(hubAction)
} else {
    // WebOS pairing - WebSockets can not be opened from an APP - Defer pairing to the device initialization
}

}

private def parseEventMessage(String description)
{
if (!description) {
return
}
def event = [:]
def parts = description.split(',')
parts.each { part ->
part = part.trim()
if (part.startsWith('devicetype:')) {
def valueString = part.split(":")[1].trim()
event.devicetype = valueString
}
else if (part.startsWith('mac:')) {
def valueString = part.split(":")[1].trim()
if (valueString) {
event.mac = valueString
}
}
else if (part.startsWith('networkAddress:')) {
def valueString = part.split(":")[1].trim()
if (valueString) {
event.ip = valueString
}
}
else if (part.startsWith('ssdpPath:')) {
def valueString = part.split(":")[1].trim()
if (valueString) {
event.ssdpPath = valueString
log.debug "Found ssdpPath: " + valueString
}
}
else if (part.startsWith('ssdpUSN:')) {
part -= "ssdpUSN:"
def valueString = part.trim()
if (valueString) {
event.ssdpUSN = valueString
log.debug "Found ssdpUSN: " + valueString
}
}
else if (part.startsWith('ssdpTerm:')) {
part -= "ssdpTerm:"
def valueString = part.trim()
if (valueString) {
event.ssdpTerm = valueString
log.debug "Found ssdpTerm: " + valueString
}
}
}

event

}

private Integer convertHexToInt(hex)
{
Integer.parseInt(hex,16)
}

private String convertHexToIP(hex)
{
[convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".")
}

private String convertIPtoHex(ipAddress)
{
String hex = ipAddress.tokenize( '.' ).collect { String.format( '%02x', it.toInteger() ) }.join()
trace("IP address entered is $ipAddress and the converted hex code is $hex")
return hex
}

private String convertPortToHex(port)
{
String hexport = port.toString().format( '%04X', port.toInteger() )
return hexport
}
private removeChildDevices(delete) {
delete.each {deleteChildDevice(it.deviceNetworkId)}
}

def uninstalled() {
removeChildDevices(getChildDevices())
}

/***********************************************************************************************************************
*

  • Release Notes
  • 0.2.4
  • Fixed - state machine loosing sync with device
  • Fixed - more reliable power off detection
  • Added - better websocket state handling
  • Added - Live TV data handling
  • 0.2.3
  • Fixed - spurious websocket open/close cycling
  • 0.2.2
  • Added - WebOS TV Notification, Status subscriptions, Event propagation, setVolume/setLevel support, Poll device every
  •     10 minute to improve connection stability
    
  • 0.2.1
  • fixed - parameters not properly passed to driver
  • 0.2.0
  • Modified to support LG WebOS Smart Tv
  • 0.1.1
  • Ported LG Smart Tv from Smarththings

***********************************************************************************************************************/

then go to the driver code window. creat a new driver and paste this code

/**

  • Copyright (C) Sebastian YEPES
  • Original Authors: Sam Lalor, Andrew Stanley-Jones
  • Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
  • in compliance with the License. You may obtain a copy of the License at:
  • Apache License, Version 2.0
  • Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
  • on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
  • for the specific language governing permissions and limitations under the License.

*/

import groovy.json.JsonSlurper
import groovy.json.JsonOutput
import groovy.transform.Field

@Field String VERSION = "1.0.0"

@Field List LOG_LEVELS = ["error", "warn", "info", "debug", "trace"]
@Field String DEFAULT_LOG_LEVEL = LOG_LEVELS[2]
@Field static Map callbacks = [:]

metadata {
definition (name: "LG WebOS TV", namespace: "syepes", author: "Sebastian YEPES", importUrl: "https://raw.githubusercontent.com/syepes/Hubitat/master/Drivers/LG/LG%20WebOS%20TV.groovy") {
capability "Initialize"
capability "TV"
capability "AudioVolume"
capability "Refresh"
capability "Switch"
capability "Notification"

command "off"
command "refresh"
command "refreshInputList"
command "getMouseURI"
command "externalInput", ["string"]
command "sendJson", ["string"]
command "myApps"
command "ok"
command "home"
command "left"
command "right"
command "up"
command "down"
command "back"
command "enter"
command "notificationIcon", ["string", "string"]
command "setIcon", ["string", "string"]
command "clearIcons"
command "testWebSocketReply", ["string"]

attribute "availableInputs", "list"

attribute "channelDesc", "string"
attribute "channelName", "string"
attribute "channelFullNumber", "string"

}

preferences {
section { // General
input name: "logLevel", title: "Log Level", type: "enum", options: LOG_LEVELS, defaultValue: DEFAULT_LOG_LEVEL, required: false
input name: "logDescText", title: "Log Description Text", type: "bool", defaultValue: false, required: false
}
section { // Configuration
input name: "televisionIp", type: "text", title: "Television IP Address", defaultValue: "", required: true
input name: "televisionMac", type: "text", title: "Television MAC Address", defaultValue: "", required: true
input name: "pairingKey", type: "text", title: "Pairing Key", required: true, defaultValue: ""
input name: "retryDelay", title: "Device Reconnect delay", type: "enum", options: [["5":"Retry every 5 seconds"], ["10":"Retry every 10 seconds"], ["15":"Retry every 15 seconds"], ["30":"Retry every 30 seconds"], ["45":"Retry every 45 seconds"], ["60":"Retry every minute"], ["120":"Retry 2 minute"], ["300":"Retry every 5 minutes"], ["600":"Retry every 10 minutes"]], defaultValue: 60
}
}
}

def installed() {
logger("debug", "installed() - settings: ${settings?.inspect()}")
// initialize()
}

def refresh() {
logger("debug", "refresh()")
state.deviceInfo = null
state.televisionModel = null
state.nameToInputId = null

webosRegister()
}

def webosRegister() {
logger("debug", "webosRegister() - pairing key: ${state.pairingKey}")
state.pairFailCount = 0

def payload = [
pairingType: "PROMPT",
forcePairing: false,
'client-key': state?.pairingKey,
manifest: [
appVersion: "1.1",
signed: [
localizedVendorNames: [ "": "LG Electronics" ],
appId: "com.lge.test",
created: "20140509",
permissions: [
"TEST_SECURE",
"CONTROL_INPUT_TEXT",
"CONTROL_MOUSE_AND_KEYBOARD",
"READ_INSTALLED_APPS",
"READ_LGE_SDX",
"READ_NOTIFICATIONS",
"SEARCH",
"WRITE_SETTINGS",
"WRITE_NOTIFICATION_ALERT",
"CONTROL_POWER",
"READ_CURRENT_CHANNEL",
"READ_RUNNING_APPS",
"READ_UPDATE_INFO",
"UPDATE_FROM_REMOTE_APP",
"READ_LGE_TV_INPUT_EVENTS",
"READ_TV_CURRENT_TIME",
],
localizedAppNames: [
"": "LG Remote App",
"ko-KR": "리모컨 앱",
"zxx-XX": "ЛГ Rэмotэ AПП",
],
vendorId: "com.lge",
serial: "2f930e2d2cfe083771f68e4fe7bb07",
],
permissions: [
"LAUNCH",
"LAUNCH_WEBAPP",
"APP_TO_APP",
"CLOSE",
"TEST_OPEN",
"TEST_PROTECTED",
"CONTROL_AUDIO",
"CONTROL_DISPLAY",
"CONTROL_INPUT_JOYSTICK",
"CONTROL_INPUT_MEDIA_RECORDING",
"CONTROL_INPUT_MEDIA_PLAYBACK",
"CONTROL_INPUT_TV",
"CONTROL_POWER",
"READ_APP_STATUS",
"READ_CURRENT_CHANNEL",
"READ_INPUT_DEVICE_LIST",
"READ_NETWORK_STATE",
"READ_RUNNING_APPS",
"READ_TV_CHANNEL_LIST",
"WRITE_NOTIFICATION_TOAST",
"READ_POWER_STATE",
"READ_COUNTRY_INFO",
],
manifestVersion: 1,
signatures: [
[
signatureVersion: 1,
signature: "eyJhbGdvcml0aG0iOiJSU0EtU0hBMjU2Iiwia2V5SWQiOiJ0ZXN0LXNpZ25pbmctY2VydCIsInNpZ25hdHVyZVZlcnNpb24iOjF9.hrVRgjCwXVvE2OOSpDZ58hR+59aFNwYDyjQgKk3auukd7pcegmE2CzPCa0bJ0ZsRAcKkCTJrWo5iDzNhMBWRyaMOv5zWSrthlf7G128qvIlpMT0YNY+n/FaOHE73uLrS/g7swl3/qH/BGFG2Hu4RlL48eb3lLKqTt2xKHdCs6Cd4RMfJPYnzgvI4BNrFUKsjkcu+WD4OO2A27Pq1n50cMchmcaXadJhGrOqH5YmHdOCj5NSHzJYrsW0HPlpuAx/ECMeIZYDh6RMqaFM2DXzdKX9NmmyqzJ3o/0lkk/N97gfVRLW5hA29yeAwaCViZNCP8iC9aO0q9fQojoa7NQnAtw==",
],
]
]
]

sendWebosCommand(type: "register", payload: payload, callback: { json ->
logger("trace", "webosRegister() - json: ${json?.inspect()}")

if (json?.type == "registered") {
  pKey = json.payload["client-key"]
  if (pKey != null) {
    logger("debug", "webosRegister() - received registered client-key: ${pKey}")

    state.pairingKey = pKey
    device.updateSetting("pairingKey",[type:"text", value:"${pKey}"])
    runInMillis(10, webosSubscribeToStatus)
    runInMillis(25, getMouseURI)
    // Hello doesn't seem to do anything?
    if (!state.deviceInfo) runInMillis(50, sendHello)
    if (!state.televisionModel) runInMillis(75, sendRequestInfo)
    if (!state.nameToInputId) runInMillis(100, refreshInputList)
    if (!state.serviceList) runInMillis(125, getServiceList)
  }
  return true
} else if (json?.type == "response") {
    return false
}

})
}

def sendHello() {
logger("debug", "sendHello()")
sendWebosCommand(type: "hello", id: "hello")
}

def handler_hello(data) {
logger("debug", "handler_hello() - data: ${data?.inspect()}")
state.deviceInfo = data
}

def sendRequestInfo() {
logger("debug", "sendRequestInfo()")

sendWebosCommand(uri: "system/getSystemInfo", callback: { json ->
logger("trace", "sendRequestInfo() - json: ${json?.inspect()}")
state.televisionModel = json.payload?.modelName
state.televisionReceiver = json.payload?.receiverType
})
}

def refreshInputList() {
logger("debug", "refreshInputList() - current list size: ${state.nameToInputId?.size()}")

sendWebosCommand(uri: "com.webos.applicationManager/listLaunchPoints", payload: , callback: { json ->
logger("trace", "refreshInputList() - json: ${json?.inspect()}")
def inputList =
def nameToInputId = [:]
json?.payload?.launchPoints.each { app ->
logger("debug", "refreshInputList() - App Name: ${app.title}, App: ${app}")
inputList += app.title
nameToInputId[app.title] = app.id
}

state.nameToInputId = nameToInputId
state.inputList = inputList

sendWebosCommand(uri: 'tv/getExternalInputList', callback: { jsonExt ->
  logger("trace", "refreshInputList() - jsonExt: ${jsonExt?.inspect()}")

  jsonExt?.payload?.devices?.each { device ->
    logger("debug", "refreshInputList() - Device: ${device?.label}")
    inputList += device.label
    nameToInputId[device.label] = device.appId
  }
  state.nameToInputId = nameToInputId
  state.inputList = inputList
  logger("debug", "refreshInputList() - Inputs: ${state.inputList}")

  sendEvent(name: "availableInputs", value: inputList);
})

})
}

def getMouseChild() {
logger("debug", "getMouseChild() - televisionIp: ${televisionIp}")

try {
def mouseDev = getChildDevice("LG_TV_Mouse_${televisionIp}")
if(!mouseDev) mouseDev = addChildDevice("syepes", "LG WebOS Mouse", "LG_TV_Mouse_${televisionIp}")
return mouseDev
} catch(e) {
logger("error", "getMouseChild() - Failed to get mouse dev: ${e}")
}
return null
}

def getMouseURI() {
logger("debug", "getMouseURI()")

def mouseDev = getMouseChild()

sendWebosCommand(uri: "com.webos.service.networkinput/getPointerInputSocket", payload: , callback: { json ->
logger("trace", "getMouseURI() - json: ${json?.inspect()}")

if (json?.payload?.socketPath) {
  logger("debug", "getMouseURI() - Send Mouse driver URI: ${json.payload.socketPath}")
  mouseDev?.setMouseURI(json.payload.socketPath)
}

})
}

def sendJson(String json) {
sendCommand(json);
}

def powerEvent(String onOrOff, String type = "digital") {
logger("debug", "powerEvent() - onOrOff: ${onOrOff}, type: ${type}")

def descriptionText = "is ${onOrOff}"
if (state.power != onOrOff){
logger("info", "powerEvent() - ${descriptionText} [$type]")
}

state.power = onOrOff
sendEvent(name: "switch", value: onOrOff, descriptionText: descriptionText, type: type)
if (type == "physical") {
sendEvent(name: "power", value: onOrOff, descriptionText: descriptionText, type: type)
}

if ((onOrOff == "off") && (type == "physical")) {
sendEvent(name: "channelDesc", value: "[off]", descriptionText: descriptionText)
sendEvent(name: "channelName", value: "[off]", descriptionText: descriptionText)
sendEvent(name: "input", value: "[off]", descriptionText: descriptionText)
// Socket status should follow the system reported status
interfaces.webSocket.close()
}
}

def initialize() {
logger("debug", "initialize() - ip: ${televisionIp}, mac: ${televisionMac}, key: ${pairingKey}, debug: ${debug}, logText: ${descriptionText}")
logger("debug", "initialize() - settings: ${settings.inspect()}")

// Websocket has closed/errored, erase all callbacks
callbacks = [:]

// Set some basic state, clear channel info
state.sequenceNumber = 1
state.lastChannel = [:]
state.pairFailCount = 0

// When reconnectPending is true it stops reconnectWebsocket
// from rescheudling initialize()
state.reconnectPending = false
state.webSocket = "initialize"

unschedule()

def mouseDev = getMouseChild()

interfaces.webSocket.close()

if(!televisionMac) {
def mac = getMACFromIP(televisionIp)
if (mac){
device.updateSetting("televisionMac",[value:mac,type:"string"])
}
}

try {
logger("info", "initialize() - Connecting secure websocket to: wss://${televisionIp}:3001/")
interfaces.webSocket.connect("wss://${televisionIp}:3001/", ignoreSSLIssues: true)
} catch(e) {
logger("error", "initialize() - WebSocket connect ${e?.inspect()}")
}
}

def webSocketStatus(String status){
logger("debug", "webSocketStatus() - status: [${status}], State: [${state.webSocket}]")

if(status.startsWith('failure: ')) {
//logger("error", "webSocketStatus() - ${status}")

if ((status == "failure: No route to host (Host unreachable)") || (status == "failure: connect timed out") || status.startsWith("failure: Failed to connect") || status.startsWith("failure: sent ping but didn't receive pong")) {
  logger("info", "webSocketStatus() - WebSocket is closed")
  powerEvent("off", "physical")
}
state.webSocket = "closed"
reconnectWebSocket()

} else if(status == 'status: open') {
logger("info", "webSocketStatus() - WebSocket is open")

// success! reset reconnect delay
powerEvent("on", "physical")
state.webSocket = "open"
webosRegister()
state.reconnectDelay = 2

} else if (status == "status: closing"){
logger("info", "webSocketStatus() - WebSocket connection closing")
unschedule()

if (state.webSocket == 'initialize') {
  logger("info", "webSocketStatus() - Ignoring WebSocket close due to initialization")
} else {
  if (state.power == "on") {
    // TV should be on and reachable - try to reconnect
    reconnectWebSocket(1)
  } else {
    reconnectWebSocket()
  }
}
state.webSocket = "closed"

} else {
logger("error", "webSocketStatus() - WebSocket error, reconnecting")
powerEvent("off", "physical")
state.webSocket = "closed"
reconnectWebSocket()
}
}

def reconnectWebSocket(delay = null) {
logger("debug", "reconnectWebSocket() - delay: ${delay}")

// first delay is 2 seconds, doubles every time
if (state.reconnectPending == true) {
logger("warn", "reconnectWebSocket() - Rejecting additional reconnect request")
return
}
delay = delay ?: state.reconnectDelay
state.reconnectDelay = delay * 2
settings_retryDelay = settings.retryDelay.toInteger()
// don't let delay get too crazy, max it out at user setting
if (state.reconnectDelay > settings_retryDelay) {
state.reconnectDelay = settings_retryDelay
}

//If the TV is offline, give it some time before trying to reconnect
state.reconnectPending = true
runIn(delay, initialize)
}

def updated() {
logger("debug", "updated() - ip: ${settings.televisionIp}, mac: ${settings.televisionMac}, key: ${settings.pairingKey}")
initialize()
}

def logsStop() {
logger("debug", "logsStop()")
}

def setParameters(String IP, String MAC, String TVTYPE, String KEY) {
logger("debug", "setParameters() - ip: ${IP}, mac: ${MAC}, type: ${TVTYPE}, key: ${KEY}")

state.televisionIp = IP
device.updateSetting("televisionIp",[type:"text", value:IP])

state.televisionMac = MAC
device.updateSetting("televisionMac",[type:"text", value:MAC])
}

def testWebSocketReply(String data) {
logger("debug", "testWebSocketReply() - data: ${data}")
parse(data)
}

// parse events into attributes
def parse(String description) {
logger("debug", "parse() - description: ${description}")

// parse method is shared between HTTP and Websocket implementations
def json = null
try {
json = new JsonSlurper().parseText(description)
if(json == null){
logger("warn", "parse() - String description not parsed")
return
}
} catch(e) {
logger("error", "parse() - Failed to parse json e = ${e}")
return
}

if (this."handler_${json.id}") {
  this."handler_${json.id}"(json.payload)
} else if (this."handler_${json.type}") {
  this."handler_${json.type}"(json.payload)
} else if (callbacks[json.id]) {
  logger("debug", "parse() - callback for json.id: " + json.id)

  callbacks[json.id].delegate = this
  callbacks[json.id].resolveStrategy = Closure.DELEGATE_FIRST
  def done = callbacks[json.id].call(json)
  if ((done instanceof Boolean) && (done == false)) {
    logger("debug", "parse() - callback[${json.id}]: being kept, done is false")
  } else {
    callbacks[json.id] = null
  }

} else if (json?.type == "error") {
if (json?.id == "register_0") {
if (json?.error.take(3) == "403") {
// 403 error cancels the pairing process
pairingKey = ""
state.pairFailCount = state.pairFailCount ? state.pairFailCount + 1 : 1
logger("debug", "parse() - received register_0 error: ${json.error} fail count: ${state.pairFailCount}")
if (state.pairFailCount < 6) { webosRegister() }
}
} else {
if (json?.error.take(3) == "401") {
logger("warn", "parse() - received error: ${json.error}")
//if (state.registerPending == false) { webosRegister() }
//webosRegister()
}
}
}
}

def webosSubscribeToStatus() {
logger("debug", "webosSubscribeToStatus()")

sendWebosCommand(uri: 'audio/getStatus', type: 'subscribe', id: 'audio_getStatus')
sendWebosCommand(uri: 'com.webos.applicationManager/getForegroundAppInfo', type: 'subscribe', id: 'getForegroundAppInfo')
sendWebosCommand(uri: 'tv/getChannelProgramInfo', type: 'subscribe', id: 'getChannelProgramInfo')
//sendCommand('{"type":"subscribe","id":"status_%d","uri":"ssap://com.webos.applicationManager/getForegroundAppInfo"}')
sendCommand('{"type":"subscribe","id":"status_%d","uri":"ssap://com.webos.service.tv.time/getCurrentTime"}')

// schedule a poll every 10 minutes to help keep the websocket open
// runEvery10Minutes("webosSubscribeToStatus")
}

def getServiceList() {
logger("debug", "getServiceList()")

state.remove('serviceList')
state.serviceList =
sendWebosCommand(uri: 'api/getServiceList', callback: { json ->
logger("trace", "getServiceList() - json: ${json.serviceList}")
json?.payload?.services.each { service ->
state.serviceList << service?.name
}
logger("debug", "getServiceList() - Services: ${state.serviceList}")
})
}

def handler_audio_getStatus(data) {
logger("debug", "handler_audio_getStatus() - data: ${data?.inspect()}")
def descriptionText = "volume is ${data.volume}"

logger("info", "${descriptionText}")
sendEvent(name: "volume", value: data.volume, descriptionText: descriptionText)

}

def handler_getForegroundAppInfo(data) {
logger("debug", "handler_getForegroundAppInfo() - data: ${data?.inspect()}")

// Some TVs send this message when powering off
// data: [subscribed:true, appId:, returnValue:true, windowId:, processId:]
// json for testing: {"type":"response","id":"getForegroundAppInfo","payload":{"subscribed":true,"appId":"","returnValue":true,"windowId":"","processId":""}}
if (!data.appId && !data.processId) {
powerEvent("off", "physical")
logger("info", "handler_getForegroundAppInfo() - Received POWER DOWN notification")
return
}

def appId = data.appId
def niceName = appId
state.nameToInputId.each { name, id ->
if (appId == id) niceName = name
}

def descriptionText = "channelName is ${niceName}"
logger("info", "${descriptionText}")

sendEvent(name: "channelName", value: niceName, descriptionText: descriptionText)
if (niceName != "LiveTV") sendEvent(name: "channelDesc", value: "[none]")

state.lastApp = niceName
if (niceName == "LiveTV") {
runIn(3, "getChannelInfo")
} else {
state.lastChannel = [:]
}
}

def getChannelInfo() {
logger("debug", "getChannelInfo()")
sendWebosCommand(uri: 'tv/getChannelProgramInfo', id: 'getChannelProgramInfo')
}

def handler_getChannelProgramInfo(data) {
logger("debug", "handler_getChannelProgramInfo() - data: ${data?.inspect()}")

if (data.errorCode) {
def lastChannel = [:]
lastChannel.description = "${data.errorText}"
state.lastChannel = lastChannel
sendEvent(name: "channelDesc", value: lastChannel.channelDesc)
// Resubscribe, after error subscription appears to be ended
if (device.currentChannelName == "LiveTV") {
runIn(15, "getChannelInfo")
}
return
}

def lastChannel = [
description: "${data.channel?.channelNumber}/${data.channel?.channelName}",
number: data.channel?.channelNumber,
majorNumber: data?.channel?.majorNumber ?: data.channel?.channelNumber,
minorNumber: data?.channel?.minorNumber ?: 0,
name: data.channel?.channelName ?: "",
]

state.lastChannel = lastChannel
sendEvent(name: "channelDesc", value: lastChannel.description)
// This is defined as a number, not a decimal so send the major number
def descriptionText = "full channel number is ${lastChannel?.majorNumber}-${lastChannel?.minorNumber}"
sendEvent(name: "channel", value: lastChannel?.majorNumber)
logger("info", "${descriptionText}")

descriptionText = "channelName is ${lastChannel.name}"
sendEvent(name: "channelName", value: lastChannel.name, descriptionText: descriptionText)
logger("info", "${descriptionText}")
}

def genericHandler(json) {
logger("debug", "genericHandler() - json: ${data?.inspect()}")
}

def deviceNotification(String notifyMessage) {
logger("debug", "deviceNotification() - notifyMessage: ${notifyMessage?.inspect()}")

def icon_info = notifyMessage =~ /^[(.+?)](.+)/
logger("debug", "deviceNotification() - new message $notifyMessage found icon: ${icon_info != null}")

if (!icon_info) {
sendWebosCommand(uri: "system.notifications/createToast", payload: [message: notifyMessage])
} else {
logger("debug", "deviceNotification() - icon_name match ${icon_name}")
def icon_name = icon_info[0][1]
def msg = icon_info[0][2]
notificationIcon(msg, icon_name)
}
}

def setIcon(String icon_name, String data) {
logger("debug", "setIcon() - icon_name: ${icon_name?.inspect()}, data: ${data?.inspect()}")
state.icon_data[icon_name] = data
}

def clearIcons() {
logger("debug", "clearIcons()")
state.icon_data = [:]
}

def notificationIcon(String notifyMessage, String icon_name) {
logger("debug", "notificationIcon() - notifyMessage: ${notifyMessage?.inspect()}, icon_name: ${icon_name?.inspect()}")

def base_url = "https://raw.githubusercontent.com/pasnox/oxygen-icons-png/master/oxygen/32x32"
def icon_extention = "png"
def full_uri = "${base_url}/${icon_name}.png"

if (!state.icon_data) {
state.icon_data = [:]
}

if (!state.icon_data[icon_name]) {
try {
logger("info", "notificationIcon() - asking for ${full_uri}")

  def start_time = now()
  httpGet(full_uri, { resp ->
    handleIconResponse(resp, [
      icon_extention: icon_extention,
      icon_name: icon_name,
      notify_message: notifyMessage,
      start_time: start_time
    ])
  })

} catch (Exception e) {
  logger("warn", "notificationIcon() - asking for ${full_uri}")
  deviceNotification("<Failed to find icon: ${e.message}>${notifyMessage}")
}

} else {
String icon = state.icon_data[icon_name]
logger("debug", "notificationIcon() - icon size: ${icon.size()} sending notifcation: ${notifyMessage} name: ${icon_name} icon: ${state.icon_data[icon_name]}")
sendWebosCommand(uri: "system.notifications/createToast", payload: [message: notifyMessage, iconData: icon, iconExtension: icon_extention])
}
}

def handleIconResponse(resp, data) {
logger("debug", "handleIconResponse() - resp: ${resp?.inspect()}, data: ${data?.inspect()}")

int n = resp.data?.available()
logger("debug", "handleIconResponse() - resp.status: ${resp.status} took: ${now() - data.start_time}ms size: ${n}")

byte bytes = new byte[n]
resp.data.read(bytes, 0, n)
def base64String = bytes.encodeBase64().toString()
logger("debug", "handleIconResponse() - size of b64: ${base64String.size()}")

state.icon_data[data.icon_name] = base64String
notificationIcon(data.notify_message, data.icon_name)
}

def on() {
logger("debug", "on()")

powerEvent("on")
def mac = settings.televisionMac ?: state.televisionMac
if (!mac) {
logger("error", "on() - No mac address know for TV, can't send wake on lan")
return
}

logger("info", "on() - Sending Magic Packet to: ${mac}")
def result = new hubitat.device.HubAction (
"wake on lan ${mac}",
hubitat.device.Protocol.LAN,
null,[secureCode: “0000”]
)

logger("debug", "on() - Sending Magic Packet to: ${mac}, result: ${result}")
return result
}

def off() {
logger("debug", "off()")
powerEvent("off")
sendWebosCommand(uri: 'system/turnOff')
}

def channelUp() {
logger("debug", "channelUp()")
sendWebosCommand(uri: 'tv/channelUp')
}

def channelDown() {
logger("debug", "channelDown()")
sendWebosCommand(uri: 'tv/channelDown')
}

// handle commands
def volumeUp() {
logger("debug", "volumeUp()")
sendWebosCommand(uri: 'audio/volumeUp')
}

def volumeDown() {
logger("debug", "volumeDown()")
sendWebosCommand(uri: 'audio/volumeDown')
}

def setVolume(level) {
logger("debug", "setVolume() - level: ${level}")
sendWebosCommand(uri: 'audio/setVolume', payload: [volume: level])
}

def setLevel(level) {
logger("debug", "setLevel() - level: ${level}")
setVolume(level)
}

def sendMuteEvent(muted) {
logger("debug", "sendMuteEvent() - muted: ${muted}")

def descriptionText = "mute is ${muted}"
logger("info", "${descriptionText}")
sendEvent(name: "mute", value: muted, descriptionText: descriptionText)
}

def unmute() {
logger("debug", "unmute()")
sendWebosCommand(uri: 'audio/setMute', payload: [mute: false], callback: { json ->
logger("trace", "unmute() - json: ${json}")
if (json?.payload?.returnValue) {
sendMuteEvent("unmuted")
}
})
}

def mute() {
logger("debug", "mute()")
sendWebosCommand(uri: 'audio/setMute', payload: [mute: true], callback: { json ->
logger("trace", "mute() - json: ${json}")
if (json?.payload?.returnValue) {
sendMuteEvent("muted")
}
})
}

def externalInput(String input) {
logger("debug", "externalInput() - input: ${input}")

if (state.nameToInputId && state.nameToInputId[input]) {
input = state.nameToInputId[input]
}
sendWebosCommand(uri: "system.launcher/launch", payload: [id: input], callback: { json ->
logger("trace", "externalInput() - json: ${json}")
})
}

def enter() {
logger("debug", "enter()")

def mouseDev = getMouseChild()
mouseDev?.sendButton('ENTER')
//return sendWebosCommand(uri: "com.webos.service.ime/sendEnterKey")
}

def back() {
logger("debug", "back()")
def mouseDev = getMouseChild()
mouseDev?.sendButton('BACK')
}

def up() {
logger("debug", "up()")
def mouseDev = getMouseChild()
mouseDev?.sendButton('UP')
}

def down() {
logger("debug", "down()")
def mouseDev = getMouseChild()
mouseDev?.sendButton('DOWN')
}

def left() {
logger("debug", "left()")
def mouseDev = getMouseChild()
mouseDev?.left()
}

def right() {
logger("debug", "right()")
def mouseDev = getMouseChild()
mouseDev?.right()
}

def myApps() {
logger("debug", "myApps()")
sendWebosCommand(uri: 'system.launcher/launch', payload: [id: 'com.webos.app.discovery'])
}

def play() {
logger("debug", "play()")
sendWebosCommand(uri: "media.controls/play")
}

def pause() {
logger("debug", "pause()")
sendWebosCommand(uri: "media.controls/pause")
}

def home() {
logger("debug", "home()")
logger("debug", "home() - OLD Inputs: ${state.inputList} total length: ${state.toString().length()}")

state.remove('serviceList')
state.serviceList =
sendWebosCommand(uri: 'api/getServiceList', callback: { json ->
logger("trace", "home() - getServiceList: ${json?.payload}")

json?.payload?.services.each { service ->
  state.serviceList << service?.name
}
logger("info", "home() - Services: ${state.serviceList}")

})
}

def sendCommand(cmd) {
logger("debug", "sendCommand() - cmd: ${cmd?.inspect()}")

def msg = String.format(cmd,state.sequenceNumber)
logger("debug", "sendCommand() - msg: ${msg?.inspect()}")

try {
// send the command
interfaces.webSocket.sendMessage(msg)
} catch (Exception e) {
logger("warn", "sendCommand() - Exception ${e}")
}
state.sequenceNumber++
}

def sendWebosCommand(Map params) {
logger("debug", "sendWebosCommand() - params: ${params?.inspect()}")

def id = params.id ?: ("command_" + state.sequenceNumber++)
def cb = params.callback ?: { genericHandler(it) }
def message_data = [
'id': id,
'type': params.type ?: "request",
]

if (params.uri) {
message_data.uri = "ssap://" + params.uri
}

if (params.payload) {
message_data.payload = params.payload
}

def json = JsonOutput.toJson(message_data)
logger("debug", "sendWebosCommand() - Sending: ${json} storing callback: ${id}")

callbacks[id] = cb
interfaces.webSocket.sendMessage(json)
logger("debug", "sendWebosCommand() - Sending json: ${json}")
}

private void parseStatus(state, json) {
logger("debug", "parseStatus() - state: ${state?.inspect()}, json: ${json?.inspect()}")

def rResp = false
if ((state.power == "off") && !(json?.payload?.subscribed == true)) {
// when TV has indicated power off, do not process status messages unless they are subscriptions
logger("debug", "parseStatus() - ignoring unsubscribed status updated during power off... message: ${json}")
return
}

if (json?.payload?.returnValue == true) {
// The last (valid) message sent by the TV when powering off is a subscription response for foreground app status with appId, windowId and processID all NULL
if (json?.payload?.subscribed) {
logger("debug", "parseStatus() - appID: "+ (description.contains("appId")?"T":"F") +", windowId: "+ (description.contains("windowId")?"T":"F") +", processId: "+ (description.contains("processId")?"T":"F"))

  if (description.contains("appId") && description.contains("windowId") && description.contains("processId")) {
    if ((json?.payload?.appId == null) || (json?.payload?.appId == "")) {
      // The TV is powering off - change the power state, but leave the websocket to time out
      powerEvent("off", "physical")
      logger("info", "Received POWER DOWN notification")
    }
  }
}

}
}

/**

  • @param level Level to log at, see LOG_LEVELS for options
  • @param msg Message to log
    */
    private logger(level, msg) {
    if (level && msg) {
    Integer levelIdx = LOG_LEVELS.indexOf(level)
    Integer setLevelIdx = LOG_LEVELS.indexOf(logLevel)
    if (setLevelIdx<0) {
    setLevelIdx = LOG_LEVELS.indexOf(DEFAULT_LOG_LEVEL)
    }
    if (levelIdx<= setLevelIdx) {
    log."${level}" "${device.displayName} ${msg}"
    }
    }
    }

finally go to the app that you created and run it.. make sure the TV is on... run the app.. select the TV.. click done..

the device is created for you and works.. let me know of you need anything

finally the tv is goin to ask you to let the remote control from hubitat control your TV

Thanks, i'll give it a try!

did it work?

Haven't had a chance to try it out