[RELEASE] LGTV with webOS

LOL no sooner did I say “all good”, that I started getting a string of errors in the log. Note, the TV at issue wasn’t/isn’t even powered on (nobody was in the house all day today), so I’m not sure what triggered this or what it means:

Hi guys !

I've just installed this driver and it works fine.

I noticed that, after setting the "Amazon Echo Skill" to use the TV device, I can only turn it on/off with Alexa.

Am I doing something wrong? Or do I need to develop a "wrapper" device to expose the commands (e.g. open Netflix) to Alexa?

Thanks in advance.

Just a heads-up that cost me half an hour of searching: If you rename your connection (My PC is connected to HDMI-4), it also changes in the Activities...

So 'activity' HDMI4 was missing and didn't do anything when I tried it... I have to use 'Pc' and then it switches correctly! :sweat_smile:

And @dandanache : Thanks for this awesome piece of code! :heart_on_fire:

1 Like

Just did some more testing... Switching 'On' doesn't work, but sending a WOL packet from my firewall's dhcp-server worked... After that I could send a 'screen on'...

I can see (in the debug log) that the driver tries to send a WOL packet, but the TV doesn't seem to respond to it, but responded instantly to the firewall's WOL?
(The Wired MAC address in Hubitat matches the MAC address in the firewall, the WiFi is OFF)

Anything I can do to troubleshoot why the WOL packet from the driver doesn't work, and the firewall turns it on instantly?

1 Like

I am not much of a network guy, but I believe the WOL package is filtered by one of the devices/layers (switch/router/wlan/firewall/etc) between the Hubitat hub (that sends the WOL message) and the TV. I remember that there was another case like this in this thread.

That's not the problem, as the pfSense firewall - whose WOL does work - is attached to the same switch the C8 sits on.
I have since installed the separate WOL app (by RamDev) on my C8, and its WOL wakes the TV as well, so I now have a (kludgey) workaround:

  • Send WOL packet from RamDev's APP
  • Send Power On from the LG device
  • Send Activity "Pc" from the LG device

So there's something different about how you send the WOL packet and how RamDev does it...

1 Like

thanks you so much for this.

it works great but I have only a small issue.

current activity remains stuck even when tv is off.
This makes me unable to use a certain app as a trigger as if i turn on the tv fast and open the same app the activity remains unchanged.

Can you please make it blank the activity when is off or change it to null or something?

I'd greatly appreciate it, thanks

1 Like

Released version 1.8.2 with the following small change:

Fixed

  • Change current activity to off when device is turned off - @ady.adrianu

Have fun!

1 Like

thank you but i have another issue
i tried to connect it to a 2nd tv, model is lg c5 but the tv doesn't seem to pop permission asking , it worked straight away on my c2 , i did the exact same thing yet it cant seem to connect to my c5 , any idea?
I can ping the ip just fine from cmd or hubitat networking and is reachable
Is it possible LG changed their api or something? c2 is running web os23 while c5 is on web os 25

2025-11-09 15:51:00.821info
Lg websocket is offline [physical]
dev:17162025-11-09 15:51:00.819debug
Lg ▶ webSocketStatus(status: closing)
dev:17162025-11-09 15:51:00.782debug
Lg ◀ Sending websocket messages: [type:hello, id:hubitat_1762696260782, sourceId:hubitat.hub]
dev:17162025-11-09 15:51:00.779debug
Lg ▶ webSocketStatus(status: open)
dev:17162025-11-09 15:51:00.697info
Lg websocket is connecting [digital]
dev:17162025-11-09 15:51:00.584debug
Lg ▲ Connecting to 192.168.50.116 ...
dev:17162025-11-09 15:51:00.405debug
Lg pinging 192.168.50.116 ...
dev:17162025-11-09 15:50:55.510info
Lg websocket is offline [physical]
dev:17162025-11-09 15:50:55.507debug
Lg ▶ webSocketStatus(status: closing)
dev:17162025-11-09 15:50:55.476debug
Lg ◀ Sending websocket messages: [type:hello, id:hubitat_1762696255475, sourceId:hubitat.hub]
dev:17162025-11-09 15:50:55.473debug
Lg ▶ webSocketStatus(status: open)
dev:17162025-11-09 15:50:55.382info
Lg websocket is connecting [digital]
dev:17162025-11-09 15:50:55.271debug
Lg ▲ Connecting to 192.168.50.116 ...
dev:17162025-11-09 15:50:55.091debug
Lg pinging 192.168.50.116 ...
dev:17162025-11-09 15:50:50.420info
Lg websocket is offline [physical]
dev:17162025-11-09 15:50:50.418debug
Lg ▶ webSocketStatus
(status: closing)

so far i tried factory reset, changing region , changing ip and many other things, no damn prompt on the tv, spent half a day and couldn't get the c5 to connect as c2. I restested c2 and it prompts the connection right away when clicking save on ip, but c5, nothing... m-a disperat

yeah seems to be a known problem on home assistant too

the fix seems to be here Fix system info by thecode · Pull Request #501 · home-assistant-libs/aiowebostv · GitHub

hopefully you can adapt with the new way of pairing ,I can test it for you, thank you

So after several hours of messing with the code and talking to chat gpt I managed to fix it myself applying home assistant solution , TV pairing prompt now works and after that everything works as normal. Be aware that my code sets the ping interval in seconds not minutes as I wanted faster response, everything else is unchanged.
Please update the app asap as the new webos version is rolling to older models too, 5 and 4 series already got it.
Here's the fully working code, dai o bere, dupa ce dau si eu pt driver :smiley:

/**
 * LGTV with webOS
 *
 * @see https://codeberg.org/dan-danache/hubitat/src/branch/main/lgtv-drivers
 */
import groovy.json.JsonBuilder
import groovy.transform.CompileStatic
import groovy.transform.Field

import hubitat.device.HubAction
import hubitat.device.Protocol
import hubitat.helper.NetworkUtils

import com.hubitat.app.ChildDeviceWrapper

@Field static final String DRIVER_NAME = 'LGTV with webOS'
@Field static final String DRIVER_VERSION = '1.8.2'

@Field static final List<String> PICTURE_MODES = ['cinema', 'eco', 'expert1', 'expert2', 'game', 'normal', 'photo', 'sports', 'technicolor', 'vivid', 'hdrEffect', 'filmMaker', 'hdrCinema']
@Field static final List<String> SOUND_MODES = ['aiSoundPlus', 'aiSound', 'standard', 'news', 'music', 'movie', 'sports', 'game']
@Field static final List<String> SOUND_OUTPUT = ['tv_speaker', 'external_arc', 'external_optical', 'bt_soundbar', 'mobile_phone', 'lineout', 'headphone', 'tv_speaker_bluetooth']
@Field static final Integer MAX_FAST_PING = 20

