[PORT] LG Smart TV Discovery 2012+

Would you be able to post your changes here for the toggling on/off issue? I am having this issue on my CX. You mentioned changing the state to on only with app id is not null. How do I make this change?

Thank you

Sure... My code is below... I also didn't care about the mouse (I use this exclusively to keep my Harmony remote in sync) so I removed that from my device handler. Not sure if it's important to you but you can do a diff to try to figure out the differences between mine and asj's.

Also, it's been working well for months (I haven't touched it since my post). Enjoy.

/**
 *  LG WebOs TV Device Type
 *
 * ImportURL: https://raw.githubusercontent.com/as-j/LG_Smart_TV_hubitat/master/LG_WebOS_TV_Driver.groovy
 *
 *  Notifcation icons are fetched from: 
 *  https://github.com/pasnox/oxygen-icons-png/tree/master/oxygen/32x32
 * 
 *  They are named without extention as: <directory>/<file>
 *  
 *  For example:
 *    - file status/battery-low.png has an icon name: status/battery-low
 *    - file actions/checkbox.ping has an icon name: actions/checkbox
 * 
 *  They can be used in a notifcation message formated as:
 *  [icon name]Notification Message
 * 
 *  For example:
 *  [status/battery-low]My Battery is low!
 * 
 *  Or you can use the custom command "notificationIcon" which takes 2 strings
 *  the message, and icon name
 *
 *  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 
 *  Refactoring of callbacks, removed hand rolled json: asj, 9/18/2019
 *
***See Release Notes at the bottom***
***********************************************************************************************************************/
public static String version()      {  return "v0.3.0"  }

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

metadata {
	definition (name: "LG WebOS TV", namespace: "asj", author: "Andrew Stanley-Jones", importUrl: "https://raw.githubusercontent.com/as-j/LG_Smart_TV_hubitat/master/LG_WebOS_TV_Driver.groovy")
	{
		capability "Initialize"
		capability "TV"
		capability "AudioVolume"
		capability "Refresh"
		capability "Switch"
		capability "Notification"

		command "off"
		command "refresh"
        command "refreshInputList"
		command "externalInput", ["string"]
        command "sendJson", ["string"]
		command "myApps"
		command "ok"
		command "home"
        command "notificationIcon", ["string", "string"]
        command "setIcon", ["string", "string"]
        command "clearIcons"
        
        attribute "availableInputs", "list"
		
		attribute "channelDesc", "string"
		attribute "channelName", "string"
        attribute "channelFullNumber", "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: "pairingKey", type: "text", title: "Pairing Key", required: true, defaultValue: ""
		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 name: "retryDelay", type: "enum", title: "Device Reconnect delay", options: reconnectRate, defaultValue: 60
        //standard logging options
        input name: "logEnable", type: "bool", title: "Enable debug logging", defaultValue: false
        input name: "logInfoEnable", type: "bool", title: "Enable info logging", defaultValue: false
        input name: "txtEnable", type: "bool", title: "Enable descriptionText logging from state changes", defaultValue: false


	}
}

@Field static Map callbacks = [:]

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

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

def log_debug(logMsg) {
	if (logEnable) log.debug(logMsg)
}

def log_info(logMsg) {
	if (logInfoEnable) log.info(logMsg)
}

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

def refresh() {
    log_info "refresh: refreshing System Info"
    state.deviceInfo = null
    state.televisionModel = null
    state.nameToInputId = null
    
    webosRegister()
}

def webosRegister() {
    log_info "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 ->
        log_debug("webosRegister: got json: ${json}")
        if (json?.type == "registered") {
            pKey = json.payload["client-key"]
            if (pKey != null) {
                log_debug("parseWebsocketResult: received registered client-key: ${pKey}")
                state.pairingKey = pKey
                device.updateSetting("pairingKey",[type:"text", value:"${pKey}"])
				runInMillis(10, webosSubscribeToStatus)
                // Hello doesn't seem to do anything?
                if (!state.deviceInfo) runInMillis(25, sendHello)
                if (!state.televisionModel) runInMillis(50, sendRequestInfo)
                if (!state.nameToInputId) runInMillis(75, refreshInputList)                
                if (!state.serviceList) runInMillis(100, getServiceList)
            }
            return true
        } else if (json?.type == "response") {
            return false
        }
    })
}

