Issue with LG smart tv

Continuing the discussion from [PORT] LG Smart TV Discovery 2012+:

Mine says " 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.
"
Then doesn't add any tv to my hubitat. Nothing pops up on my screen.

1 Like

I have a similar issue with my l
LG C2. If I use the older app it will discover the TV and create the device, but the TV pairing screen does not come up. If I use the new app available in the HPM, it will not create the device at all. This is only an issue with my C2, which had previously worked. My CX still connect correctly.

I'm also having the same issue. App finds TV (G3) but TV does not prompt for connection. I did have it working until yesterday when I updated the TV firmware, now nothing. Did either of you get this sorted?

No. I can still connect to my CX just fine, but after several attempts, reinstalled, updates, backdates, I even fully reset the TV, I finally gave up on the C2. It does connect just fine to my home assistant though. So I just use that to automate the TV now.

Hey. Cheers for the reply. I now have it working through Home Assistant which is good. Apparently the newer firmware on the TV requires a secure websocket which is why the TV is not prompting for authorisation.

@syepes released a version of this app and driver a couple years ago. Perhaps the code can be updated to handle new requirements by LG to use a secure websocket connection.

Using home assistant has become my work around as well. Unfortunately it means I cannot keep all of my automations in Hubitat. Home assistant must now take care of them. Furthermore, my CX has also now been updated to include this requirement. It no longer responds to requests through this app either.

I've managed to get it working by updating the driver code to use secure websocket, pretty easy fix, ping me if interested

Absolutely interested

Here's the modified driver code. enjoy :slight_smile: let me know if you face any issues