metadata {
    definition(name:DRIVER_NAME, namespace:'dandanache', author:'Dan Danache', importUrl:'https://codeberg.org/dan-danache/hubitat/raw/branch/main/lgtv-drivers/lgtv-with-webos.groovy') {
        capability 'Actuator'
        capability 'Initialize'
        capability 'Refresh'
        capability 'Switch'
        capability 'AudioVolume'
        capability 'TV'
        capability 'Notification'
        capability 'MediaController'
        capability 'MediaTransport'
        capability 'ImageCapture'
        capability 'SpeechSynthesis'

        attribute 'websocket', 'enum', ['connecting', 'online', 'offline']
        attribute 'channelName', 'string'
        attribute 'screen', 'enum', ['on', 'off', 'standby', 'screensaver']
        attribute 'pictureBrightness', 'number'
        attribute 'pictureContrast', 'number'
        attribute 'pictureColor', 'number'
        attribute 'pictureBacklight', 'number'
        attribute 'pictureMode', 'enum', PICTURE_MODES
        attribute 'soundOutput', 'enum', SOUND_OUTPUT
        attribute 'soundMode', 'enum', SOUND_MODES
        attribute 'pingInterval', 'number'
    }

    command 'deviceNotification', [
        [name:'Text*', type:'STRING', description:'Notification text*'],
        [name:'Type', type:'ENUM', description:'Notification type', constraints:['Toast - Goes away after few seconds', 'Alert - Stays on screen until dismissed']]
    ]
    command 'setChannel', [[name:'Channel number*', type:'STRING']]
    command 'screenOn'
    command 'screenOff'
    command 'setPicture', [
        [name:'Brightness', type:'NUMBER', description:'Picture brightness (1 to 100)'],
        [name:'Contrast', type:'NUMBER', description:'Picture contrast (1 to 100)'],
        [name:'Color', type:'NUMBER', description:'Picture color (1 to 100)'],
        [name:'Backlight', type:'NUMBER', description:'Picture backlight (1 to 100)'],
        [name:'Mode', type:'ENUM', description:'Select picture mode', constraints:PICTURE_MODES.sort()]
    ]
    command 'setPingInterval', [[name:'Interval*', type:'NUMBER', description:'How often to check if the TV was turned on using the remote (in seconds, 0=disabled)']]
    command 'setSoundOutput', [[name:'Output*', type:'ENUM', description:'Select sound output', constraints:SOUND_OUTPUT.sort()]]
    command 'startVideo', [[name:'URL*', type:'STRING', description:'URL of video file to play']]
    command 'startWebPage', [[name:'URL*', type:'STRING', description:'URL to open in Web Browser']]
    command 'screenSaverOn'
    command 'screenSaverOff'
    command 'pushRemoteButtons', [[name:'TV Remote buttons sequence <a href="https://codeberg.org/dan-danache/hubitat/src/branch/main/lgtv-drivers/README.md#tv-remote-control" target="_blank" title="Learn more about this">📖</a> *', type:'STRING']]
	command "resetPairingKey"

    preferences {
        input(
            name:'helpInfo', type:'hidden',
            title:"""
            <div style="min-height:55px; background:transparent url('https://codeberg.org/dan-danache/hubitat/raw/branch/main/lgtv-drivers/img/lgtv.webp') no-repeat left center;background-size:auto 55px;padding-left:65px">
                LGTV with webOS <small>v${DRIVER_VERSION}</small><br>
                <small><div>
                • <a href="https://codeberg.org/dan-danache/hubitat/src/branch/main/lgtv-drivers/README.md" target="_blank">driver details</a><br>
                • <a href="https://community.hubitat.com/t/release-lgtv-with-webos/148892" target="_blank">community page</a><br>
                </div></small>
            </div>
            """
        )
        input(
            name:'logLevel', type:'enum', title:'Log verbosity', required:true,
            description: 'Choose the messages that appear in the "Logs" section',
            options: [
                '1' : 'Debug - log everything',
                '2' : 'Info - log important events',
                '3' : 'Warning - log events that require attention',
                '4' : 'Error - log errors'
            ],
            defaultValue: '1'
        )
        input(
            name:'ipAddr', type:'string', title:'IP address', required:true,
            description: 'Enter the device IP address'
        )
        input(
            name:'useSSL', type:'bool', title:'Enable SSL', required:true,
            description:'Enable SSL when connecting to the TV websocket',
            defaultValue:true
        )
        input(
            name:'simulateRemote', type:'bool', title:'Simulate TV Remote', required:true,
            description:'Allows you to simulate TV Remote button pushes (creates a child device)',
            defaultValue:false
        )
    }
}

// Called when the device is first added
void installed() {
    log_warn 'Installing device ...'

    // Init state
    state.activities = [:]
    utils_sendEvent name:'switch', value:'off', descriptionText:'Power initialized to off', type:'digital'
    utils_sendEvent name:'websocket', value:'offline', descriptionText:'Websocket initialized to offline', type:'digital'
    setPingInterval 5
}