def sendHello() {
    log_info "sendHello: requesting HELLO packet"
    sendWebosCommand(type: "hello", id: "hello")
}

def handler_hello(data) {
    log_debug "Got Hello: ${data}"
    state.deviceInfo = data
}

def sendRequestInfo() {
    log_info "sendRequestInfo: requesting SystemInfo packet"
    sendWebosCommand(uri: "system/getSystemInfo", callback: { json ->
        log_debug "sendRequestInfo(): Got: $json"
        state.televisionModel = json.payload?.modelName
        state.televisionReceiver = json.payload?.receiverType
    })
}

def refreshInputList() {
    log_info "refreshInputList: current list size: ${state.nameToInputId?.size()}"
    sendWebosCommand(uri: "com.webos.applicationManager/listLaunchPoints", payload: [], callback: { json ->
        def inputList = []
        def nameToInputId = [:]
        json?.payload?.launchPoints.each { app ->
            log_debug("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 ->
            jsonExt?.payload?.devices?.each { device ->
                log_debug("Found: ${device?.label} $device")
                inputList += device.label
                nameToInputId[device.label] = device.appId
            }
            state.nameToInputId = nameToInputId
            state.inputList = inputList
            log_info("Inputs: ${state.inputList}")
            sendEvent(name: "availableInputs", value: inputList);
        })
    })
}

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

def powerEvent(String onOrOff, String type = "digital") {
    def descriptionText = "${device.displayName} is ${onOrOff}"
    if (state.power != onOrOff) log_info "${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)
        // Socket status should follow the system reported status
        interfaces.webSocket.close()
    }
}

