/** * 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 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("${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}" } } }