// Called when the "Save Preferences" button is clicked
void updated(boolean auto = false) {
    log_info "Saving preferences${auto ? ' (auto)' : ''} ..."

    if (logLevel == null) {
        logLevel = '1'
        device.updateSetting 'logLevel', [value:logLevel, type:'enum']
    }
    if (logLevel == '1') runIn 1800, 'logsOff'
    log_info "🛠️ Log verbosity = ${logLevel}"

    // Update device network Id
    if (ipAddr != null) {
        device.deviceNetworkId = ipAddr.tokenize('.').collect { String.format('%02X', it.toInteger()) }.join()
        connect()
        log_info "🛠️ IP address = ${ipAddr}"
    }

    if (useSSL == null) {
        useSSL = true
        device.updateSetting 'useSSL', [value:useSSL, type:'bool']
    }
    log_info "🛠️ Enable SSL = ${useSSL}"

    if (simulateRemote == null) {
        simulateRemote = false
        device.updateSetting 'simulateRemote', [value:simulateRemote, type:'bool']
    }
    log_info "🛠️ Simulate TV Remote = ${simulateRemote}"
    ChildDeviceWrapper childDevice = getChildDevice "${device.deviceNetworkId}-Remote"
    if (simulateRemote) {
        childDevice = childDevice ?: addChildDevice('dandanache', 'LGTV Remote', "${device.deviceNetworkId}-Remote", [name:'LGTV Remote', label:"${device.displayName} - Remote", isComponent:true])
        log_info "🛠️ TV Remote child device = ${childDevice}"
    } else {
        if (childDevice) {
            log_debug "🛠️ Removing child device ${childDevice} ..."
            deleteChildDevice "${device.deviceNetworkId}-Remote"
        }
    }
}
void setPingInterval(BigDecimal pingInterval) {
    if (pingInterval < 0) {
        log_error("Invalid ping interval specified: ${pingInterval} minutes")
        return
    }

    // Update attribute
    int newValue = pingInterval.toInteger()
    utils_sendEvent name:'pingInterval', value:newValue, unit:'seconds', descriptionText:"Ping interval is ${newValue} min ${newValue == 0 ? ' (disabled)' : ''}", type:'digital'

    // Schedule periodic device ping
    unschedule('pingDevice')
    if (newValue == 0) return
    schedule "0/${newValue} * * ? * * *", 'pingDevice'

    // Ping right now
    pingDevice()
}
void pingDevice() {
    if (!ipAddr || "${device.currentValue('websocket', true)}" == 'online') return
    log_debug "Pinging ${ipAddr} ..."
    if (NetworkUtils.ping(ipAddr, 1)?.packetsReceived > 0) connect()
}
void logsOff() {
    log_info '⏲️ Automatically reverting log level to "Info"'
    device.updateSetting 'logLevel', [value:'2', type:'enum']
}

// ===================================================================================================================
// Implement Capabilities
// ===================================================================================================================

// capability.Initialize
// This method will run when the hub starts
void initialize() {
    state.lastTx = 0
    state.lastRx = 0
    connect(true)
}

// capability.Refresh
void refresh() {
    log_debug '🎬 Refreshing device state ...'

    // We also have subscription for this values
    utils_sendMessage([type:'request', uri:'ssap://audio/getVolume'])
    utils_sendMessage([type:'request', uri:'ssap://tv/getCurrentChannel'])

    // These values rarely change, so we only request them on demand
    utils_sendMessage([type:'request', uri:'ssap://system/getSystemInfo'])
    utils_sendMessage([type:'request', uri:'ssap://com.webos.service.update/getCurrentSWInformation'])
    utils_sendMessage([type:'request', uri:'ssap://settings/getSystemSettings', payload:[category:'network', keys: ['deviceName']]])
    utils_sendMessage([type:'request', uri:'ssap://config/getConfigs', payload:[configNames:['tv.nyx.*']]])

    // https://github.com/JPersson77/LGTVCompanion/blob/master/Docs/Commandline.md
    // https://github.com/chros73/bscpylgtv/blob/master/docs/available_settings_CX.md
    //utils_sendMessage([type:'request', uri:'ssap://settings/getSystemSettings', payload:[category:'sound', keys: ['soundMode']]])
    //utils_sendMessage([type:'request', uri:'ssap://config/getConfigs', payload:[configNames:['audio.*']]])
}

// capability.Switch
void on() {
    log_debug '🎬 Powering on ...'
    util_wakeOnLan getDataValue('wifiMacAddress')
    util_wakeOnLan getDataValue('wiredMacAddress')
    if (ipAddr) util_wakeOnLan getMACFromIP(ipAddr)

    // Start fast pinging the IP for a maximum of MAX_FAST_PING times
    runIn 1, 'fastPing', [data:[currentRetry:0]]
}
private void fastPing(Map data) {
    data.currentRetry += 1
    if (!ipAddr || "${device.currentValue('switch', true)}" == 'on' || data.currentRetry > MAX_FAST_PING) {
        log_debug 'Fast ping terminated'
        return
    }

    log_debug "Fast pinging ${ipAddr}: ${data.currentRetry} / ${MAX_FAST_PING} ..."
    if (NetworkUtils.ping(ipAddr, 1)?.packetsReceived > 0 && "${device.currentValue('switch', true)}" != 'on') connect()

    // Keep fast-pinging until switch changes to 'on' or MAX_FAST_PING limit is reached
    runIn 1, 'fastPing', [data:data]
}
void off() {
    log_debug '🎬 Powering off ...'
    utils_sendMessage([type:'request', uri:'ssap://system/turnOff'])
    utils_sendEvent name:'switch', value:'off', descriptionText:'Power is off', type:'digital'
}

// capability.AudioVolume
void mute() {
    log_debug '🎬 Muting sound ...'
    utils_sendMessage([type:'request', uri:'ssap://audio/setMute', payload:[mute:true]])
}
void unmute() {
    log_debug '🎬 Unmuting sound ...'
    utils_sendMessage([type:'request', uri:'ssap://audio/setMute', payload:[mute:false]])
}
void volumeUp() {
    log_debug '🎬 Raising volume ...'
    utils_sendMessage([type:'request', uri:'ssap://audio/volumeUp'])
}
void volumeDown() {
    log_debug '🎬 Lowering volume ...'
    utils_sendMessage([type:'request', uri:'ssap://audio/volumeDown'])
}
void setVolume(String level) { setVolume Integer.parseInt(level) }
void setVolume(BigDecimal level) {
    log_debug "🎬 Setting volume to ${level}% ..."
    if (level < 0 || level > 100) return
    utils_sendMessage([type:'request', uri:'ssap://audio/setVolume', payload:[volume:level]])
}

// capability.TV
void channelUp() {
    log_debug '🎬 Changing channel up ...'
    utils_sendMessage([type:'request', uri:'ssap://tv/channelUp'])
}
void channelDown() {
    log_debug '🎬 Changing channel down ...'
    utils_sendMessage([type:'request', uri:'ssap://tv/channelDown'])
}

// capability.Notification
void deviceNotification(String text, String type = 'Toast') {
    log_debug "🎬 Sending ${type} notification ..."
    if (type.startsWith('Alert') || text.startsWith('!')) {
        utils_sendMessage([type:'request', uri:'"ssap://system.notifications/createAlert', payload:[message:text.replaceAll('^!', ''), buttons:[[label:'Dismiss']]]])
    } else {
        utils_sendMessage([type:'request', uri:'"ssap://system.notifications/createToast', payload:[message:text]])
    }
}