def initialize() {
    log_info("LG Smart TV Driver - initialize - ip: ${televisionIp}  mac: ${televisionMac}  key: ${pairingKey} debug: ${debug} logText: ${descriptionText}")
    log_debug("LG Smart TV Driver - 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()
    
    interfaces.webSocket.close()
    
    if(!televisionMac) {
        def mac = getMACFromIP(televisionIp)
        if (mac) device.updateSetting("televisionMac",[value:mac,type:"string"])
    }

    try {
        log_debug("Connecting websocket to: \"ws://${televisionIp}:3000/\"")
        interfaces.webSocket.connect("ws://${televisionIp}:3000/")
    } catch(e) {
        //if (logEnable) log.debug "initialize error: ${e.message}"
        log_warn "initialize error: ${e.message}"
        log.error "WebSocket connect failed"
    }
}

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

	if(status.startsWith('failure: ')) {
		log_debug("failure message from web socket ${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")) {
			log_debug("failure: No route/connect timeout/no pong for websocket protocol")
			powerEvent("off", "physical")
		}
		state.webSocket = "closed"
		reconnectWebSocket()
	} 
	else if(status == 'status: open') {
		log_info("websocket is open")
		// success! reset reconnect delay
        //powerEvent("on", "physical")
		state.webSocket = "open"
        webosRegister()
        state.reconnectDelay = 5
	} 
	else if (status == "status: closing"){
		log_debug("WebSocket connection closing.")
		unschedule()
		if (state.webSocket == 'initialize') {
			log_warn("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 {
		log_error "WebSocket error, reconnecting."
		powerEvent("off", "physical")
		state.webSocket = "closed"
		reconnectWebSocket()
	}
}

def reconnectWebSocket(delay = null) {
	// first delay is 2 seconds, doubles every time
	if (state.reconnectPending == true) { 
		log_debug("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

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

def updated()
{
    log_info "LG Smart TV Driver - updated - ip: ${settings.televisionIp}  mac: ${settings.televisionMac}  key: ${settings.pairingKey} debug: ${settings.logEnable} logText: ${settings.descriptionText}"
    if (logEnable) runIn(1800, "logsStop")
	initialize()
}

def logsStop(){
    log_info "debug logging disabled..."
    device.updateSetting("logEnable",[value:"false",type:"bool"])
}

def setParameters(String IP, String MAC, String TVTYPE, String KEY) {
	log_info "LG Smart TV Driver - 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])
	log_debug("LG Smart TV Driver - Parameters SET- ip: ${televisionIp}  mac: ${televisionMac} key: ${pairingKey}")
}

// parse events into attributes
def parse(String description) 
{
    // parse method is shared between HTTP and Websocket implementations
	log_debug("parse: $description")
	def json = null
    try{
        json = new JsonSlurper().parseText(description)
        if(json == null){
            log_warn("parseWebsocketResult: String description not parsed")
            return
        }
        //log_debug("json = ${json}")
    }  catch(e) {
        log.error("parseWebsocketResult: Failed to parse json e = ${e}")
        return
    }
    
	//def rResp = false
    if (this."handler_${json.id}") {
        this."handler_${json.id}"(json.payload)
    } else if (this."handler_${json.type}") {
        this."handler_${json.type}"(json.payload)
		//log_info("testing 1")
		//rResp = true
    } else if (callbacks[json.id]) {
        log_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)) {
            log_debug("Callback[${json.id}]: being kept, done is false")
        } else {
            callbacks[json.id] = null
        }
		//log_info("testing 2")
		//rResp = true
	} 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
				log_info("parse: received register_0 error: ${json.error} fail count: ${state.pairFailCount}")
				if (state.pairFailCount < 6) { webosRegister() }
			}
		} else {
			if (json?.error.take(3) == "401") {
				log_info("parse: received error: ${json.error}")
				//if (state.registerPending == false) { webosRegister() }
				//webosRegister()
			}
		}
	}
	//if (rResp == true) {
	//	log_info("rResp on event")
	//	powerEvent("on", "physical")
	//}
}

def 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() {
    state.remove('serviceList')
    state.serviceList = []
    sendWebosCommand(uri: 'api/getServiceList', callback: { json ->
        log_debug("getServiceList: ${json?.payload}")
        json?.payload?.services.each { service ->
            state.serviceList << service?.name
        }
        log_debug("Services: ${state.serviceList}")
    })
}

def handler_audio_getStatus(data) {
    log_debug("handler_audio_getStatus: got: $data")
    def descriptionText = "${device.displayName} volume is ${data.volume}"
    if (txtEnable) log_info "${descriptionText}" 
    sendEvent(name: "volume", value: data.volume, descriptionText: descriptionText)
}

def handler_getForegroundAppInfo(data) {
	log_debug("handler_getForegroundAppInfo: got: $data")
    
    // 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")
        log_info("Received POWER DOWN notification.")
        return
    }
    
	powerEvent("on", "physical")
    
    def appId = data.appId
    def niceName = appId
    state.nameToInputId.each { name, id ->
        if (appId == id) niceName = name
    }
    
    def descriptionText = "${device.displayName} channelName is ${niceName}"
    if (txtEnable) log_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() {
    sendWebosCommand(uri: 'tv/getChannelProgramInfo', id: 'getChannelProgramInfo')
}

def handler_getChannelProgramInfo(data) {
    log_debug("handler_getChannelProgramInfo: got: $data")
    
    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 = "${device.displayName} full channel number is ${lastChannel.majorNumber}-${lastChannel.minorNumber}"
    sendEvent(name: "channel", value: lastChannel.majorNumber)
    if (txtEnable) log_info "${descriptionText}" 
    
    descriptionText = "${device.displayName} channelName is ${lastChannel.name}"
    sendEvent(name: "channelName", value: lastChannel.name, descriptionText: descriptionText)
    if (txtEnable) log_info "${descriptionText}" 
}

