Skip to content

Instantly share code, notes, and snippets.

@ahbanavi
Created August 7, 2026 14:31
Show Gist options
  • Select an option

  • Save ahbanavi/df36819131803ee2b9120fb9c15842ab to your computer and use it in GitHub Desktop.

Select an option

Save ahbanavi/df36819131803ee2b9120fb9c15842ab to your computer and use it in GitHub Desktop.
Full voice control of an LG C5 (webOS 25) from Home Assistant + Google Assistant, without LG ThinQ — IR turn-on because Wake-on-LAN can't work, screen-off, app launching, repurposed remote keys

Full voice control of an LG C5 (webOS 25) from Home Assistant + Google Assistant — without LG ThinQ

Everything here runs over the LAN: no LG cloud, no Nabu Casa, no rooting the TV, nothing sideloaded onto it. Home Assistant talks to the set with the built-in webostv integration, and Google only ever sees HA's own fulfillment.

Written up because ThinQ was a dead end for me and most of the individual pieces below are either undocumented, wrong in the obvious place, or only findable by reading the integration source.

Tested on an OLED65C56LA, firmware 33.31.68, HA 2026.7.4.

Contents


Why not LG ThinQ

ThinQ cannot link a Middle-East-region TV to a UK Google account. The set is region-locked, the Google account is not, and the ThinQ side never completes. HA's lg_thinq config entry failed on every start with Error setting up entry LG ThinQ for lg_thinq … TimeoutError and served nothing.

Going through webostv sidesteps the region check completely — the integration speaks to the TV directly over the LAN and no LG cloud is in the path. Everything except power-on works this way; power-on cannot use the network at all (see below).

Gotcha: the TV registers twice

The set shows up as two media_player entities:

Entity Platform Use it?
media_player.lg_webos_tv_<model> Google Cast discovery No — reports off even while the TV is on
media_player.lg_webos_tv_<model>_2 webostv Yes

The _2 suffix is purely because Cast claimed the name first. Check platform in the entity registry before wiring anything to a TV entity — a power automation on the Cast entity looks plausible and never fires.

If you expose both to Google you also get the same TV twice in the Home app, so mark the Cast one expose: false explicitly.

Turning the TV on — Wake-on-LAN cannot work, so: IR

The webostv.turn_on trigger

webostv can only turn the TV off. Powered down, the set leaves the LAN and there is no socket to send anything to. HA's answer is the webostv.turn_on trigger: an automation on it is the turn-on action, and media_player.turn_on runs whatever that automation does.

Registering it has two effects, and both are required for a working Google exposure:

  • The entity stops going unavailable when off — it reports a clean off instead. This is deliberate in the integration: _set_availability(bool(self._turn_on)) (media_player.py:330), i.e. it refuses to claim "off" when it has no way to undo that.
  • supported_features gains TURN_ON (bit 128): 24381 → 24509. Computed live from the trigger registration (supported_features property, media_player.py:339). Without such an automation Google is handed a TV it can turn off and never on.

Use the webostv.turn_on trigger, not a plain state trigger. It is a PluggableAction, and the registration itself is what the entity checks. A hand-rolled automation watching the entity would fire your turn-on action but leave TURN_ON unadvertised to Google.

⚠ Wake-on-LAN cannot work if the TV is on a USB Ethernet adapter

Worth checking before you spend an evening on it, as I did. My TV's network is a TP-Link UE300 USB-to-Ethernet adapter, not the C5's own port — the MAC starts 00:e0:4c, the Realtek OUI (RTL8153). The MAC is correct and the magic packet is fine. The failure is physical:

TV state ARP for the TV's IP (checked from two different hosts)
on REACHABLE, pings, webostv connects
off nothing — no reply at all

LG cuts USB port power in standby, so the adapter dies with the TV and there is no interface left to hear the packet. No setting fixes this: Quick Start+ does not power USB, and Mobile TV On only arms LG's own NICs.

The obvious fixes both had a catch for me: the C5's built-in Ethernet is 100 Mb (4K remux streaming wants the adapter's gigabit), and Wi-Fi is 2.4 GHz-only in this region.