// capability.MediaController
void getAllActivities() {
    log_debug '🎬 Getting all activities ...'

    // Remove old activities
    Map<String, String> activities = [:]
    activities['com.webos.app.home'] = 'Home'
    activities['com.webos.app.livetv'] = 'Live TV'
    activities['com.webos.app.miracast'] = 'Miracast'
    state.activities = activities

    utils_sendMessage([type:'request', uri:'ssap://tv/getExternalInputList'])
    utils_sendMessage([type:'request', uri:'ssap://com.webos.applicationManager/listLaunchPoints'])
}
void getCurrentActivity() {
    log_debug '🎬 Getting current activity ...'
    utils_sendMessage([type:'request', uri:'ssap://com.webos.applicationManager/getForegroundAppInfo'])
}
void startActivity(String activityname) {
    String appId = state.activities.find { it.value == activityname }?.key ?: activityname
    log_debug "🎬 Starting activity: [${activityname}] (${appId}) ..."
    utils_sendMessage([type:'request', uri:'ssap://system.launcher/launch', payload:[id:appId]])
}

// capability.MediaTransport
void play() {
    log_debug '🎬 Play ...'
    utils_sendMessage([type:'request', uri:'ssap://media.controls/play'])
    utils_sendEvent name:'transportStatus', value:'playing', descriptionText:'Media transport status is playing', type:'digital'
}
void pause() {
    log_debug '🎬 Pause ...'
    utils_sendMessage([type:'request', uri:'ssap://media.controls/pause'])
    utils_sendEvent name:'transportStatus', value:'paused', descriptionText:'Media transport status is paused', type:'digital'
}
void stop() {
    log_debug '🎬 Stop ...'
    utils_sendMessage([type:'request', uri:'ssap://media.controls/stop'])
    utils_sendEvent name:'transportStatus', value:'stopped', descriptionText:'Media transport status is stopped', type:'digital'
}

// capability.ImageCapture
void take() {
    log_debug '🎬 Taking a screenshot ...'
    utils_sendMessage([type:'request', uri:'ssap://tv/executeOneShot', payload:[path:'/tmp/capture.jpg', method:'DISPLAY', format:'JPG']])
}

// capability.SpeechSynthesis
void speak(String text, BigDecimal volume = null, String voice = null) {
    log_debug "🎬 Speaking text [${text}] ..."
    Map result = textToSpeech(text, voice)
    log_debug "Sending TTS file ${result} ..."
    // utils_sendMessage([type:'request', uri:'ssap://com.webos.applicationManager/open', payload:[target:result.uri, mime:'audio/mp3']])
    utils_sendMessage([type:'request', uri:'ssap://com.webos.applicationManager/launch', payload:[
        id: state.activities.find { it.value == 'Media Player' || it.value == 'Music' }?.key ?: 'com.webos.app.mediadiscovery',
        params: [payload: [[
            fullPath:result.uri, mediaType:'MUSIC', deviceType:'DMR', lastPlayPosition:0,
            fileName: "Message from ${location.hub.name}",
            thumbnail: "http://${location.hub.localIP}/ui2/images/apple-touch-icon.png",
            dlnaInfo: [
                protocolInfo: 'http-get:*:audio/mpeg:DLNA.ORG_PN=MP3;DLNA.ORG_OP=01;DLNA.ORG_FLAGS=01500000000000000000000000000000',
                contentLength: '-1',
                duration: result.duration,
                opVal: 1,
                flagVal: 0,
                cleartextSize: '-1'
            ]
        ]]]
    ]])
}

// ===================================================================================================================
// Implement custom commands
// ===================================================================================================================
void setChannel(String channel) {
    log_debug "🎬 Changing channel to [${channel}] ..."
    utils_sendMessage([type:'request', uri:'ssap://tv/openChannel', payload:[channelNumber:channel]])
}
void screenOn() {
    log_debug '🎬 Turning screen on ...'
    utils_sendMessage([type:'request', uri:'ssap://com.webos.service.tvpower/power/turnOnScreen', payload:[standbyMode:'active']])
}
void screenOff() {
    log_debug '🎬 Turning screen off ...'
    utils_sendMessage([type:'request', uri:'ssap://com.webos.service.tvpower/power/turnOffScreen', payload:[standbyMode:'active']])
}
void setPicture(BigDecimal brightness, BigDecimal contrast = null, BigDecimal color = null, BigDecimal backlight = null, String mode = null) {
    log_debug "🎬 Setting picture to: brightness=${brightness}, contrast=${contrast}, color=${color}, backlight=${backlight}, mode=${mode}] ..."
    Map<String, String> settings = [:]
    if (brightness && brightness >= 1 && brightness <= 100) settings.put 'brightness', brightness.intValue().toString()
    if (contrast && contrast >= 1 && contrast <= 100) settings.put 'contrast', contrast.intValue().toString()
    if (color && color >= 1 && color <= 100) settings.put 'color', color.intValue().toString()
    if (backlight && backlight >= 1 && backlight <= 100) settings.put 'backlight', backlight.intValue().toString()
    if (mode) settings.put 'pictureMode', mode
    if (settings.size() > 0) utils_sendMessage([type:'request', uri:'ssap://settings/setSystemSettings', payload:[category:'picture', settings:settings]])
}
void setSoundOutput(String output) {
    log_debug "🎬 Setting sound output to: [${output}] ..."
    utils_sendMessage([type:'request', uri:'ssap://settings/setSystemSettings', payload:[category:'sound', settings: [soundOutput:output]]])
    //utils_sendMessage([type:'request', uri:'com.webos.service.apiadapter/audio/changeSoundOutput', payload:[output:output]]])
}
void startVideo(String url) {
    log_debug "🎬 Playing video file: [${url}]..."

    // https://gist.github.com/aabytt/bddbb1bcf031a050d89a89aeee3a6737#playling-a-link-with-standard-lg-webos-player
    utils_sendMessage([type:'request', uri:'ssap://com.webos.applicationManager/launch', payload:[
        id: state.activities.find { it.value == 'Media Player' || it.value == 'Photo & Video' }?.key ?: 'com.webos.app.mediadiscovery',
        params: [payload: [[fullPath:url, mediaType:'VIDEO', deviceType:'DMR', lastPlayPosition:0]]]
    ]])
}
void startWebPage(String url) {
    log_debug "🎬 Opening web page: [${url}]..."
    utils_sendMessage([type:'request', uri:'ssap://com.webos.applicationManager/launch', payload:[
        id: 'com.webos.app.browser',
        params: [target:url]
    ]])
}
void screenSaverOn() {
    log_debug '🎬 Starting screen saver ...'
    utils_sendMessage([type:'request', uri:'ssap://system.launcher/launch', payload:[id:'com.webos.app.screensaver']])
}
void screenSaverOff() {
    log_debug '🎬 Stopping screen saver ...'
    utils_sendMessage([type:'request', uri:'ssap://system.launcher/close', payload:[id:'com.webos.app.screensaver']])
}
void pushRemoteButtons(String buttonSequence) {
    if (!simulateRemote) {
        log_info 'The "Simulate TV Remote" option is currently disabled in the "Preferences" tab'
        state.remove 'remoteButtonSequence'
        return
    }
    if ("${device.currentValue('switch', true)}" != 'on') {
        log_info 'Cannot push TV Remote buttons as device is not switched on'
        state.remove 'remoteButtonSequence'
        return
    }

    log_debug "🎬 Pushing TV Remote buttons: ${buttonSequence} ..."
    state.remoteButtonSequence = buttonSequence.toUpperCase()
    utils_sendMessage([type:'request', uri:'ssap://com.webos.service.networkinput/getPointerInputSocket'])
}