def genericHandler(json) {
	log_debug("genericHandler: got json: ${json}")
}

def deviceNotification(String notifyMessage) {
    def icon_info = notifyMessage =~ /^\[(.+?)\](.+)/
    log_info "deviceNotification(): new message $notifyMessage found icon: ${icon_info != null}"
    if (!icon_info) {      
        sendWebosCommand(uri: "system.notifications/createToast",
                         payload: [message: notifyMessage])
    } else {
        log_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) {
    state.icon_data[icon_name] = data
}

def clearIcons() {
    state.icon_data = [:]
}

def notificationIcon(String notifyMessage, String icon_name) {
    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 {
            log_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,
                ])
            })
            /*
            def postParams = [
                uri: address_card_base_url,
                requestContentType: "image/png",
                timeout: 10]
            asynchttpGet('handleIconResponse', postParams, [
                icon_extention: icon_extention,
                icon_name: icon_name,
                notify_message: notifyMessage,
                start_time: now(),
            ])
            */
        } catch (Exception e) {
            log.warn "notificationIcon(): Failed to fetch icon: ${e.message} sending blank"
            deviceNotification("<Failed to find icon: ${e.message}>${notifyMessage}")
        }
    } else {
        String icon = state.icon_data[icon_name]
        log_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) {
    int n = resp.data?.available()
    log_info "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()
    log_debug "handleIconResponse(): size of b64: ${base64String.size()}"
    
    state.icon_data[data.icon_name] = base64String
    notificationIcon(data.notify_message, data.icon_name)
}

def on()
{
	log_info "on(): Executing 'Power On'"
	powerEvent("on")
    def mac = settings.televisionMac ?: state.televisionMac
    if (!mac) {
        log_error "No mac address know for TV, can't send wake on lan"
        return
    }
	log_debug "Sending Magic Packet to: $mac"
	def result = new hubitat.device.HubAction (
       	"wake on lan $mac",
       	hubitat.device.Protocol.LAN,
       	null,[secureCode: “0000”]
    )
    log_info "Sending Magic Packet to: " + result
	
    return result
}

def off()
{
	log_info "off(): Executing 'Power Off'"
	powerEvent("off")

    sendWebosCommand(uri: 'system/turnOff')
}

def channelUp() 
{
	log_info "channelUp(): Executing 'channelUp'"
    sendWebosCommand(uri: 'tv/channelUp')
}

def channelDown() 
{
	log_info "channelDown(): Executing 'channelDown'"
    sendWebosCommand(uri: 'tv/channelDown')
}


// handle commands
def volumeUp() 
{
	log_info "volumeUp(): Executing 'volumeUp'"
    sendWebosCommand(uri: 'audio/volumeUp')
}

def volumeDown() 
{
	log_info "volumeDown(): Executing 'volumeDown'"
    sendWebosCommand(uri: 'audio/volumeDown')
}

def setVolume(level) {
	log_info "setVolume(): Executing 'setVolume' with level '${level}'"
    sendWebosCommand(uri: 'audio/setVolume', payload: [volume: level])
}

def setLevel(level) { setVolume(level) }

def sendMuteEvent(muted) {
    def descriptionText = "${device.displayName} mute is ${muted}"
    if (txtEnable) log_info "${descriptionText}" 
    sendEvent(name: "mute", value: muted, descriptionText: descriptionText)
}

def unmute() {
    log_info "Executing mute false"
    sendWebosCommand(uri: 'audio/setMute', payload: [mute: false], callback: { json ->
        log_debug("unmute(): reply is $json")
        if (json?.payload?.returnValue) sendMuteEvent("unmuted")
    })
}

def mute() {
    log_info "Executing: mute true"
    sendWebosCommand(uri: 'audio/setMute', payload: [mute: true], callback: { json ->
        log_debug("mute(): reply is $json")
        if (json?.payload?.returnValue) sendMuteEvent("muted")
    })
}