/**
 *  LG Smart TV Device Type
 *
 *
 *  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.5"  }

import groovy.json.JsonSlurper

metadata {
	definition (name: "LG Smart TV", namespace: "ekim", author: "Sam Lalor")
	{
		capability "Initialize"
		capability "TV"
		capability "AudioVolume"
		capability "Music Player"
		capability "Refresh"
		capability "Switch"
		capability "Notification"

		command "on"
		command "off"
		command "refresh"
		command "externalInput"
		command "back"
		command "up"
		command "down"
		command "left"
		command "right"
		command "myApps"
		command "ok"
		command "home"
//        command "wake"

		attribute "CurrentInput", "string"
		attribute "sessionId", "string"
//		attribute "mute", "string"
		
		attribute "channelDesc", "string"
		attribute "channelName", "string"
		attribute "channelData", "string"

	}

	preferences {
		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: "televisionType", type: "text", title: "Television Type (NETCAST or WEBOS)", defaultValue: "", required: true
		input name: "pairingKey", type: "text", title: "Pairing Key", required: true, defaultValue: ""
		input ("debug", "bool", title: "Enable debug logging", defaultValue: false)
		input ("descriptionText", "bool", title: "Enable description text logging", defaultValue: true)
		input ("channelDetail", "bool", title: "Enable verbose channel data (WebOS Only)", defaultValue: false)
		def reconnectRate = [:]
		reconnectRate << ["5" : "Retry every 5 seconds"]
		reconnectRate << ["10" : "Retry every 10 seconds"]
		reconnectRate << ["15" : "Retry every 15 seconds"]
		reconnectRate << ["30" : "Retry every 30 seconds"]
		reconnectRate << ["45" : "Retry every 45 seconds"]
		reconnectRate << ["60" : "Retry every minute"]
		reconnectRate << ["120" : "Retry every minute"]
		reconnectRate << ["300" : "Retry every 5 minutes"]
		reconnectRate << ["600" : "Retry every 10 minutes"]
		input ("retryDelay", "enum", title: "Device Reconnect delay (WebOS Only)", options: reconnectRate, defaultValue: 60)
	}
}

def log_warn(logMsg) {
	log.warn(logMsg)
}

def log_error(logMsg) {
	log.error(logMsg)
}

def log_debug(logMsg) {
	if ((debug == true) || (descriptionText == true)) { log.debug(logMsg) }
}

def log_info(logMsg) {
	if (descriptionText == true) { log.info(logMsg) }
}

def installed()
{
    log_debug("LG Smart TV Driver - installed - ip: ${televisionIp}  mac: ${televisionMac} type: ${televisionType}  key: ${pairingKey}  debug: ${debug} logText: ${descriptionText}")
    log_debug("LG Smart TV Driver - installed - settings: " + settings.inspect())
//    initialize()
}

def webosRegister() {
	// prove we are registered
    state.pairFailCount = 0
    state.registerPending = true
//	def msg = '{"type":"register","id":"register_0","payload":{"forcePairing":false,"pairingType":"PIN","client-key":"' + pairingKey + '","manifest":{"manifestVersion":1,"appVersion":"1.1","signed":{"created":"20140509","appId":"com.lge.test","vendorId":"com.lge","localizedAppNames":{"":"LG Remote App","ko-KR":"??? ?","zxx-XX":"?? R??ot? A??"},"localizedVendorNames":{"":"LG Electronics"},"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"],"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"],"signatures":[{"signatureVersion":1,"signature":"eyJhbGdvcml0aG0iOiJSU0EtU0hBMjU2Iiwia2V5SWQiOiJ0ZXN0LXNpZ25pbmctY2VydCIsInNpZ25hdHVyZVZlcnNpb24iOjF9.hrVRgjCwXVvE2OOSpDZ58hR+59aFNwYDyjQgKk3auukd7pcegmE2CzPCa0bJ0ZsRAcKkCTJrWo5iDzNhMBWRyaMOv5zWSrthlf7G128qvIlpMT0YNY+n/FaOHE73uLrS/g7swl3/qH/BGFG2Hu4RlL48eb3lLKqTt2xKHdCs6Cd4RMfJPYnzgvI4BNrFUKsjkcu+WD4OO2A27Pq1n50cMchmcaXadJhGrOqH5YmHdOCj5NSHzJYrsW0HPlpuAx/ECMeIZYDh6RMqaFM2DXzdKX9NmmyqzJ3o/0lkk/N97gfVRLW5hA29yeAwaCViZNCP8iC9aO0q9fQojoa7NQnAtw=="}]}}}'
	def msg = '{"type":"register","id":"register_0","payload":{"forcePairing":false,"pairingType":"PROMPT","client-key":"' + pairingKey + '","manifest":{"manifestVersion":1,"appVersion":"1.1","signed":{"created":"20140509","appId":"com.lge.test","vendorId":"com.lge","localizedAppNames":{"":"LG Remote App","ko-KR":"??? ?","zxx-XX":"?? R??ot? A??"},"localizedVendorNames":{"":"LG Electronics"},"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"],"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"],"signatures":[{"signatureVersion":1,"signature":"eyJhbGdvcml0aG0iOiJSU0EtU0hBMjU2Iiwia2V5SWQiOiJ0ZXN0LXNpZ25pbmctY2VydCIsInNpZ25hdHVyZVZlcnNpb24iOjF9.hrVRgjCwXVvE2OOSpDZ58hR+59aFNwYDyjQgKk3auukd7pcegmE2CzPCa0bJ0ZsRAcKkCTJrWo5iDzNhMBWRyaMOv5zWSrthlf7G128qvIlpMT0YNY+n/FaOHE73uLrS/g7swl3/qH/BGFG2Hu4RlL48eb3lLKqTt2xKHdCs6Cd4RMfJPYnzgvI4BNrFUKsjkcu+WD4OO2A27Pq1n50cMchmcaXadJhGrOqH5YmHdOCj5NSHzJYrsW0HPlpuAx/ECMeIZYDh6RMqaFM2DXzdKX9NmmyqzJ3o/0lkk/N97gfVRLW5hA29yeAwaCViZNCP8iC9aO0q9fQojoa7NQnAtw=="}]}}}'
	log_debug("webosRegister: sending Auth request: ${msg}")
	interfaces.webSocket.sendMessage(msg)
}

def webosStartPairing() {
    state.pairFailCount = 0
    state.registerPending = true
	// def registerCMD = '{"type":"register","id":"register_0","payload":{"forcePairing":true,"pairingType":"PIN","client-key":"","manifest":{"manifestVersion":1,"appVersion":"1.1","signed":{"created":"20140509","appId":"com.lge.test","vendorId":"com.lge","localizedAppNames":{"":"LG Remote App","ko-KR":"??? ?","zxx-XX":"?? R??ot? A??"},"localizedVendorNames":{"":"LG Electronics"},"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"],"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"],"signatures":[{"signatureVersion":1,"signature":"eyJhbGdvcml0aG0iOiJSU0EtU0hBMjU2Iiwia2V5SWQiOiJ0ZXN0LXNpZ25pbmctY2VydCIsInNpZ25hdHVyZVZlcnNpb24iOjF9.hrVRgjCwXVvE2OOSpDZ58hR+59aFNwYDyjQgKk3auukd7pcegmE2CzPCa0bJ0ZsRAcKkCTJrWo5iDzNhMBWRyaMOv5zWSrthlf7G128qvIlpMT0YNY+n/FaOHE73uLrS/g7swl3/qH/BGFG2Hu4RlL48eb3lLKqTt2xKHdCs6Cd4RMfJPYnzgvI4BNrFUKsjkcu+WD4OO2A27Pq1n50cMchmcaXadJhGrOqH5YmHdOCj5NSHzJYrsW0HPlpuAx/ECMeIZYDh6RMqaFM2DXzdKX9NmmyqzJ3o/0lkk/N97gfVRLW5hA29yeAwaCViZNCP8iC9aO0q9fQojoa7NQnAtw=="}]}}}'
	def registerCMD = '{"type":"register","id":"register_0","payload":{"forcePairing":true,"pairingType":"PROMPT","client-key":"","manifest":{"manifestVersion":1,"appVersion":"1.1","signed":{"created":"20140509","appId":"com.lge.test","vendorId":"com.lge","localizedAppNames":{"":"LG Remote App","ko-KR":"??? ?","zxx-XX":"?? R??ot? A??"},"localizedVendorNames":{"":"LG Electronics"},"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"],"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"],"signatures":[{"signatureVersion":1,"signature":"eyJhbGdvcml0aG0iOiJSU0EtU0hBMjU2Iiwia2V5SWQiOiJ0ZXN0LXNpZ25pbmctY2VydCIsInNpZ25hdHVyZVZlcnNpb24iOjF9.hrVRgjCwXVvE2OOSpDZ58hR+59aFNwYDyjQgKk3auukd7pcegmE2CzPCa0bJ0ZsRAcKkCTJrWo5iDzNhMBWRyaMOv5zWSrthlf7G128qvIlpMT0YNY+n/FaOHE73uLrS/g7swl3/qH/BGFG2Hu4RlL48eb3lLKqTt2xKHdCs6Cd4RMfJPYnzgvI4BNrFUKsjkcu+WD4OO2A27Pq1n50cMchmcaXadJhGrOqH5YmHdOCj5NSHzJYrsW0HPlpuAx/ECMeIZYDh6RMqaFM2DXzdKX9NmmyqzJ3o/0lkk/N97gfVRLW5hA29yeAwaCViZNCP8iC9aO0q9fQojoa7NQnAtw=="}]}}}'
    log_debug("webosStartPairing: requesting Authorization")
    interfaces.webSocket.sendMessage(registerCMD)
}

def setPower(boolean newState) {
	state.power = newState
	log_debug("setPower: setting state.power = " + (newState ? "ON":"OFF"))
}

def sendPowerEvent(boolean newState) {
	state.lastPower = state.power
	state.power = newState
	log.debug("sendPowerEvent: sending state.power = " + (newState ? "ON":"OFF") + ((state.lastPower == state.power)?" event":" state change event"))
	sendEvent(name: "power", value: (newState?"on":"off"), displayed:false, isStateChange: ((state.lastPower == state.power)?false:true))
	sendEvent(name: "switch", value: (newState?"on":"off"), displayed:false, isStateChange: ((state.lastPower == state.power)?false:true))
}

def setPaired(boolean newState) {
	state.paired = newState
	log_debug("setPaired: setting state.paired = " + (newState ? "TRUE":"FALSE"))
}

def initialize()
{
    log_debug("LG Smart TV Driver - initialize - ip: ${televisionIp}  mac: ${televisionMac}  type: ${televisionType}  key: ${pairingKey} debug: ${debug} logText: ${descriptionText}")
    log_debug("LG Smart TV Driver - initialize - settings:" + settings.inspect())
    state.sequenceNumber = 1
	state.currentInput = ""
	state.lastInput = ""
	state.channel = ""
	state.lastChannel = ""
	state.channelDesc = ""
	state.lastChannelDesc = ""
	state.channelName = ""
	state.channelData = ""
	sendEvent(name: "channelDesc", value: "", isStateChange: true)
	sendEvent(name: "channel", value: "", isStateChange: true)
	sendEvent(name: "channelName", value: "", isStateChange: true)
	sendEvent(name: "channelData", value: "", isStateChange: true)
	sendEvent(name: "CurrentInput", value: "", isStateChange: true)
    setPaired(false)
    state.pairFailCount = 0
    state.reconnectPending = false
    setPower(false)
	state.webSocket = "initialize"
	unschedule()

    if (televisionType == "WEBOS") {
		if (state.webSocket == "open") {
			interfaces.webSocket.close()
		}
        try {
            log_debug("Connecting secure websocket to: \"wss://${televisionIp}:3001/\"")
            interfaces.webSocket.connect("wss://${televisionIp}:3001/", ignoreSSLIssues: true)
        } 
        catch(e) {
            //if (logEnable) log.debug "initialize error: ${e.message}"
            log_warn "initialize error: ${e.message}"
            log.error "WebSocket connect failed"
        }
//        if ((pairingKey == null) || (pairingKey == "")) {
//            webosStartPairing()
//        } else {
//            state.paired = true
//			webosRegister()
//        }
    }
}

def updated()
{
    log_debug("LG Smart TV Driver - updated - ip: ${televisionIp}  mac: ${televisionMac}  type: ${televisionType}  key: ${pairingKey} debug: ${debug} logText: ${descriptionText} state: "+state.inspect())
    log_debug("LG Smart TV Driver - updated - ip: ${settings.televisionIp}  mac: ${settings.televisionMac}  type: ${settings.televisionType}  key: ${settings.pairingKey} debug: ${settings.debug} logText: ${settings.descriptionText} state: "+state.inspect())
	initialize()
}

def setParameters(String IP, String MAC, String TVTYPE, String KEY) {
	log_debug("LG Smart TV Driver - setParameters - ip: ${IP}  mac: ${MAC}  type: ${TVTYPE}  key: ${KEY}")
	state.televisionIp = IP
	settings.televisionIp = IP
	device.updateSetting("televisionIp",[type:"text", value:IP])
	state.televisionMac = MAC
	settings.televisionMac = MAC
	device.updateSetting("televisionMac",[type:"text", value:MAC])
	state.televisionType = TVTYPE
	settings.televisionType = TVTYPE
	device.updateSetting("televisionType",[type:"text", value:TVTYPE])
	if (TVTYPE == "NETCAST") {
		state.pairingKey = KEY
		settings.pairingKey = KEY
		device.updateSetting("pairingKey",[type:"text", value:KEY])
	}
	log_debug("LG Smart TV Driver - Parameters SET- ip: ${televisionIp}  mac: ${televisionMac}  type: ${televisionType}  key: ${pairingKey}")
}

// parse events into attributes
def parse(String description) 
{
    // parse method is shared between HTTP and Websocket implementations
	log_debug "Parsing '${description}'"
    
    if (televisionType == "NETCAST") {
        if (description == "updated") 
        {
    	    sendEvent(name:'refresh', displayed:false)
        }
        else
        {
    	    parseHttpResult(description)
        }
    } else {
        // parse the websocket response
        parseWebsocketResult(description)
    }
}

def parseWebsocketResult(String description){
	log_debug("parseWebsocketResult")
	def json = null
    try{
        json = new groovy.json.JsonSlurper().parseText(description)
        if(json == null){
            log_warn("parseWebsocketResult: String description not parsed")
            return
        }
        log_info("json = ${json}"    )
    }  catch(e) {
        log.error("parseWebsocketResult: Failed to parse json e = ${e}")
        return
    }
	if (json?.type == "registered") {
		if (json?.id == "register_0") {
			// this is a response to our pairing request - we are registered
			if (!(json?.payload["client-key"] == null)){
				pKey = json.payload["client-key"]
				log_warn("parseWebsocketResult: received registered client-key: ${pKey}")
				state.pairingKey = pKey
				settings.pairingKey = pKey
				device.updateSetting("pairingKey",[type:"text", value:"${pKey}"])
				pairingKey = pKey
				log_warn("parseWebsocketResult:      set registered client-key: ${pairingKey}")
				setPaired(true)
				state.registerPending = false
				// start running the poll routine for ongoning status updates
				log_info("parseWebsocketResult:      requesting HELLO packet")
				sendCommand('{"type":"hello","id":"status_%d"}')
				log_info("parseWebsocketResult:      requesting SystemInfo packet")
				sendCommand('{"type":"request","id":"status_%d","uri":"ssap://system/getSystemInfo"}')
//				log_warn("parseWebsocketResult:      requesting CurrentSWInformation packet")
//				sendCommand('{"type":"request","id":"status_%d","uri":"ssap://com.webos.service.update/getCurrentSWInformation"}')
				webosSubscribeToStatus()
            }
        }
    }
    if (json?.type == "response") {
        if (json?.id == "register_0") {
            // this is a response to our pairing request - we are waiting for user authorization at the TV
            if (!(json?.payload["client-key"] == null)){
                pKey = json.payload["client-key"]
                log_warn("parseWebsocketResult: received response client-key: ${pKey}")
                state.pairingKey = pKey
                settings.pairingKey = pKey
				pairingKey = pKey
				device.updateSetting("pairingKey",[type:"text", value:"${pKey}"])
                log_warn("parseWebsocketResult:      set response client-key: ${pairingKey}")
                setPaired(true)
                state.registerPending = false
            }
        }
        if (json?.id.startsWith("command_")) {
            if (json?.payload?.returnValue == true) {
                //we received an afirmative response
                webosPollStatus()
            }
        }
		if (json?.id.startsWith("status_")) {
			def rResp = false
			if ((state.power == false) && !(json?.payload?.subscribed == true)) {
				// when TV has indicated power off, do not process status messages unless they are subscriptions
					log_warn("ignoring unsubscribed status updated during power off...")
			} else {
				if (json?.payload?.channel) { 
					state.lastChannelDesc = state.channelDesc
					state.channel = json?.payload?.channel?.channelNumber
					state.channelDesc = json?.payload?.channel?.channelNumber + " ("+ json?.payload?.channel?.majorNumber + "." + json?.payload?.channel?.minorNumber + "): " + json?.payload?.channel?.channelName
					def cChange = ((state.lastChannelDesc == state.channelDesc)?false:true)
					def cData = json?.payload?.channel
					cData << [channelDesc: state.channelDesc]
					if (!channelDetail) {
						cData = [
							channelDesc: state.channelDesc,
							channelMode: json?.payload?.channel?.channelMode,
							channelNumber: json?.payload?.channel?.channelNumber,
							majorNumber: json?.payload?.channel?.majorNumber,
							minorNumber: json?.payload?.channel?.minorNumber,
							channelName: json?.payload?.channel?.channelName,
						]
					}
					sendEvent(name: "channelDesc", value: state.channelDesc, displayed:false, isStateChange: cChange)
					sendEvent(name: "channel", value: state.channel, displayed:false, isStateChange: cChange)
					sendEvent(name: "channelName", value: json?.payload?.channel?.channelName, displayed:false, isStateChange: cChange)
					sendEvent(name: "channelData", value: cData, displayed:false, isStateChange: cChange)
					log_info("state.channelDesc = ${state.channelDesc}")
					rResp = true
				}
				if (json?.payload?.returnValue == true) {
					if (json?.payload?.volume) { 
						state.lastVolume = state.Volume
						state.Volume = json?.payload?.volume
						sendEvent(name: "volume", value: state.Volume, displayed:false, isStateChange: ((state.lastVolume == state.Volume)?false:true))
						log_info("state.Volume = ${state.Volume}")
						rResp = true
					}
					if (json?.payload?.mute != null) { 
						state.lastMute = state.Mute
						state.Mute = json?.payload?.mute
						sendEvent(name: "mute", value: state.Mute, displayed:false, isStateChange: ((state.lastMute == state.Mute)?false:true))
						log_info("state.Mute = ${state.Mute}")
						rResp = true
					}
					if (json?.payload?.modelName) { 
						state.ModelName = json?.payload?.modelName
						log_info("state.ModelName = ${state.ModelName}")
						rResp = true
					} 
					if (json?.payload?.appId) { 
						state.lastInput = state.CurrentInput
						state.CurrentInput = json?.payload?.appId
						log.info("state.CurrentInput = ${state.CurrentInput}")
						sendEvent(name: "CurrentInput", value: state.CurrentInput, displayed:false, isStateChange: ((state.lastInput == state.CurrentInput)?false:true))
						if (!(state.lastInput == state.CurrentInput) && (state.CurrentInput == "com.webos.app.livetv")) {
							sendCommand('{"type":"subscribe","id":"status_channel_0","uri":"ssap://tv/getChannelProgramInfo"}')
						}
						if ((state.lastInput == "com.webos.app.livetv") && !(state.CurrentInput == "com.webos.app.livetv")) {
							sendCommand('{"type":"unsubscribe","id":"status_channel_0","uri":"ssap://tv/getChannelProgramInfo"}')
							state.channel = ""
							state.lastChannel = ""
							state.channelDesc = ""
							state.lastChannelDesc = ""
							state.channelName = ""
							state.channelData = ""
							sendEvent(name: "channelDesc", value: "", displayed:false, isStateChange: true)
							sendEvent(name: "channel", value: "", displayed:false, isStateChange: true)
							sendEvent(name: "channelName", value: "", displayed:false, isStateChange: true)
							sendEvent(name: "channelData", value: "", displayed:false, isStateChange: true)
						}
						rResp = true
        	        }
					if (rResp == true) {
						sendPowerEvent(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) {
						log.debug("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
								sendPowerEvent(false)
								state.CurrentInput = ""
								state.lastInput = ""
								state.channel = ""
								state.lastChannel = ""
								state.channelDesc = ""
								state.lastChannelDesc = ""
								state.channelName = ""
								state.channelData = ""
								sendEvent(name: "channelDesc", value: "", displayed:false, isStateChange: true)
								sendEvent(name: "channel", value: "", displayed:false, isStateChange: true)
								sendEvent(name: "channelName", value: "", displayed:false, isStateChange: true)
								sendEvent(name: "channelData", value: "", displayed:false, isStateChange: true)
								sendEvent(name: "CurrentInput", value: "", displayed:false, isStateChange: true)
								log.warn("Received POWER DOWN notification.")
							}
						}
					}
				}
			}
		}
	}
	if (json?.type == "hello") {
		if (json?.payload?.protocolVersion) {
		}
		if (json?.payload?.deviceOS) {
			state.deviceOS = json?.payload?.deviceOS
		}
		if (json?.payload?.deviceOSVersion) {
			state.deviceOSVersion = json?.payload?.deviceOSVersion
		}
		if (json?.payload?.deviceOSReleaseVersion) {
			state.deviceOSReleaseVersion = json?.payload?.deviceOSReleaseVersion
		}
		if (json?.payload?.deviceUUID) {
			state.deviceUUID = json?.payload?.deviceUUID
		}
	}
	if (json?.type == "error") {
		if (json?.id == "register_0") {
			if (json?.error.take(3) == "403") {
				// 403 error cancels the pairing process
				pairingKey = ""
				setPaired(false)
				state.pairFailCount = state.pairFailCount ? state.pairFailCount + 1 : 1
				log_info("parseWebsocketResult: received register_0 error: ${json.error} fail count: ${state.pairFailCount}")
				if (state.pairFailCount < 6) { webosStartPairing() }
			}
		} else {
			if (json?.error.take(3) == "401") {
				log_info("parseWebsocketResult: received error: ${json.error}")
				if (state.registerPending == false) { webosStartPairing() }
				//webosStartPairing()
			}
		}
	}
}

def webSocketStatus(String status){
	//if (logEnable) log.debug "webSocketStatus- ${status}"
	log_debug ("webSocketStatus: State: [${state.webSocket}]   Reported Status: [${status}]")

	if(status.startsWith('failure: ')) {
		log_warn("failure message from web socket ${status}")
		setPaired(false)
		if (state.power == false) { state.reconnectDelay = 30 }
		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")) {
			log_warn("failure: No route/connect timeout/no pong for websocket protocol")
//			if (state.power) {
//				sendEvent(name: "power", value: "off", displayed:false, isStateChange: true)
//				sendEvent(name: "switch", value: "off", displayed:false, isStateChange: true)
//			}
//			state.power = false
			sendPowerEvent(false)
			//retry every 60 seconds
			state.reconnectDelay = 30
		}
		state.webSocket = "closed"
		reconnectWebSocket()
	} 
	else if(status == 'status: open') {
		log_info("websocket is open")
		// success! reset reconnect delay
		pauseExecution(1000)
		webosPollStatus()
		state.reconnectDelay = 1
		state.webSocket = "open"
		if ((pairingKey == null) || (pairingKey == "")) {
			webosStartPairing()
		} else {
			setPaired(true)
			webosRegister()
		}
	} 
	else if (status == "status: closing"){
		log_warn("WebSocket connection closing.")
		setPaired(false)
		unschedule()
		if (state.webSocket == 'initialize') {
			log_warn("Ignoring WebSocket close due to initialization.")
		} else {
			if (state.power == true) {
				// TV should be on and reachable - try to reconnect
				reconnectWebSocket()
			} else {
				// We explicitly turned off the TV - reduce the reconnect time and try to reconnect every 60 seconds
				state.reconnectDelay = 30
				reconnectWebSocket()
        	}
		}
		state.webSocket = "closed"
	} 
	else {
		log_error "WebSocket error, reconnecting."
//		if (state.power == true) {
//			sendEvent(name: "power", value: "off", displayed:false, isStateChange: true)
//			sendEvent(name: "switch", value: "off", displayed:false, isStateChange: true)
//		}
//		state.power = false
		sendPowerEvent(false)
		setPaired(false)
		state.webSocket = "closed"
		reconnectWebSocket()
	}
}

def reconnectWebSocket() {
	// first delay is 2 seconds, doubles every time
	if (state.reconnectPending == true) { 
		log_debug("Rejecting additional reconnect request")
		return
	}
	state.reconnectDelay = (retryDelay ?: 60) as int
//	state.reconnectDelay = (state.reconnectDelay ?: 1) * 2
//	don't let delay get too crazy, max it out at 10 minutes
	if(state.reconnectDelay > 600) state.reconnectDelay = 600
	log_info("websocket reconnect - delay = ${state.reconnectDelay}")
	//If the TV is offline, give it some time before trying to reconnect
	state.reconnectPending = true
	log_warn("Scheduling reconnect in ${state.reconnectDelay} seconds")
	runIn(state.reconnectDelay, initialize)
}

def webosSubscribeToStatus() {
	if (state.paired) {
		sendCommand('{"type":"subscribe","id":"status_%d","uri":"ssap://audio/getStatus"}')
		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("webosPollStatus")
}

def webosPollStatus() {
	if (!state.registerPending) {
		log_debug("webosPollStatus - paired = "+(state.paired?"TRUE":"FALSE")+"  currentInput = "+state.CurrentInput)
		if (state.paired) {
			// send webos commands to poll the TV status
			log_debug("webosPollStatus: requesting device status...")
			sendCommand('{"type":"request","id":"status_%d","uri":"ssap://audio/getStatus"}')
			//sendCommand('{"type":"request","id":"status_%d","uri":"ssap://tv/getExternalInputList"}')
			sendCommand('{"type":"request","id":"status_%d","uri":"ssap://com.webos.applicationManager/getForegroundAppInfo"}')
			if (state.CurrentInput == "com.webos.app.livetv") {
				sendCommand('{"type":"request","id":"status_%d","uri":"ssap://tv/getChannelProgramInfo"}')
			}
		} else {
			log_debug("webosPollStatus: Nothing to do...")
		}
	}
}


def deviceNotification(String notifyMessage) {
    if (televisionType == "WEBOS") { 
		if (state.paired) {
			return sendCommand('{"type":"request","id":"command_%d","uri":"ssap://system.notifications/createToast","payload":{"message":"'+notifyMessage+'"}}')
		}
	}
}

def on()
{
	log_debug "Executing 'Power On'"
	sendPowerEvent(true)
//	sendEvent(name: "switch", value: "on", displayed:false, isStateChange: true)
//	sendEvent(name: "power", value: "on", displayed:false, isStateChange: true)
	return wake()
}

def off()
{
	log_debug "Executing 'Power Off'"
	sendPowerEvent(false)
//    sendEvent(name: "switch", value: "off", displayed:false, isStateChange: true)
//	sendEvent(name: "power", value: "off", displayed:false, isStateChange: true)
    if (televisionType == "NETCAST") { 
        return sendCommand(1)
    } else {
        return sendCommand('{"type":"request","id":"command_%d","uri":"ssap://system/turnOff"}')
    }
}

def channelUp() 
{
	log_debug "Executing 'channelUp'"
    if (televisionType == "NETCAST") { 
        return sendCommand(27)
    } else {
        return sendCommand('{"type":"request","id":"command_%d","uri":"ssap://tv/channelUp"}')
    }
}

def channelDown() 
{
	log_debug "Executing 'channelDown'"
    if (televisionType == "NETCAST") { 
        return sendCommand(28)
    } else {
        return sendCommand('{"type":"request","id":"command_%d","uri":"ssap://tv/channelDown"}')
    }
}


// handle commands
def volumeUp() 
{
	log_debug "Executing 'volumeUp'"
    if (televisionType == "NETCAST") { 
        return sendCommand(24)
    } else {
        return sendCommand('{"type":"request","id":"command_%d","uri":"ssap://audio/volumeUp"}')
    }
}

def volumeDown() 
{
	log_debug "Executing 'volumeDown'"
    if (televisionType == "NETCAST") { 
        return sendCommand(25)
    } else {
        return sendCommand('{"type":"request","id":"command_%d","uri":"ssap://audio/volumeDown"}')
    }
}

def setVolume(level) {
	log_debug "Executing 'setVolume' with level '${level}'"
    if (televisionType == "NETCAST") { 
        //return sendCommand(25)
    } else {
        return sendCommand('{"type":"request","id":"command_%d","uri":"ssap://audio/setVolume","payload":{"volume":'+level+'}}')
    }
}

def setLevel(level) { setVolume(level) }


def refresh() 
{
    log_debug "Executing 'refresh'"
    if (televisionType == "NETCAST") { 
	    return sessionIdCommand()
	} else {
		log_info("refresh: refreshing System Info")
		sendCommand('{"type":"hello","id":"status_%d"}')
		sendCommand('{"type":"request","id":"status_%d","uri":"ssap://system/getSystemInfo"}')
		return webosPollStatus()
	}
}

def unmute() {
	return mute()
}

def mute() 
{
	log_debug "Executing 'mute'"
//  		sendEvent(name:'mute', value:'On', displayed:false)
    if (televisionType == "NETCAST") { 
        return sendCommand(26)
    } else {
        def newMute = !(state.Mute ?: false)
        return sendCommand('{"type":"request","id":"command_%d","uri":"ssap://audio/setMute","payload":{"mute":'+newMute+'}}')
    }
}

def externalInput()
{
    if (televisionType == "NETCAST") { 
        return sendCommand(47)
    } else {
        def cInput = state.CurrentInput ?: "com.webos.app.hdmi1"
        def nInput = null
	    switch(cInput) {
		    case "com.webos.app.externalinput.av1" :
		        nInput = "com.webos.app.externalinput.component"
			    break
		    case "com.webos.app.externalinput.component" :
		        nInput = "com.webos.app.hdmi1"
			    break
		    case "com.webos.app.hdmi1" :
		        nInput = "com.webos.app.hdmi2"
			    break
		    case "com.webos.app.hdmi2" :
		        nInput = "com.webos.app.hdmi3"
			    break
		    case "com.webos.app.hdmi3" :
		        nInput = "com.webos.app.livetv"
			    break
		    case "com.webos.app.livetv" :
		        nInput = "com.webos.app.externalinput.av1"
			    break
			default :
				nInput = "com.webos.app.hdmi1"
				break
	    }
        return sendCommand('{"type":"request","id":"command_%d","uri":"ssap://system.launcher/launch","payload":{"id":"' + nInput + '"}}')
//        return sendCommand('{"type":"request","id":"command_%d","uri":"ssap://tv/switchInput","payload":{"inputId":"HDMI_1"}}')
    }
}

def back()
{
    if (televisionType == "NETCAST") { 
        return sendCommand(23)
    } else {
    }
}

def up()
{
    if (televisionType == "NETCAST") { 
        return sendCommand(12)
    } else {
    }
}

def down()
{
	return sendCommand(13)
    if (televisionType == "NETCAST") { 
        return sendCommand(13)
    } else {
    }
}

def left()
{
    if (televisionType == "NETCAST") { 
        return sendCommand(14)
    } else {
    }
}

def right()
{
    if (televisionType == "NETCAST") { 
        return sendCommand(15)
    } else {
    }
}

def myApps()
{
    if (televisionType == "NETCAST") { 
        return sendCommand(417)
    } else {
        sendCommand('{"type":"request","id":"command_%d","uri":"ssap://system.launcher/launch","payload":{"id":"com.webos.app.discovery"}}')
		return webosPollStatus()
    }
}

def ok()
{
    if (televisionType == "NETCAST") { 
        return sendCommand(20)
    } else {
        sendCommand('{"type":"request","id":"command_%d","uri":"com.webos.service.ime/sendEnterKey"}')
	}
}

def home()
{
    if (televisionType == "NETCAST") { 
        return sendCommand(21)
    } else {
    }
}

def wake() {
	log_debug "Sending Magic Packet to: $televisionMac"
	def result = new hubitat.device.HubAction (
       	"wake on lan $televisionMac",
       	hubitat.device.Protocol.LAN,
       	null,[secureCode: β€œ0000”]
    )
		log_info "Sending Magic Packet to: " + result
	
    return result
	//sendHubCommand(result)
	
}

def sendCommand(cmd)
{
    if (televisionType == "NETCAST") { 
    	def actions = []
    
   	    actions << sessionIdCommand()
   	    actions << tvCommand(cmd)
   
        actions = actions.flatten()
        return actions
    } else {
        def msg = String.format(cmd,state.sequenceNumber)
        log_debug("sendCommand: " + msg)
        // send the command
        try {
            interfaces.webSocket.sendMessage(msg)
        }
        catch (Exception e) 
        {
    		log_warn "Hit Exception $e on sendCommand"
        }
        state.sequenceNumber++
    }

}

def sessionIdCommand()
{
    def commandText = "<?xml version=\"1.0\" encoding=\"utf-8\"?><auth><type>AuthReq</type><value>$pairingKey</value></auth>"       
    def httpRequest = [
      	method:		"POST",
        path: 		"/roap/api/auth",
        body:		"$commandText",
        headers:	[
        				HOST:			"$televisionIp:8080",
                        "Content-Type":	"application/atom+xml",
                    ]
	]
    
    try 
    {
    	def hubAction = new hubitat.device.HubAction(httpRequest)
        log_warn "hub action: $hubAction"
        return hubAction
    }
    catch (Exception e) 
    {
		log.debug "Hit Exception $e on $hubAction"
	}
}

def tvCommand(cmd)
{
    def commandText = "<?xml version=\"1.0\" encoding=\"utf-8\"?><command><type>HandleKeyInput</type><value>${cmd}</value></command>"

    def httpRequest = [
      	method:		"POST",
        path: 		"/udap/api/command",
        body:		"$commandText",
        headers:	[
        				HOST:			"$televisionIp:8080",
                        "Content-Type":	"application/atom+xml",
                    ]
	]
    
    try 
    {
    	def hubAction = new hubitat.device.HubAction(httpRequest)
        log_debug "hub action: $hubAction"
    	return hubAction
    }
    catch (Exception e) 
    {
		log.debug "Hit Exception $e on $hubAction"
	}
}



def appCommand()
{
	log.debug "Reached App Command"
    def commandText = "<?xml version=\"1.0\" encoding=\"utf-8\"?><envelope><api type=\"command\"><name>AppExecute</name><auid>1</auid><appname>Netflix</appname><contentId>1</contentId></api></envelope>"

    def httpRequest = [
      	method:		"POST",
        path: 		"/udap/api/command",
        body:		"$commandText",
        headers:	[
        				HOST:			"$televisionIp:8080",
                        "Content-Type":	"application/atom+xml",
                    ]
	]
    
    try 
    {
    	def hubAction = new hubitat.device.HubAction(httpRequest)
        log_debug "hub action: $hubAction"
    	return hubAction
    }
    catch (Exception e) 
    {
		log_warn "Hit Exception $e on $hubAction"
	}
}

private parseHttpResult (output)
{
	def headers = ""
	def parsedHeaders = ""
    
    def msg = parseLanMessage(output)

    def headersAsString = msg.header // => headers as a string
    def headerMap = msg.headers      // => headers as a Map
    def body = msg.body              // => request body as a string
    def status = msg.status          // => http status code of the response
    def json = msg.json              // => any JSON included in response body, as a data structure of lists and maps
    def xml = msg.xml                // => any XML included in response body, as a document tree structure
    def data = msg.data              // => either JSON or XML in response body (whichever is specified by content-type header in response)
	log.debug "status check ekim: status: $status"

	log_debug "headers: $headerMap, status: $status, body: $body, data: $json"
  
    if (status == 200){
    	parseSessionId(body)
    }
    else if (status == 401){
    	log_info "Unauthorized - clearing session value"
    	sendEvent(name:'sessionId', value:'', displayed:false)
        sendEvent(name:'refresh', displayed:false)
    }
}

def String parseSessionId(bodyString)
{
	def sessionId = ""
	def body = new XmlSlurper().parseText(bodyString)
  	sessionId = body.session.text()

	if (sessionId != null && sessionId != "")
  	{
  		sendEvent(name:'sessionId', value:sessionId, displayed:false)
  		log_debug "session id: $sessionId"
    }
}

private parseHttpHeaders(String headers) 
{
	def lines = headers.readLines()
	def status = lines[0].split()

	def result = [
	  protocol: status[0],
	  status: status[1].toInteger(),
	  reason: status[2]
	]

	if (result.status == 200) {
		log_debug "Authentication successful! : $status"
	}
    else
    {
    	log_debug "Authentication Unsuccessful: $status"
    }

	return result
}

private def delayHubAction(ms) 
{
    log_debug("delayHubAction(${ms})")
    return new hubitat.device.HubAction("delay ${ms}")
}

/***********************************************************************************************************************
*
* Release Notes
*
* 0.2.5
* Fixed - old channel data not removed on TV poweron
* Added - user selectable connection retry time (WebOS only)
*
* 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
*
* Issues
* Unable to turn tv on (tried Wake on Lan unsuccessfully) - fixed (wake on lan / Mobile TV On must be enabled on the TV)
* Settings not carrying over from App to Driver
*
***********************************************************************************************************************/