// ===================================================================================================================
// Websocket helpers
// ===================================================================================================================

void connect(boolean force = false) {
    if (!ipAddr) {
        log_warn 'Device IP address not set in Preferences tab. Make sure you are using DHCP reservation for the TV\'s IP address!'
        return
    }

    if (!force && "${device.currentValue('websocket', true)}" == 'connecting') {
        int timeoutIn = state.containsKey('lastWx') ? state.lastWx + 25000 - now() : 0
        if (timeoutIn > 0) {
            log_debug "✋ Another connection is already in progress; it will time out in ${timeoutIn/1000} seconds"
            return
        }
    }

    log_debug "▲ Connecting to ${ipAddr} ${force ? '(forced) ' : ''}..."
    disconnect()
    interfaces.webSocket.connect(useSSL ? "wss://${ipAddr}:3001/" : "ws://${ipAddr}:3000/", headers: ['Content-Type': 'application/json'], ignoreSSLIssues: true)

    utils_sendEvent name:'websocket', value:'connecting', descriptionText:'Websocket is connecting', type:'digital'
    state.lastWx = now()

    // Send hello
    utils_sendMessage([type:'hello', id:'hello'], false)

    // NEW: Pre-registration system info for newer TVs
    Map sysInfoRequest = [type:'request', id:'pre_reg_sys_info', uri:'ssap://system/getSystemInfo', payload:[:]]
    utils_sendMessage(sysInfoRequest, false)
    
    // Delay registration slightly to ensure system info arrives first
    runIn(1, 'register')
}


void disconnect() {
    try { interfaces.webSocket.close() } catch (e) { }
}

/**
 * Resets the pairing key so that the TV prompts for pairing again.
 */
void resetPairingKey() {
    log_warn "🔑 Resetting stored client-key and forcing re-pairing..."
    
    // Clear saved key
    state.remove('pk')

    // Disconnect and reconnect to trigger new pairing
    disconnect()
    pauseExecution(1000)
    connect(true)

    // Notify user
    deviceNotification("Hubitat pairing reset. Please accept the new pairing request on your TV.")
}



void register() {
    if (state.pk) {
        utils_sendMessage([type:'register', payload:['client-key':state.pk]], false)
        return
    }

    utils_sendMessage([
        type: 'register',
        payload: [
            pairingType: 'PROMPT',
            manifest: [
                appVersion: '1.1',
                manifestVersion: 1,
                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', 'READ_SETTINGS', 'CONTROL_TV_SCREEN', 'CONTROL_TV_STANBY', 'CONTROL_FAVORITE_GROUP', 'CONTROL_USER_INFO', 'CHECK_BLUETOOTH_DEVICE', 'CONTROL_BLUETOOTH', 'CONTROL_TIMER_INFO', 'STB_INTERNAL_CONNECTION', 'CONTROL_RECORDING', 'READ_RECORDING_STATE', 'WRITE_RECORDING_LIST', 'READ_RECORDING_LIST', 'READ_RECORDING_SCHEDULE', 'WRITE_RECORDING_SCHEDULE', 'READ_STORAGE_DEVICE_LIST', 'READ_TV_PROGRAM_INFO', 'CONTROL_BOX_CHANNEL', 'READ_TV_ACR_AUTH_TOKEN', 'READ_TV_CONTENT_STATE', 'READ_TV_CURRENT_TIME', 'ADD_LAUNCHER_CHANNEL', 'SET_CHANNEL_SKIP', 'RELEASE_CHANNEL_SKIP', 'CONTROL_CHANNEL_BLOCK', 'DELETE_SELECT_CHANNEL', 'CONTROL_CHANNEL_GROUP', 'SCAN_TV_CHANNELS', 'CONTROL_TV_POWER', 'CONTROL_WOL'],
                signatures: [[
                    signatureVersion: 1,
                    signature: 'eyJhbGdvcml0aG0iOiJSU0EtU0hBMjU2Iiwia2V5SWQiOiJ0ZXN0LXNpZ25pbmctY2VydCIsInNpZ25hdHVyZVZlcnNpb24iOjF9.hrVRgjCwXVvE2OOSpDZ58hR+59aFNwYDyjQgKk3auukd7pcegmE2CzPCa0bJ0ZsRAcKkCTJrWo5iDzNhMBWRyaMOv5zWSrthlf7G128qvIlpMT0YNY+n/FaOHE73uLrS/g7swl3/qH/BGFG2Hu4RlL48eb3lLKqTt2xKHdCs6Cd4RMfJPYnzgvI4BNrFUKsjkcu+WD4OO2A27Pq1n50cMchmcaXadJhGrOqH5YmHdOCj5NSHzJYrsW0HPlpuAx/ECMeIZYDh6RMqaFM2DXzdKX9NmmyqzJ3o/0lkk/N97gfVRLW5hA29yeAwaCViZNCP8iC9aO0q9fQojoa7NQnAtw==',
                ]],
                signed: [
                    created: '20140509',
                    appId: 'com.lge.test',
                    vendorId: 'com.lge',
                    localizedAppNames: ['':'LG Remote App', 'ko-KR':'리모컨 앱', 'zxx-XX':'ЛГ Rэмotэ AПП'],
                    localizedVendorNames: ['': 'LG Electronics'],
                    permissions: ['TEST_SECURE', 'CONTROL_INPUT_TEXT', 'CONTROL_MOUSE_AND_KEYBOARD', 'READ_INSTALLED_APPS', 'READ_LGE_SDX', 'READ_NOTIFICATIONS', 'SEARCH', 'WRITE_SETTINGS', 'WRITE_NOTIFICATION_ALERT', 'CONTROL_POWER', 'READ_CURRENT_CHANNEL', 'READ_RUNNING_APPS', 'READ_UPDATE_INFO', 'UPDATE_FROM_REMOTE_APP', 'READ_LGE_TV_INPUT_EVENTS', 'READ_TV_CURRENT_TIME'],
                    serial: '2f930e2d2cfe083771f68e4fe7bb07'
                ]
            ]
        ]
    ], false)
}
void subscribe() {

    // Setup subscriptions
    utils_sendMessage([type:'subscribe', uri:'ssap://audio/getStatus'])
    utils_sendMessage([type:'subscribe', uri:'ssap://tv/getCurrentChannel'])
    utils_sendMessage([type:'subscribe', uri:'ssap://com.webos.applicationManager/getForegroundAppInfo'])
    utils_sendMessage([type:'subscribe', uri:'ssap://com.webos.media/getForegroundAppInfo'])
    utils_sendMessage([type:'subscribe', uri:'ssap://com.webos.service.tvpower/power/getPowerState'])
    utils_sendMessage([type:'subscribe', uri:'ssap://settings/getSystemSettings', payload:[category:'picture', keys:['brightness', 'contrast', 'color', 'backlight', 'pictureMode']]])
    utils_sendMessage([type:'subscribe', uri:'ssap://settings/getSystemSettings', payload:[category:'sound', keys:['soundMode', 'soundOutput']]])

    // First time with talk with this device
    if (!getDataValue('modelName')) {

        // Enable Wake on LAN
        log_debug 'Enabling Wake On LAN (WOL) ...'
        utils_sendMessage([type:'request', uri:'ssap://settings/setSystemSettings', payload:[category:'network', settings: [wolwowlOnOff:'true']]])

        // Get some device information that never changes
        utils_sendMessage([type:'request', uri:'ssap://system/getSystemInfo'])
        utils_sendMessage([type:'request', uri:'ssap://com.webos.service.connectionmanager/getinfo'])

        // Send a notification on TV
        deviceNotification "<b>Mesage from ${location.hub.name}</b><br>Well done! Configuration is now complete 👍"
    }
}