"Turn on via Bluetooth" is not a path either, even with the TV setting enabled. It only accepts a phone registered through the LG ThinQ app — the same proprietary handshake already blocked by the region mismatch. Nothing public reverse-engineers it; every wake project (LGTVCompanion, PyWebOSTV, LG_Buddy, the HA integration itself) does WoL or ThinQ.

If your TV is on its built-in NIC, plain Wake-on-LAN under the webostv.turn_on trigger is the easy answer and you can skip the rest of this section. Also enable Quick Start+ (Settings → General → Devices → TV Management) so the set keeps its network stack alive when off.

IR, over a Tuya blaster, fully local

Turn-on has to leave the network entirely, so: a Tuya IR blaster in the same room facing the TV, driven by tuya_local — local push, no Tuya cloud in the path.

HA's core Tuya integration cannot drive an IR blaster — it comes through as (unsupported) and yields no remote entity at all. tuya_local does. For an S11+ IR+RF Pro (product id nwpvjlq7qacvnqf3) the s11_rfir_remote config matches exactly and gives you remote.ir_blaster on DP 201 send / 202 receive.

Two tuya_local gotchas:

  • The blaster answers with no DPs at all when idle (LOCAL DPS: {"updated_at": …}), so tuya_local logs Device matches None with quality of 0% and cannot auto-select a type. The right config is still offered in the list — pick it by hand. The 0% is not a failure.
  • tuya_local discovers Tuya devices itself. A discovery flow for the blaster's IP may already be sitting at the local step waiting for a key, so a fresh "add integration" aborts with already_in_progress. Finish the pending flow instead — list flows over the websocket API with config_entries/flow/progress.

No code learning is needed. remote.send_command accepts a raw code with a b64: prefix (remote.py:_extract_codes), and tinytuya — bundled by tuya_local — encodes NEC to Tuya's pulse format:

from tinytuya.Contrib.IRRemoteControlDevice import IRRemoteControlDevice as IR
IR.pulses_to_base64(IR.nec_to_pulses(0x20DF23DC))   # LG discrete power ON
LG NEC code meaning use
20DF23DC discrete power ON the turn-on automation
20DFA35C discrete power OFF unused — webostv turns off over the socket, more reliable
20DF10EF power toggle avoid: state-dependent

Use the discrete code and you need no state condition: re-sending it to a TV that is already on is a no-op, so a redundant "turn on the TV" from Google cannot switch it off. Learned codes work too (remote.learn_command), but the physical LG remote only emits the toggle.

Needs line of sight to the TV's IR receiver.

Result, driving the exact call Google makes: media_player.turn_offoff over the socket; media_player.turn_on → IR → the TV pings after ~8 s and HA reports on after ~16 s.

tv-turn-on-ir.yaml

Screen off — a dark panel on a TV that is still on

"Turn off the TV screen" blanks the panel while webOS keeps running: audio keeps playing, the app stays in the foreground, the TV stays on the LAN, and HA still reports the media_player as on. That last part is what makes it safe to add — none of your existing to: 'off' automations see anything happen.

Plain webOS call, no payload:

Command Resulting getPowerState
com.webos.service.tvpower/power/turnOffScreen Screen Off
com.webos.service.tvpower/power/turnOnScreen Active
com.webos.service.tvpower/power/getPowerState Active / Screen Saver / Screen Off
curl -s -X POST -H "Authorization: Bearer $HA_TOKEN" -H 'Content-Type: application/json' \
  -d '{"entity_id":"media_player.lg_webos_tv_x_2","command":"com.webos.service.tvpower/power/turnOffScreen"}' \
  "$HA_URL/api/services/webostv/command?return_response"

Fronted by input_boolean.tv_screen, where ON means the panel is lit. The switch is inverted against its name on purpose: the phrase people say is "turn off the TV screen", and that has to land on turning a switch off. A switch literally named Screen Off would need the absurd "turn on screen off".

The resync poll is not optional. Any remote button press wakes the panel and webOS pushes no event for it, so without polling the switch sits at off while the picture is back — and the next "turn off the TV screen" is a silent no-op, because the switch is already off, so nothing changes state and nothing triggers. The poll only runs while the switch says off and the TV is on, so it is idle the rest of the time. It costs one re-fire with a turnOnScreen at an already-lit panel, which the TV ignores.

