[PORT] LG Smart TV Discovery 2012+

Is this app available on Hubitat Package Manager? That will be really nice if it can be made available there so that we can easily get the latest version installed.

I wonder if something changed on the Hubitat? Just tested with my tester hub and works with my old LG tv. Hubitat so is ancient, tv is old….but maybe Hubitat has stopped send WoL frames.

Or have you put in a smarter home AP/switch and it’s filtering these frames?

There is a fork of the app and drivers available in the HPM.

It’s also possible to browse available packages with @bptworld’s package explorer app (conveniently available in HPM as well).

I don't think so, but who knows. I just tried this WOL app, and it works for me.

For everyone who has the "ON" problem may want to try a work-around where turning on the TV through the LGTV app fires off the WOL device -- not elegant, but it'll work.

@asj, I ended up using WOL (per above) to work around the On problem. Not elegant, but it works. Unless I noticed that "Home" doesn't work either. I tried every other command in the driver and they all work, except for On and Home. And I don't see a workaround for Home.

I actually wonder if LG changed their interface somehow.

Any ideas? Would it be helpful if I shared logging?

So far, it has not found my C1 set. Could you add a function where I can tell it the IP address. That always works.

It showed no updates.

I am using LAN and it was still not found. You should be able to just program the IP address. That always works.

This is, by far one of my favorite apps I have found on Hubitat! Thank you. Im having a little issue where I can only add one TV, it does find both but once one is set up it goes through without sending the on screen prompt to the second? Also it is stubborn and will only find one TV at a time :man_shrugging: Any ideas?

Also is there a way to use the 'Pause' and 'Play' buttons?

Just running into this also, I can add either tv at one time but not both together. The child device is created for the second set and is missing only the pairing key. I’ve tried downloading some 3rd party LG Remote apps hoping that they would expose their own pairing key and I could just copy/paste but nada so far. No apps I’ve tried seem to show it but there are many on the App Store I have not tried. This may work… otherwise, like you, appreciate any suggestions. If you figure it out please do let me know!

Actually just occurred to me:

-Pair TV 1, go into child device, copy pairing key.
-Delete app which deletes child devices then reinstall and pair TV 2.
-Pair TV 1 again, it will fail pairing but your key should still be save by the TV. Copy the key you saved into TV 1 child device.
-Profit?

Seems logically like it might work, will try tomorrow.

So, after a little play I have found a workaround. Seems that when both devices are created it does not like initializing them immediately so I have written a rule to run when the power is turned on to wait 60 seconds then custom run the initialize button which then forces the TV to connect. Otherwise it just spams the logs if you retry every minute or so.

Bit of a work around but it works for me.

Hi, can someone please explain how do I obtain a pairing key?

I'm having the same issue. Any luck?

@yototogblo Can you please explain what exactly did you change in the code? as my C9 also goes with the turn on/off commands

It's been over a year so don't remember what exactly I changed. I made a bunch of changes though and my TV has been working flawlessly since then so I haven't revisited this. I did disable the mouse driver as I never use that.

Below is my code:

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

I can't seem to remember what I did to get the pairing key. I'll let you know if it'll pop up again

Thanks. Unfortunately if didn't improve my situation. I'm not really sure why my simple command is not working. The on/off on the lg device app works and the on/off of my Govee strip device app also works, but a simple rule that says to turn on/off the strip if the tv is turned on/off doesn't work. it's really strange...

What exactly is the issue? Is your TV not paired? Does Hubitat detect the on off states? Have you put in the IP address of your TV? Is a separate app the issue?

I installed Govee strip lights on my LG C9. Download drivers for the govee strip & for the LG webOS (Which is now with your revised code).

Both work well separately. On the LG app I can turn volume up and down and turn on or off the tv from HE app.

Now I'm trying to make a rule in which when the LG is turned on/off the Govee does the same, and I can't seem to get a get them to work together when trying to setup this with Simple Automation Rules or with RM. In RM I tried "Switch", "Digital Switch" and "Physical switch". Nothing works.