// ===================================================================================================================
// Websocket callback
// ===================================================================================================================

void webSocketStatus(String message) {
    log_debug "▶ webSocketStatus(${message})"

    String websocket = utils_parseStatus message

    // If websocket just opened, say hello (skip state checks)
    if (websocket == 'online') {
        utils_sendMessage([type:'hello', id:'hello'], false)
        return
    }

    // Update "websocket" attribute
    utils_sendEvent name:'websocket', value:websocket, descriptionText:"Websocket is ${websocket}", type:'physical'
}

def resetPairing() {
    log_warn "Manual pairing reset requested"
    try {
        interfaces.webSocket.close()
    } catch (e) { }
    state.remove('pk')
    state.remove('registered')
    state.remove('lastRegister')
    runIn(3, 'connect')
}

void parse(String description) {
    log_debug "▶ Received message: ${description}"
    state.lastRx = now()

    Map msg = parseJson(description)
    String type = msg.payload?.callerId == 'secondscreen.client' || msg.payload?.callerId == 'com.webos.service.apiadapter' || now() - (state.lastTx ?: 0) < 2000 ? 'digital' : 'physical'

    // Empty reponse. Thanks for nothing!
    if (msg.payload.keySet().size() == 1 && (msg.payload.returnValue == true || msg.payload.subscription == true)) return

    switch (msg.type) {

        // TV sent an error message. Oh noes!
     case 'error':
    def errText = msg.payload?.errorText ?: ''
    if (errText.contains('client-key does not exist')) {
        log_warn "Stored client-key invalid; removing and forcing re-pairing sequence."
        try {
            interfaces.webSocket.close()
        } catch (e) {
            log_debug "WebSocket close error (expected): ${e.message}"
        }
        state.remove('pk')
        // Give Hubitat a moment to fully close before reconnecting
        runIn(5, 'connect')
        return
    }
        
        if (msg.payload?.errorCode == 401) {
    log_warn "Authentication failed (401). Clearing pairing key."
    interfaces.webSocket.close()
    state.remove('pk')
    runIn(12, 'connect')
    return
}
            // Ignore unsupported config keys and TV bind fails
            if (msg.payload?.errorText?.startsWith('Some keys are not allowed') || msg.payload?.errorText?.startsWith('com.webos.service.utp/bind returns invalid result')) return

            // Live TV weird error
            if (msg.payload?.errorText ==~ /^broadcastId = .* is already closed$/) return

            // Service not supported on some (old) devices
            if (msg.error == '404 no such service or method') return

            case 'error':
    def errMsg = msg.payload?.errorText ?: msg.error
    if (errMsg?.contains('client-key does not exist')) {
        log_warn "⚠️ Invalid or missing client-key detected. Resetting pairing automatically..."
        resetPairingKey()
        return
    }

    // Ignore unsupported config keys and TV bind fails
    if (msg.payload?.errorText?.startsWith('Some keys are not allowed') ||
        msg.payload?.errorText?.startsWith('com.webos.service.utp/bind returns invalid result')) return

    // Live TV weird error
    if (msg.payload?.errorText ==~ /^broadcastId = .* is already closed$/) return

    // Service not supported on some (old) devices
    if (msg.error == '404 no such service or method') return

    log_error "▶ Received error message: ${errMsg}"
    return


        // Hello (request)
        case 'hello':
            log_debug '▶ Proper protocol has been observed. Starting authentication ...'

            // Start registration
            runIn 1, 'register'
            return

        // Register (request)
        case 'registered':
            log_debug '▶ Authentication complete. Oh yeah!'
            state.pk = msg.payload['client-key']

            // Update "websocket" and "switch" attributes
            utils_sendEvent name:'websocket', value:'online', descriptionText:'Websocket is online', type:'physical'
            utils_sendEvent name:'switch', value:'on', descriptionText:'Power is on', type:'physical'

            // Setup subscriptions
            runIn 1, 'subscribe'

            // Auto-sync device state with Hubitat
            runIn 7, 'refresh'
            return

        // TV sent a response to a command sent by us
        case 'response':
            Map payload = msg.payload
            switch (payload) {

                // ssap://audio/getStatus (subscription)
                case { contains it, [volumeStatus:null] }:
                    int volume = payload.volumeStatus.volume
                    String mute = payload.volumeStatus.muteStatus ? 'muted' : 'unmuted'
                    String soundOutput = payload.volumeStatus.soundOutput ?: ''
                    utils_sendEvent name:'volume', value:volume, unit:'%', descriptionText:"Sound volume is ${volume}%", type:type
                    utils_sendEvent name:'mute', value:mute, descriptionText:"Sound is ${mute}", type:type
                    utils_sendEvent name:'soundOutput', value:soundOutput, descriptionText:"Sound output is ${soundOutput}", type:type
                    return

                case { contains it, [mute:null, volume:null] }:
                    int volume = payload.volume
                    String mute = payload.mute ? 'muted' : 'unmuted'
                    utils_sendEvent name:'volume', value:volume, unit:'%', descriptionText:"Sound volume is ${volume}%", type:type
                    utils_sendEvent name:'mute', value:mute, descriptionText:"Sound is ${mute}", type:type
                    return
                
// Pre-registration system info
case { contains it, [id:'pre_reg_sys_info'] }:
    // Update modelName and other static info before registration
    utils_dataValue('modelName', payload.modelName)
    utils_dataValue('fwVersion', "${payload.product_name} / ${payload.major_ver}.${payload.minor_ver}")
    log_debug "🖥️ Pre-registration system info received: ${payload.modelName} / ${payload.major_ver}.${payload.minor_ver}"
    return

                // ssap://tv/getCurrentChannel (subscription)
                case { contains it, [channelNumber:null] }:
                    String channel = payload.channelNumber
                    String channelName = payload.channelName ?: 'unknown'
                    utils_sendEvent name:'channel', value:channel, descriptionText:"Channel is ${channel}", type:type
                    utils_sendEvent name:'channelName', value:channelName, descriptionText:"Channel name is ${channelName}", type:type
                    return

                // ssap://com.webos.applicationManager/getForegroundAppInfo (subscription)
                case { contains it, [appId:'', processId:'', windowId:''] }:
                    utils_sendEvent name:'switch', value:'off', descriptionText:'Power is off', type:type
                    utils_sendEvent name:'currentActivity', value:'off', descriptionText:"Current activity is off", type:type
                    disconnect()
                    return

                case { contains it, [appId:null] }:
                    utils_sendEvent name:'switch', value:'on', descriptionText:'Power is on', type:type
                    String currentActivity = state.activities?.find { it.key == payload.appId }?.value ?: 'unknown'
                    utils_sendEvent name:'currentActivity', value:currentActivity, descriptionText:"Current activity is ${currentActivity}", type:type
                    return

                // ssap://settings/getSystemSettings (subscription)
                case { contains it, [category:'picture'] }:
                    String pictureBrightness = payload.settings?.brightness ?: ''
                    String pictureContrast = payload.settings?.contrast ?: ''
                    String pictureColor = payload.settings?.color ?: ''
                    String pictureBacklight = payload.settings?.backlight ?: ''
                    String pictureMode = payload.settings?.pictureMode ?: ''
                    utils_sendEvent name:'pictureBrightness', value:pictureBrightness, unit:'%', descriptionText:"Picture brightness is ${pictureBrightness}%", type:type
                    utils_sendEvent name:'pictureContrast', value:pictureContrast, unit:'%', descriptionText:"Picture contrast is ${pictureContrast}%", type:type
                    utils_sendEvent name:'pictureColor', value:pictureColor, unit:'%', descriptionText:"Picture color is ${pictureColor}%", type:type
                    utils_sendEvent name:'pictureBacklight', value:pictureBacklight, unit:'%', descriptionText:"Picture backlight is ${pictureBacklight}%", type:type
                    utils_sendEvent name:'pictureMode', value:pictureMode, descriptionText:"Picture mode is ${pictureMode}", type:type
                    return

                case { contains it, [category:'sound'] }:
                    String soundOutput = payload.settings?.soundOutput
                    utils_sendEvent name:'soundOutput', value:soundOutput, descriptionText:"Sound output is ${soundOutput}", type:type

                    String soundMode = payload.settings?.soundMode
                    utils_sendEvent name:'soundMode', value:soundMode, descriptionText:"Sound mode is ${soundMode}", type:type
                    return

                // ssap://com.webos.media/getForegroundAppInfo (subscription)
                case { contains it, [foregroundAppInfo:null] }:
                    String transportStatus = utils_parseForegroundAppInfo(payload.foregroundAppInfo)
                    utils_sendEvent name:'transportStatus', value:transportStatus, descriptionText:"Media transport status is ${transportStatus}", type:type
                    return

                // ssap://tv/getExternalInputList (request)
                case { contains it, [devices:null] }:
                    Map activities = state.activities ?: [:]
                    payload.devices.each { activities[it.appId] = it.label }
                    state.activities = activities
                    runIn 5, 'updateActivities'
                    return

                // ssap://com.webos.applicationManager/listLaunchPoints (request)
                case { contains it, [launchPoints:null] }:
                    Map activities = state.activities ?: [:]
                    payload.launchPoints.each { activities[it.id] = it.title }
                    state.activities = activities
                    runIn 5, 'updateActivities'
                    return

                // ssap://settings/getSystemSettings (request)
                case { contains it, [category:'network'] }:
                    utils_dataValue 'networkName', payload.settings?.deviceName
                    return

                // ssap://com.webos.service.update/getCurrentSWInformation (request)
                case { contains it, [sw_type:null] }:
                    utils_dataValue 'fwVersion', "${payload.product_name} / ${payload.major_ver}.${payload.minor_ver}"
                    return

                // ssap://com.webos.service.tvpower/power/turnOnScreen (request)
                case { contains it, [state:'Screen Off'] }:
                    utils_sendEvent name:'screen', value:'off', descriptionText:'Screen is off', type:type
                    return

                case { contains it, [state:'Active'] }:
                    utils_sendEvent name:'screen', value:'on', descriptionText:'Screen is on', type:type
                    return

                case { contains it, [state:'Screen Saver'] }:
                    utils_sendEvent name:'screen', value:'screensaver', descriptionText:'Screen is in screensaver mode', type:type
                    return

                case { contains it, [state:'Active Standby'] }:
                    utils_sendEvent name:'screen', value:'standby', descriptionText:'Screen is in active standby', type:type
                    return

                // ssap://system/getSystemInfo (request)
                case { contains it, [modelName:null] }:
                    utils_dataValue 'modelName', payload.modelName
                    utils_dataValue 'receiverType', payload.receiverType
                    return

                // ssap://com.webos.service.connectionmanager/getinfo (request)
                case { contains it, [wifiInfo:null] }:
                case { contains it, [wiredInfo:null] }:
                    utils_dataValue 'wifiMacAddress', payload.wifiInfo?.macAddress
                    utils_dataValue 'wiredMacAddress', payload.wiredInfo?.macAddress
                    return

                // ssap://config/getConfigs (request)
                case { contains it, [configs:null] }:
                    utils_dataValue 'platformVersion', payload.configs['tv.nyx.platformVersion']
                    return

                // ssap://com.webos.service.networkinput/getPointerInputSocket (request)
                case { contains it, [socketPath:null] }:
                    getChildDevice("${device.deviceNetworkId}-Remote").pushButtons(payload.socketPath, "${state.remoteButtonSequence}")
                    state.remove 'remoteButtonSequence'
                    return

                // ssap://tv/executeOneShot
                case { contains it, [imageUri:null] }:
                    String image = payload.imageUri
                    utils_sendEvent name:'image', value:image, descriptionText:"Capture URL is ${image}", type:type
                    return

                // Pairing prompt (request)
                case { contains it, [pairingType:'PROMPT'] }:
                    log_warn '🙋‍♂️ Go and accept the pairing request on your TV screen. HAUL ■■■!'
                    return

                // ssap://audio/getVolume (request)
                case { contains it, [soundOutput:null] }:
                    String soundOutput = payload.soundOutput ?: ''
                    utils_sendEvent name:'soundOutput', value:soundOutput, descriptionText:"Sound output is ${soundOutput}", type:type

                    if (payload.containsKey('muteStatus')) {
                        String mute = payload.muteStatus ? 'muted' : 'unmuted'
                        utils_sendEvent name:'mute', value:mute, descriptionText:"Sound is ${mute}", type:type
                    }
                    return

                // Other messages we don't care about
                case { contains it, [muteStatus:null] }:
                case { contains it, [sessionId:null] }:
                case { contains it, [volume:null] }:
                case { contains it, [toastId:null] }:
                case { contains it, [alertId:null] }:
                case { contains it, [missingConfigs:null] }:
                case { contains it, [returnValue:true, method:'setSystemSettings'] }:
                    return
            }
    }

    // Unexpected websocket message
    log_error "🚩 Received unexpected websocket message: description=${description}"
}

// ===================================================================================================================
// Logging helpers (something like this should be part of the SDK and not implemented by each driver)
// ===================================================================================================================

private void log_debug(String message, String prefix = device.displayName) {
    if (logLevel == '1') log.debug "${prefix} ${message.uncapitalize()}"
}
private void log_info(String message, String prefix = device.displayName) {
    if (logLevel <= '2') log.info "${prefix} ${message.uncapitalize()}"
}
private void log_warn(String message, String prefix = device.displayName) {
    if (logLevel <= '3') log.warn "${prefix} ${message.uncapitalize()}"
}
private void log_error(String message, String prefix = device.displayName) {
    log.error "${prefix} ${message.uncapitalize()}"
}

// ===================================================================================================================
// Helper methods (keep them simple, keep them dumb)
// ===================================================================================================================

private void utils_sendMessage(Map message, boolean checkState = true) {
    if (!ipAddr) {
        log_warn 'Device IP address not set in Preferences tab. Make sure you are using DHCP reservation for the TV\'s IP Address!'
        return
    }

    if (checkState && "${device.currentValue('switch', true)}" != 'on') {
        log_info 'Device is not switched on. Command not sent.'
        return
    }

    if (checkState && "${device.currentValue('websocket', true)}" != 'online') {
        log_info 'Websocket is not connected anymore. Connecting now ...'
        runIn 1, 'connect'
        return
    }

    // Send websocket message
    state.lastTx = now()
    message.id = "hubitat_${now()}"
    message.sourceId = 'hubitat.hub'

    log_debug "◀ Sending websocket messages: ${message}"
    String payload = new JsonBuilder(message)
    interfaces.webSocket.sendMessage(payload)
}
private void utils_sendEvent(Map event) {
    if (event.value == null || event.value == '') return
    if ("${device.currentValue(event.name, true)}" != "${event.value}") {
        log_info "${event.descriptionText} [${event.type}]"
    } else {
        log_debug "${event.descriptionText} [${event.type}]"
    }
    sendEvent event
}
private void utils_dataValue(String key, String value) {
    if (value == null || value == '') return
    log_debug "Update data value: ${key}=${value}"
    updateDataValue key, value
}
private String utils_parseStatus(String message) {
    switch (message) {
        case 'status: open': return 'online'
        case 'status: closing': return 'offline'
        case ~/^failure: connect timed out.*/: return 'offline'
        case ~/^failure: unexpected end of stream on http:\/\/.*/:
            log_warn 'Failed to establish a stable websocket connection to your TV. You should enable SSL in the Preferences tab. DO IT!'
            return 'offline'
        case ~/^failure: .*/:
            log_info "Oh snap! ${message}"
            return 'offline'
        default: return 'offline'
    }
}
private String utils_parseForegroundAppInfo(List<Map> foregroundAppInfo) {
    String playState = foregroundAppInfo.find { it.windowId != '' }?.playState
    if (playState == 'loaded' || playState == 'playing') return 'playing'
    return playState == 'paused' ? 'paused' : 'stopped'
}
private void util_wakeOnLan(String macAddr) {
    if (macAddr == null || macAddr == '') return
    String cmd = "wake on lan ${macAddr.replaceAll(':', '').toUpperCase()}"
    log_debug "◀ Sending LAN command: ${cmd}"
    sendHubCommand(new HubAction(cmd, Protocol.LAN))
}
void updateActivities() {
    List<String> activities = state.activities*.value.sort()
    utils_sendEvent name:'activities', value:new JsonBuilder(activities), descriptionText:"Supported activities is ${activities}", type:type
}

// switch/case syntactic sugar
@CompileStatic private boolean contains(Map msg, Map spec) {
    return msg.keySet().containsAll(spec.keySet()) && spec.every { it.value == null || it.value == msg[it.key] }
}

Edit: Improved the code to reset the stored pairing key to fix "401 client-key does not exist"error,
also added a new Reset Pairing Key command for manual reset.
Basically when I made a new virtual device and tried to connect to the same tv I would get this error because it was already paired, now it handles this better and should automatically reset the pairing key and prompt for a new pair on tv.

I haven't tested backward compatibity for older webos versions but it should handle the old firmwares too, if someone can confirm is still works on older versions like the original code would be nice.

I'm having the same issue on a B5 I recently purchased. After I click save, no notification appears for approval.

i already posted the solution, pay attention

I am paying attention and I did see your solution. I'm just backing you up that there is an issue with the newer models/webos.

Released version 1.8.3 with the following fix:

Fixed


Note: you may need to remove and re-add the devices that failed to register properly.

Kudos to @ady.adrianu (and his Black Friday TV :slight_smile:) for raising the issue and also providing the fix :+1:

Have fun!

1 Like