tv-screen.yaml

Launching apps by voice

"Turn on Jellyfin" launches the app, powering the TV on first if it is off. One input_boolean per app:

Switch Source selected on the TV
input_boolean.tv_jellyfin Jellyfin
input_boolean.tv_youtube YouTube
input_boolean.tv_spotify Spotify - Music and Podcasts
input_boolean.tv_ps5 PS5 Game Console (an HDMI input, not an app — same wiring)

Why switches and not Google's InputSelector trait, which HA already advertises with all 16 sources on it:

  • The phrasing is "switch the TV input to Jellyfin". A mouthful.
  • That input list is only correct while the TV is on. After an HA restart with a dark TV, _update_sources has nothing to enumerate, and if Google re-syncs in that window you get a TV whose only input is Live TV. (media_player.py:294 keeps the previous list in memory — but that memory is gone across a restart.)
  • TYPE_SWITCH is the category Gemini still acts on from a locked phone. See below.

Two load-bearing details in the automation:

  • The cold-start wait. select_source on a powered-off TV does nothing — webostv has no socket. So it calls media_player.turn_on (→ the IR automation), waits for state on, then sleeps 6 s more: webOS accepts the connection before its launcher is ready, and a select_source sent in that window is silently dropped.
  • input_boolean.tv_app_switching — an unexposed guard flag. Switching Jellyfin → YouTube has to clear the Jellyfin switch, and without the guard the "switch turned off" automation would read its own cleanup as "the user is done watching" and power the TV down mid-launch. Every automation that writes these switches sets the flag first.

A third automation clears all four switches when the TV goes off by any route, so Google stops showing a dark TV as running Jellyfin.

tv-apps.yaml

⚠ Gemini and the lock screen

Worth knowing if you control HA by voice from a phone rather than a speaker. Gemini gates what it will do on a locked phone by device category. It allows lights & power (lights, outlets, switches), climate (AC, thermostats, fans), window coverings and media — and action.devices.types.SCENE is not on that list. Asking it to "activate Movie Mode" while locked produced an unlock prompt every time, and no request ever reached HA.

First, the setting that has to be on regardless — it is not the classic Assistant one, Gemini has its own: Gemini app → profile picture → Settings → Gemini on lock screen → "Use Gemini without unlocking".

Then: HA derives the Google device type purely from the entity domain (const.py:142), so re-fronting something with a different domain changes its category:

HA domain Google type Gemini acts on it when locked?
scene, script, button, input_button TYPE_SCENE no
switch, input_boolean, group TYPE_SWITCH yes

Which is the real reason everything above is an input_boolean.

Repurposing the remote's Netflix / Prime / Shahid keys

The Magic Remote's Netflix / Prime Video / Disney+ / Shahid keys are hardwired to launch those app ids. You can land them somewhere useful instead:

Key Lands on How
Netflix Jellyfin select_source
Prime Video YouTube select_source
Shahid HDMI 4 webostv.commandsystem.launcher/launch com.webos.app.hdmi4

Nothing is modified on the TV. HA watches the source attribute and switches away ~1 s after the app starts — the swap happens on the splash screen, so no sign-in is ever needed on the app being hijacked.

Why from HA and not on the TV