2 Likes

Fantastic. Thank you. This is a good step. It's connecting again to my CX. It looks like you modified the original code from way back when this project started. Several features are missing, such as the ability to select inputs using a text string. Also I was unable to retrieve a new pairing code. Luckily I still have my original one and I can now at least use it for power and volume control as well as reporting on/off status correctly.

If we could import the changes to the newer code versions this would be back to exactly what we need

sure if you can paste the newer driver here I'll edit it and post it back

Sure thing. Here's the newest version available through the HPM

/**
 *  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:
 *
 *  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.
 *
 */

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

@Field String VERSION = "1.0.0"

@Field List<String> 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 websocket to: ws://${televisionIp}:3000/")
    interfaces.webSocket.connect("ws://${televisionIp}:3000/")
  } 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}"
    }
  }
}

Is it supposed to fill in the IP and MAC address itself? I'm probably doing something wrong but if I try to save the IP address and MAC address, it demands the pairing key which I cannot find.

here you go

/**
 *  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:
 *
 *  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.
 *
 */

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

@Field String VERSION = "1.0.0"

@Field List<String> 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}"
    }
  }
}

Fantastic. thats exactly what was needed. Hopefully the original author will add these changes to the HPM version. but for now that fixed it and is back up and running again. Thank you

yes, the app that is used with the driver will search for the TV, and once it's found it will create a new device and add all three of those automatically. If you don't have the app you must put all three in manually. if you don't have a pairing key, an initialization is meant to trigger the TV to give you one. Mo.hesham's fix to the code makes this happen.

1 Like

It finally worked! It came up on the TV that a mobile device was trying to pair with it, which had never come up for me before.

The newest driver code returns an error for me, though - something like found ')' was expecting '{' on line 331 where the "ignoreSSLIssues: true" line is

If I change it, it just comes up with an error on the line below so I'm back to the previously modified driver code and the basics are working for me

Thanks very much

Yes there was an extra ) at the end of that line. I removed one and everything worked perfectly

Wicked. All working here too. Thanks @mo.hesham. Only thing I'm missing is the ability to switch to Live TV as an input. Any ideas why that wouldn't be in the list here but working on HA? Not a deal breaker as I can always do some exercise by using the remote. Thanks again