def externalInput(String input)
{
    if (state.nameToInputId && state.nameToInputId[input]) input = state.nameToInputId[input]
    
    sendWebosCommand(uri: "system.launcher/launch", payload: [id: input], callback: { json ->
        log_debug("externalInfo(): reply is $json")
    })
}

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

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

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

def home()
{
    log_debug("OLD Inputs: ${state.inputList} total length: ${state.toString().length()}")

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

def sendCommand(cmd)
{
    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 sendWebosCommand(Map params) 
{
	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)
	
	log_debug("Sending: $json storing callback: $id")
	
	callbacks[id] = cb
	
	interfaces.webSocket.sendMessage(json)
	
	log_debug("sendWebosCommand sending json: $json")
}

private void parseStatus(state, json) {
    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
        log_warn("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) {
            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
                    powerEvent("off", "physical")
                    log_info("Received POWER DOWN notification.")
                }
            }
        }
    }
}

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

that worked like a charm. thank you very much. and myself i don't need the mouse driver either (i really wish LG would kill that thing to be honest)

1 Like

What’s the primary difference between this driver and the previous version?

I changed the way the driver figures out if the TV is on (see below quote) and I disabled the mouse driver since I don't care about it.

Switch applies to every time the TV turns on or off either through HE or physically (e.g. with the remote). Power is only physically. So if the TV is on and you turn it off through HE, switch would become off while power would stay on.

What issue are you having?

Here's an issue I'm having... and it may be because I'm not understanding the relationship between "switch" and "power"

If I send a command to the TV from HE to turn the switch on, the "switch" comes on right away and the "power" comes on a few seconds later. If I send a second command to the TV before "power" comes on, the command is often ignored. My automation therefore sends a command to turn on the "switch" and then waits for "power" to come on before trying to change the input. The problem is I get inconsistent results. Sometimes "power" reports in within a few seconds, sometimes it seems to take a lot longer. Sometimes the second command to change the external input works fine, sometimes it is ignored.

Oh and I really hate this LG TV.

Here's what the log shows when the rule does not work correctly...

dev:10592020-11-18 12:28:44.275 pm debugfailure: No route/connect timeout/no pong for websocket protocol

dev:10592020-11-18 12:28:44.274 pm debugfailure message from web socket failure: connect timed out

dev:10592020-11-18 12:28:44.272 pm debugwebSocketStatus: State: [initialize] Reported Status: [failure: connect timed out]

So I never use on or off from HE but I'm familiar with the driver so can comment on what's happening.

When you turn on the TV from HE, there are 3 different events that happen:

  1. Switch turns to "on" instantly.
  2. Wake on lan command is sent to turn the TV on. This has some delay.
  3. The driver tries to reconnect to the TV with a default reconnect rate of 1 minute. Once this succeeds, the "power" changes to "on". So if it had tried to connect 1s before the TV turn on, you'd have to wait 59s for "power" to change to "on". So in that scenario, "power" would switch to "on" 59s + however seconds to actually turn on.

The reason why the second command works at times is because the TV might already be on (before the "power" reflects it) because of the delay in 3.

What I'd recommend is reducing the reconnect delay to maybe 30 seconds and using only the "power" in your rules. There'd still be some delay but won't be inconsistent.

Thanks! That makes sense. I will experiment a bit. I could j just default to using 1 or 2 second delays but this seemed more elegant on the surface.

Don't use 1 or 2 seconds... You'd get hub slowdowns

Is there any way to stop this never-ending log repeating situation?

dev:2902020-12-07 10:26:47.617 am infowebsocket reconnect - delay = 30
dev:2902020-12-07 10:26:47.610 am debugfailure: No route/connect timeout/no pong for 
websocket protocol
dev:2902020-12-07 10:26:47.608 am debugfailure message from web socket failure: No route to host (Host unreachable)
dev:2902020-12-07 10:26:47.606 am debugwebSocketStatus: State: [initialize]   Reported Status:[failure: No route to host (Host unreachable)]
dev:2902020-12-07 10:26:44.514 am debugConnecting websocket to: "ws://192.xxx.x.x:3000/"
dev:2902020-12-07 10:26:44.512 am infoFailed to get mouse dev: com.hubitat.app.exception.UnknownDeviceTypeException: Device type 'LG Mouse WebSocket Driver' in namespace 'asj' not found
dev:2902020-12-07 10:26:44.498 am debugLG_TV_Mouse_192.168.1.208
dev:2902020-12-07 10:26:44.484 am debugLG Smart TV Driver - initialize - settings:['televisionIp':'192.168.1.208', 'televisionMac':'2Cxxxxxxx', 'pairingKey':'59a1a23a1164e3dcd0xxxxxxx', 'retryDelay':'30', 'logEnable':true, 'logInfoEnable':true, 'txtEnable':true]
dev:2902020-12-07 10:26:44.481 am infoLG Smart TV Driver - initialize - ip: 192.168.1.208  mac: 2C2Bxxxxxxxxx  key: 59a1a23a11xxxxxxxxxxxxxxxxxxxxxxxx debug: null logText: null
dev:2902020-12-07 10:26:14.427 am infowebsocket reconnect - delay = 30

@asj is this expected behavior? Is something misconfigured?

I just wanted to post a big thank you to everyone who has contributed to this driver and app. I am a first time Hubitat user with a new C7 and have been finding my way through the product, and drivers like this make everything easier. (I have a lot of Google/Nest products so am somewhat locked out of many of the automations I would like to use)

Using this driver I have been able to monitor for my LG CX turning on and to trigger my Lutron Caseta lights to go to a certain level.

The main struggle I had was getting the rule to run in a timely fashion. Often it could take up to a minute to dim the lights after turning on the TV. I think I have worked out that this was due to the default frequency the C7 was trying to make a connection. When I set it to 5 seconds, down from 60, it has improved the speed no end. Still not instant, but no more than 10 seconds now.

Here is a quick overview of my Rule: (very simple, but it is my first one)

If anyone has any advice on how to improve upon it and make it faster, I would be very grateful.

Are you using WiFi or Ethernet? The tv I have connected to Ethernet stays connected and reports status almost instantly. The WiFi connected tv drops the driver and takes some time to reconnect when powering up.

We are wifi currently but want to get Ethernet run in the house. Only thing to consider is the port is only 10/100, but I will only be using it to connect the Hubitat, all streaming is done through Google Tv with Chromecast

Remove Device issue:
My TV was discovered twice for some reason and I cannot remove either of them. The button is greyed out. Does anyone know how to remove it without removing the driver and thus breaking all my rules?

@asj if I run an Ethernet instead of Wi-Fi, would that stop this constant pinging and error throwing?

Do you have the logging options in the driver disabled? Looks like there’s debug there and those I’m sure are disable able in the code I wrote.

Otherwise just comment the lines out in code.

I might have logging enabled, and can obviously disable it, but doesn't that just treat the symptom instead of the problem? The device would still be bogging down the hub with all these operations, just without telling me, no? Or is it only going nuts because I have logging enabled?

It's fine. The back off is 10-30s can't remember off hand. Trying to open a socket is incredibly light work, even in Groovy+JVM+Slow ARM CPU. It does have to schedule work, which on this system is probably a db write, so that's probably the grunt of the work, but even that should be minimal. I designed the backoff times to try and be a compromise between responsiveness and essentially no system load.

Suddenly it seems my on command isn't working. I did raise the reconnect timer recently, would that do anything? Also, would switching to Ethernet help the driver at all, impact the on action, or reduce the constant pinging on the hub? It wouldn't be hard to run an Ethernet to it if it would help.

My TV does support wake on lan, but I don't think it does over Wi-Fi, so I might run an strength and use a wake on lan plugin instead of turning it on with this (otherwise great) driver, if I can't get on working again, or if it would be faster than the current way