The documented way to remap these keys (Ircama's gist) is to uninstall the app and sideload a shim that claims its app id — or, on a rooted set, magic_mapper / lginputhook. Neither fits a 2025 set:

  • Root is not available. Per faultmanager-autoroot, "for 2025 models, the only vulnerable firmware I've seen came preinstalled from the factory (webOS 10.0); any updates (webOS 10.1+) are patched." Mine is an OTA build, so patched. DejaVuln is patched on recent models too. Root would mean a firmware downgrade — the step that actually bricks TVs.
  • Dev-mode sideloading is safe but does not stick. Dev-mode sessions expire after ~50 h and take the sideloaded apps with them, so it needs a renewal cron forever. The gist's newest confirmed success is webOS 7.x / C2 (2022); nothing for webOS 25.

What makes the HA approach work

source reports the running app even when that app is filtered out of the config entry's source list. _update_sources sets _current_source and injects the app into the list on the app["id"] == tv_state.current_app_id branch, before the conf_sources filter is applied. So no config-entry change is needed to see Netflix / Prime Video / MBC Shahid.

Gotchas

  • The apps must stay installed. The trigger is the app launching. Uninstalled, the key falls back to the Content Store (com.webos.app.discovery) — the same app id for every key and for opening the store by hand, so the keys become indistinguishable. Hide the icons from the home row instead of deleting the apps.
  • The Disney+ key is unusable if Disney+ is not installed: it falls back to the web browser, and triggering on Web Browser would also fire on deliberate browser use.
  • HDMI labels drift. Port 4 reported HDMI 4, then PC once a source was attached. select_source matches on the label, so switch by app id (com.webos.app.hdmi4) instead.
  • webOS preloads Netflix at boot. Preload stays in the background and should not move source — but if a spurious app switch shows up at power-on, that is your cause.

Exposing it all to Google

Via the Google Home cloud-to-cloud integration (a Google Cloud project kept permanently in Draft — no publishing, no verification, no review), account-linked to HA over OAuth, with HA reachable on a public HTTPS hostname. No Nabu Casa.

Allowlist-style exposure, which is airtight for a reason worth knowing: GoogleConfig.should_expose() (google_assistant/http.py:170) computes is_default_exposed = domain_exposed_by_default and not auxiliary_entity, where domain_exposed_by_default is expose_by_default and <domain in exposed_domains>. With expose_by_default: false that term is always False and the function collapses to return explicit_expose — the per-entity flag. Nothing outside entity_config can ever be advertised, whatever you add to the instance later.

There is no filter: key for google_assistant, whatever else you may read. GOOGLE_ASSISTANT_SCHEMA is extra=vol.PREVENT_EXTRA and accepts exactly project_id, expose_by_default, exposed_domains, entity_config, secure_devices_pin, report_state, service_account. HA refuses to start with anything else.

Changing an exposed entity id changes the Google device id. Google drops one device and gains another, so run google_assistant.request_sync after any such rename or the old device lingers in the Home app.

Check what Google actually sees without saying a word out loud:

curl -s -X POST -H "Authorization: Bearer $HA_TOKEN" -H 'Content-Type: application/json' \
  -d '{"inputs":[{"intent":"action.devices.SYNC"}],"requestId":"chk"}' \
  "$HA_URL/api/google_assistant" | python3 -m json.tool

google_assistant.yaml

Files

File What
tv-turn-on-ir.yaml the webostv.turn_on trigger → IR power-on
tv-screen.yaml screen off/on + the resync poll
tv-apps.yaml app launching by voice
tv-hotkeys.yaml repurposed remote keys
google_assistant.yaml the entity_config block

Replace media_player.lg_webos_tv_oled65c56la_2 with your own entity id throughout, and the IR b64: blob with whatever tinytuya gives you (it is generic LG, so it may well work as-is).


One more trap, for anyone testing over the API

Automation triggers arm ~60 s after a restart, not when /api/ starts answering — they wait for homeassistant_started, and a slow integration can hold up bootstrap. A switch flipped in that window changes state and fires nothing, which looks exactly like a broken automation. Wait for Initialized trigger … in the log before concluding anything.

# The TV-related part of the google_assistant block in configuration.yaml.
# See the README in this gist.
#
# expose_by_default: false is the whole safety story -- should_expose() then collapses
# to the per-entity `expose` flag, so nothing outside entity_config can ever be
# advertised. There is NO `filter:` key; the schema is PREVENT_EXTRA and HA will refuse
# to start with one.
google_assistant:
project_id: <your-google-cloud-project-id>
# Pushes state to Google HomeGraph instead of waiting to be polled. Needs a service
# account -- _check_report_state rejects the config without one. Drop both lines if
# you do not want to set that up; Google will just poll on demand.
report_state: true
service_account: !include SERVICE_ACCOUNT.json
expose_by_default: false
exposed_domains: []
entity_config:
# The TV itself. media_player + device_class tv -> action.devices.types.TV, and
# supported_features 24509 yields OnOff, Volume, TransportControl, MediaState,
# InputSelector and Channel.
# ⚠ Turning the TV ON only works because of the tv_turn_on_ir automation: the TV
# leaves the LAN when off, so webostv has nothing to talk to, and the integration
# only advertises TURN_ON (bit 128) while a webostv.turn_on trigger is registered.
media_player.lg_webos_tv_oled65c56la_2:
name: TV
expose: true
room: Bedroom
aliases:
- Television
- LG TV
# The Google Cast discovery of the SAME TV. Never expose it: it duplicates the device
# in the Google Home app and its state is useless (off/unavailable while the TV is on).
# Redundant under expose_by_default: false, but explicit so it is not exposed by mistake.
media_player.lg_webos_tv_oled65c56la:
expose: false
# App launchers. The TV device above already carries Google's InputSelector trait with
# every source on it, but "switch the TV input to Jellyfin" is a mouthful, and that
# input list collapses to just Live TV whenever Google re-syncs while the TV is off
# (webostv can only enumerate apps over a live socket). These switches have neither
# problem, and TYPE_SWITCH is the category Gemini still acts on from a LOCKED phone.
input_boolean.tv_jellyfin:
name: Jellyfin
expose: true
room: Bedroom
aliases:
- Jellyfin on TV
- Movies
input_boolean.tv_youtube:
name: YouTube
expose: true
room: Bedroom
aliases:
- YouTube on TV
input_boolean.tv_spotify:
name: Spotify
expose: true
room: Bedroom
aliases:
- Spotify on TV
input_boolean.tv_ps5:
name: PS5
expose: true
room: Bedroom
aliases:
- PlayStation
- Console
# LG "screen off": the panel goes dark while webOS keeps running, audio keeps playing
# and the TV stays on the LAN -- a different thing from turning the TV off, so it is
# its own device rather than a trait on the TV above. ON = panel lit, which is what
# makes "turn off the TV screen" the natural phrasing.
input_boolean.tv_screen:
name: TV Screen
expose: true
room: Bedroom
aliases:
- Screen
- TV Display
- TV Picture
# Internal guard flag for the app automations -- never exposed. It marks "an app switch
# is being changed by an automation", which is what stops a switch-to-another-app from
# being read as "turn the TV off".
input_boolean.tv_app_switching:
expose: false
# The helpers behind those switches. Create them in the UI (Settings > Devices &
# Services > Helpers > Toggle) or declare them here -- either works.
input_boolean:
tv_jellyfin:
name: TV Jellyfin
icon: mdi:play-network
tv_youtube:
name: TV YouTube
icon: mdi:youtube
tv_spotify:
name: TV Spotify
icon: mdi:spotify
tv_ps5:
name: TV PS5
icon: mdi:sony-playstation
tv_screen:
name: TV Screen
icon: mdi:television-shimmer
tv_app_switching:
name: TV App Switching
icon: mdi:sync
# Goes in automations.yaml. See the README in this gist.
- id: tv_app_launch
alias: TV app switch → launch on TV
description: >-
One switch per app, because that is the only category Gemini acts on from a LOCKED
phone (same reason the light modes are input_booleans -- see docs). Google's own
InputSelector trait already carries every source, but "switch the TV input to
Jellyfin" is a mouthful and its input list collapses to Live TV if Google re-syncs
while the TV is off; a switch has neither problem.
Cold start is the whole point of the wait: selecting a source on a TV that is off
does nothing, because webostv has no socket to talk to. So power on over IR first
(media_player.turn_on -> the tv_turn_on_ir automation), wait for the TV to report
`on`, then give webOS a few more seconds before the app launch will stick.
input_boolean.tv_app_switching guards the OFF automation: clearing the other three
switches here would otherwise read as "user turned an app off" and power the TV down
mid-launch.
triggers:
- trigger: state
entity_id:
- input_boolean.tv_jellyfin
- input_boolean.tv_youtube
- input_boolean.tv_spotify
- input_boolean.tv_ps5
to: 'on'
variables:
sources:
input_boolean.tv_jellyfin: Jellyfin
input_boolean.tv_youtube: YouTube
input_boolean.tv_spotify: Spotify - Music and Podcasts
input_boolean.tv_ps5: PS5 Game Console
source: '{{ sources[trigger.entity_id] }}'
others: '{{ sources.keys() | reject("eq", trigger.entity_id) | list }}'
actions:
- action: input_boolean.turn_on
target:
entity_id: input_boolean.tv_app_switching
- action: input_boolean.turn_off
target:
entity_id: '{{ others }}'
- if:
- condition: not
conditions:
- condition: state
entity_id: media_player.lg_webos_tv_oled65c56la_2
state: 'on'
then:
- action: media_player.turn_on
target:
entity_id: media_player.lg_webos_tv_oled65c56la_2
- wait_template: "{{ is_state('media_player.lg_webos_tv_oled65c56la_2', 'on') }}"
timeout: '00:01:30'
continue_on_timeout: false
# webOS accepts the connection before the launcher is ready; without this the
# select_source lands on a TV that is on but still booting, and is dropped.
- delay:
seconds: 6
# Launching an app onto a blanked panel would leave it dark; this re-lights it
# through tv_screen_control. A no-op when the screen is already on.
- action: input_boolean.turn_on
target:
entity_id: input_boolean.tv_screen
- action: media_player.select_source
target:
entity_id: media_player.lg_webos_tv_oled65c56la_2
data:
source: '{{ source }}'
- delay:
seconds: 3
- action: input_boolean.turn_off
target:
entity_id: input_boolean.tv_app_switching
mode: restart
- id: tv_app_switch_off
alias: TV app switch off → turn the TV off
description: >-
Turning an app switch off means "I am done watching" -- power the TV down over the
webostv socket (more reliable than IR, and the socket is live by definition while
the TV is on). Skipped while tv_app_switching is set, which is how switching between
two apps avoids killing the TV. The `on` condition also makes this a no-op when the
switches are being cleared because the TV already went off.
triggers:
- trigger: state
entity_id:
- input_boolean.tv_jellyfin
- input_boolean.tv_youtube
- input_boolean.tv_spotify
- input_boolean.tv_ps5
to: 'off'
conditions:
- condition: state
entity_id: input_boolean.tv_app_switching
state: 'off'
- condition: state
entity_id: media_player.lg_webos_tv_oled65c56la_2
state: 'on'
actions:
- action: input_boolean.turn_on
target:
entity_id: input_boolean.tv_app_switching
- action: media_player.turn_off
target:
entity_id: media_player.lg_webos_tv_oled65c56la_2
- action: input_boolean.turn_off
target:
entity_id:
- input_boolean.tv_jellyfin
- input_boolean.tv_youtube
- input_boolean.tv_spotify
- input_boolean.tv_ps5
- delay:
seconds: 2
- action: input_boolean.turn_off
target:
entity_id: input_boolean.tv_app_switching
mode: single
- id: tv_off_clear_app_switches
alias: TV off → clear the app switches
description: >-
Keeps the switches honest when the TV is powered off by the remote, by voice, or by
anything else. Without it Google would keep showing "Jellyfin" as on for a dark TV.
Runs with the tv_app_switching guard set so the off automation above does not read
its own cleanup as a user request.
triggers:
- trigger: state
entity_id: media_player.lg_webos_tv_oled65c56la_2
to: 'off'
conditions:
- condition: or
conditions:
- condition: state
entity_id: input_boolean.tv_jellyfin
state: 'on'
- condition: state
entity_id: input_boolean.tv_youtube
state: 'on'
- condition: state
entity_id: input_boolean.tv_spotify
state: 'on'
- condition: state
entity_id: input_boolean.tv_ps5
state: 'on'
actions:
- action: input_boolean.turn_on
target:
entity_id: input_boolean.tv_app_switching
- action: input_boolean.turn_off
target:
entity_id:
- input_boolean.tv_jellyfin
- input_boolean.tv_youtube
- input_boolean.tv_spotify
- input_boolean.tv_ps5
- delay:
seconds: 2
- action: input_boolean.turn_off
target:
entity_id: input_boolean.tv_app_switching
mode: single
# Goes in automations.yaml. See the README in this gist.
- id: tv_hotkey_netflix_jellyfin
alias: TV Netflix key → Jellyfin
description: >-
Repurposes the hardwired Netflix key on the Magic Remote. The key is burned into the
remote and cannot be remapped on the TV without rooting it -- and this C5 cannot be
rooted (2025 models are only vulnerable on factory webOS 10.0; anything OTA-updated,
including this one, is patched). So instead of intercepting the key, HA watches the
foreground app and yanks the TV somewhere else ~1s after Netflix starts.
Netflix must stay INSTALLED for this to work -- the trigger is the app launching.
Uninstalled, the key opens the Content Store, which is the same app id as the Prime
key and as opening the store by hand, so it cannot be told apart. Sign-in is never
needed; the swap happens on the splash screen. Hide the icon from the home row if it
is in the way.
`source` carries the foreground app even when it is filtered out of the configured
source list -- webostv always writes the running app into _current_source and injects
it into the source list. So Netflix does not need to be an enabled source.
triggers:
- trigger: state
entity_id: media_player.lg_webos_tv_oled65c56la_2
attribute: source
to: Netflix
actions:
- action: media_player.select_source
target:
entity_id: media_player.lg_webos_tv_oled65c56la_2
data:
source: Jellyfin
mode: single
- id: tv_hotkey_prime_youtube
alias: TV Prime Video key → YouTube
description: >-
Same trick as the Netflix key, for the Prime Video key. See tv_hotkey_netflix_jellyfin
for why this is done from HA and not on the TV. "Prime Video" is the exact app title
this TV reports; the app must stay installed.
triggers:
- trigger: state
entity_id: media_player.lg_webos_tv_oled65c56la_2
attribute: source
to: Prime Video
actions:
- action: media_player.select_source
target:
entity_id: media_player.lg_webos_tv_oled65c56la_2
data:
source: YouTube
mode: single
- id: tv_hotkey_shahid_hdmi4
alias: TV Shahid key → HDMI 4
description: >-
Third repurposed hotkey, see tv_hotkey_netflix_jellyfin for the mechanism. The Disney+
key is NOT used: with Disney+ uninstalled it falls back to the web browser, and
triggering on "Web Browser" would also fire whenever the browser is opened on purpose.
The Shahid key launches its own app, so it is unambiguous -- MBC Shahid must stay
installed.
Switches by app id, not by `media_player.select_source`: this port's label is not
stable (it reported "HDMI 4", then "PC" once a source was attached), and select_source
matches on the label. `com.webos.app.hdmi4` always means the physical port.
triggers:
- trigger: state
entity_id: media_player.lg_webos_tv_oled65c56la_2
attribute: source
to: MBC Shahid
actions:
- action: webostv.command
target:
entity_id: media_player.lg_webos_tv_oled65c56la_2
data:
command: system.launcher/launch
payload:
id: com.webos.app.hdmi4
mode: single
# Goes in automations.yaml. See the README in this gist.
- id: tv_screen_control
alias: TV screen switch → panel off/on over webostv
description: >-
LG's "screen off" is not a power-off: the panel goes dark while webOS keeps running,
audio keeps playing and the TV stays on the LAN. That is a different thing from
media_player.turn_off, so it gets its own switch instead of riding on the TV device.
input_boolean.tv_screen ON = the panel is lit, so the natural phrasing "turn off the
TV screen" maps straight onto turning the switch off. TYPE_SWITCH is also the only
category Gemini acts on from a LOCKED phone -- the same reason the light modes and
the app launchers are input_booleans (see docs).
Conditioned on the TV being on: with the TV off webostv has no socket and the command
would only raise.
triggers:
- trigger: state
entity_id: input_boolean.tv_screen
to: 'off'
id: screen_off
- trigger: state
entity_id: input_boolean.tv_screen
to: 'on'
id: screen_on
conditions:
- condition: state
entity_id: media_player.lg_webos_tv_oled65c56la_2
state: 'on'
actions:
- choose:
- conditions:
- condition: trigger
id: screen_off
sequence:
- action: webostv.command
target:
entity_id: media_player.lg_webos_tv_oled65c56la_2
data:
command: com.webos.service.tvpower/power/turnOffScreen
- conditions:
- condition: trigger
id: screen_on
sequence:
- action: webostv.command
target:
entity_id: media_player.lg_webos_tv_oled65c56la_2
data:
command: com.webos.service.tvpower/power/turnOnScreen
mode: restart
- id: tv_screen_resync
alias: TV screen → resync the switch with the panel
description: >-
Any remote button press wakes the panel and webOS pushes no event for it, so the
switch would go stale -- "off" while the picture is back -- and the next "turn off the
TV screen" would be a silent no-op: the switch is already off, so no state change, so
no trigger. Polling getPowerState closes that hole. It only runs while the switch says
off and the TV is on, so it is idle in the normal case.
Setting the switch back on re-fires tv_screen_control with a turnOnScreen at an
already-lit panel. That is a no-op on the TV and is cheaper than a guard flag.
triggers:
- trigger: time_pattern
minutes: /1
conditions:
- condition: state
entity_id: input_boolean.tv_screen
state: 'off'
- condition: state
entity_id: media_player.lg_webos_tv_oled65c56la_2
state: 'on'
actions:
- action: webostv.command
target:
entity_id: media_player.lg_webos_tv_oled65c56la_2
data:
command: com.webos.service.tvpower/power/getPowerState
response_variable: power
continue_on_error: true
- condition: template
value_template: >-
{{ power is defined and
power.get('media_player.lg_webos_tv_oled65c56la_2', {}).get('state') != 'Screen Off' }}
- action: input_boolean.turn_on
target:
entity_id: input_boolean.tv_screen
mode: single
max_exceeded: silent
- id: tv_screen_reset_on_power_off
alias: TV off → reset the screen switch
description: >-
The panel always comes back lit on the next power-on, so a switch left off would make
Google report a dark screen on a TV that is showing a picture. Reset it while the TV
is off. tv_screen_control is conditioned on the TV being on, so this cannot fire a
turnOnScreen at a set that has just left the LAN.
triggers:
- trigger: state
entity_id: media_player.lg_webos_tv_oled65c56la_2
to: 'off'
conditions:
- condition: state
entity_id: input_boolean.tv_screen
state: 'off'
actions:
- action: input_boolean.turn_on
target:
entity_id: input_boolean.tv_screen
mode: single
# Goes in automations.yaml. See the README in this gist.
- id: tv_turn_on_ir
alias: TV requested on → IR power-on
description: >-
webostv cannot power the TV on by itself: the C5 drops off the LAN when off, so
media_player.turn_on has nothing to talk to. This automation IS the turn-on path,
and its existence is also what makes the entity advertise TURN_ON at all --
supported_features gains bit 128 only when a webostv.turn_on trigger is registered.
Without it Google gets a TV it can turn off but never on.
Wake-on-LAN is NOT usable here: the TV's network is a TP-Link UE300 USB adapter,
and LG cuts USB power in standby, so nothing is on the wire to hear a magic packet
(a powered-off TV answers no ARP, from the host or the router). So turn-on goes
over IR through the Tuya blaster instead, via tuya_local -- fully local, no cloud.
The code is LG's DISCRETE power-on (NEC 20DF23DC), not the remote's toggle key,
so re-sending it while the TV is already on is a no-op and needs no state guard.
Generated with tinytuya: IR.pulses_to_base64(IR.nec_to_pulses(0x20DF23DC)).
Discrete off is 20DFA35C and toggle is 20DF10EF, but turning off goes over the
webostv socket, which is more reliable than IR.
triggers:
- trigger: webostv.turn_on
entity_id: media_player.lg_webos_tv_oled65c56la_2
actions:
- action: remote.send_command
target:
entity_id: remote.ir_blaster
data:
command:
- b64:KCOUETMCMwIzAjMCMwKYBjMCMwIzAjMCMwIzAjMCMwIzAjMCMwKYBjMCmAYzAjMCMwKYBjMCmAYzApgGMwKYBjMCmAYzAjMCMwIzAjMCmAYzAjMCMwIzAjMCMwIzApgGMwKYBjMCmAYzApgGMwIzAjMCmAYzApgGMwKYBjMCMwIzAjMCMwIwdQ==
mode: single
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment