[ Voice-Activated Dog TV in Home Assistant ]

[ Overview ]

Voice command that turns on a TV and plays a random YouTube Dog TV
video automatically. End-to-end stack: Home Assistant Voice or an
Atom Echo satellite captures the phrase, a custom sentence routes it
to an intent, the intent fires a script asynchronously, the script
powers on the TV via the Android TV Remote integration, waits for
the device to fully boot, then deep-links into the YouTube app via
an ADB shell command.

End result: say "dog tv bedroom" and the TV
turns on and starts playing one of a curated list of long-form
Dog TV videos — different one each time.

The pattern generalises: the same script / sentence / intent triple
works for any voice-command-driven "open a deep link on a specific
TV" automation. ADB is doing the heavy lifting on the TV side —
the rest is HA glue. Swap the URLs and the trigger phrase and you've
got cooking shows, ambient music videos, sunset clips for plants,
whatever.

[ Requirements ]

  — Home Assistant with Home Assistant Voice or an
    Atom Echo satellite (or any Assist pipeline you
    can speak into — the phone companion app's Assist works
    fine for testing)
  — An Android TV / Google TV device with YouTube
    installed (Hisense, Nvidia Shield, Chromecast with Google TV,
    Onn 4K, etc.)
  — Android Debug Bridge (ADB) integration
    installed in HA — sends shell commands to the TV
  — Android TV Remote integration installed in
    HA — reliable power-on, plus the standard transport controls
  — ADB / Network Debugging enabled on the TV

Both integrations are needed: Android TV Remote owns the power state
(it can wake the TV from standby reliably), ADB owns the deep-link
firing. Trying to do power-on through ADB alone is hit-or-miss because
the ADB daemon often isn't reachable while the TV is fully asleep.

[ Step 1 — Enable ADB on Your TV ]

On the Android / Google TV:

  1. Settings → Device Preferences → About
  2. Click Build seven times rapidly to enable
     Developer Options
  3. Back out to Device Preferences → Developer Options
  4. Enable Network Debugging (or USB Debugging if
     you're tethering)

After this the TV listens on TCP port 5555 for ADB
connections. Some TVs also expose this on a custom port; check the
Developer Options screen.

Security note. Network ADB is unauthenticated until
the first connection prompt is accepted, and even after that the
shell is full-privilege. Keep the TV on a trusted VLAN. Don't open
port 5555 to the internet for any reason.

[ Step 2 — Add the ADB Integration in HA ]

  1. Settings → Devices & Services → Add Integration
  2. Search for Android Debug Bridge
  3. Enter the TV's IP address and port 5555
  4. Accept the ADB authorization prompt that appears on the TV screen
     — tick "always allow from this computer" so it sticks
     across reboots

This creates a media_player entity along the lines of
media_player.bedroom_tv_adb. That entity is what we send
the androidtv.adb_command service call to in step 4.

Keep the Android TV Remote integration set up too —
it handles power-on and transport controls more reliably than ADB
(particularly waking the TV from cold standby). The two integrations
coexist: ATR for power, ADB for the deep link.

[ Step 3 — Find Working YouTube Video URLs ]

Curate a short list of long-form, publicly-available Dog TV videos.
Good candidates for the bedroom:

  — 10 Hours No Loopshttps://www.youtube.com/watch?v=ggkYESx1FpcBabbling Brook with Birds (8h)https://www.youtube.com/watch?v=nYcHi9EgUHsRelax My Dog TV (10h)https://www.youtube.com/watch?v=ru1z02CXbaADOGTV Outdoor Adventure (1h)https://www.youtube.com/watch?v=QVfiP1x-vngDog Relax & Stress Relief (1h)https://www.youtube.com/watch?v=j_-azXHHGVc

Verify each URL is public before adding it. YouTube videos go
private without warning, age-restricted ones won't open from a
deep link without a logged-in account, and copyright takedowns
silently break the script. A monthly "are these still alive?"
audit isn't a bad idea.

Tip: Prefer videos that are explicitly published as
ambient / loop content. Anything with mid-roll ads, intros, or
narration defeats the purpose — you want continuous low-stim
visuals.

[ Step 4 — Create the Script in scripts.yaml ]

Open File Editor and edit /config/scripts.yaml:
dog_tv_bedroom:
  alias: Dog TV Bedroom
  sequence:
    - variables:
        dog_tv_url: >
          {% set videos = [
            'https://www.youtube.com/watch?v=ggkYESx1Fpc',
            'https://www.youtube.com/watch?v=nYcHi9EgUHs',
            'https://www.youtube.com/watch?v=ru1z02CXbaA',
            'https://www.youtube.com/watch?v=QVfiP1x-vng',
            'https://www.youtube.com/watch?v=j_-azXHHGVc'
          ] %}
          {{ videos | random }}
    - action: media_player.turn_on
      target:
        entity_id: media_player.bedroom_tv         # Android TV Remote entity
    - delay:
        seconds: 15                                # wait for TV to fully boot
    - action: androidtv.adb_command
      target:
        entity_id: media_player.bedroom_tv_adb    # ADB entity
      data:
        command: "am start -a android.intent.action.VIEW -d '{{ dog_tv_url }}'"
  mode: single
Why two entity IDs?media_player.bedroom_tv — Android TV
    Remote entity. Owns power on/off.
  — media_player.bedroom_tv_adb — ADB entity.
    Receives the YouTube deep-link command.

Two integrations, two entities, one TV. The script touches both in
sequence.

Why a 15-second delay? The TV needs time to fully
boot before its ADB daemon can accept the deep-link intent. Too
short and YouTube opens to its home screen but the video never
loads (or worse, the am start fails silently). On
faster TVs you can drop this to 8–10; on a sluggish older
Android TV stick, 20 isn't unreasonable. Tune to your hardware.

Why random? The {{ videos | random }}
Jinja filter picks a different URL on each invocation so your dog
isn't watching the same 10-hour brook every day. Same pattern
applies to any "pick one from a curated set" need.

mode: single. If the script is already running
(e.g. dog walked past the satellite again mid-15-second-delay) new
invocations are dropped. Switch to restart if you
want the new call to interrupt and restart the boot+launch cycle
instead. See the
four-modes section
of the HA scripting quick-start.

[ Step 5 — Create the Custom Sentence ]

Create a new file at /config/custom_sentences/en/dog_tv.yaml
(make the custom_sentences/en/ directory if it doesn't
exist):
language: en
intents:
  PlayDogTVBedroom:
    data:
      - sentences:
          - "dog tv bedroom"
          - "dog tv for (name1|name2) bedroom"
          - "start dog tv bedroom"
  PlayDogTVLivingRoom:
    data:
      - sentences:
          - "dog tv living room"
          - "dog tv for (name1|name2) living room"
          - "start dog tv living room"
Replace name1 and name2 with your dogs'
names (or whatever per-dog naming makes sense). The
(a|b) syntax is HA's sentence alternation operator —
any one of the listed alternatives matches.

Important: keep the trigger phrase simple and unique.
Avoid starting with the word "play" — HA's built-in
HassMediaSearchAndPlay intent will intercept it before
the custom sentence ever gets a chance to match, and you'll see
"no media player exposed" errors that look nothing like a custom
sentence problem. "dog tv bedroom" is plenty distinct.

Several phrasing variants per intent improves voice-recognition
robustness without adding ambiguity — STT (speech-to-text)
sometimes hears "start dog tv bedroom" when you said
"dog tv bedroom", and either way the intent fires.

[ Step 6 — Add the Intent Script ]

Open /config/configuration.yaml and add under
intent_script::
intent_script:
  PlayDogTVBedroom:
    speech:
      text: "Putting on Dog TV in the bedroom."
    action:
      service: script.turn_on
      target:
        entity_id: script.dog_tv_bedroom
  PlayDogTVLivingRoom:
    speech:
      text: "Putting on Dog TV in the living room."
    action:
      service: script.turn_on
      target:
        entity_id: script.dog_tv_living_room
Three things that look like minor stylistic choices but
are actually load-bearing:

  1. Async-dispatch the script with script.turn_on
     + target:.  This is the single most
     important detail on the page.

     The script's 15-second TV-boot delay is longer than the Voice
     satellite's intent-response timeout. If the intent calls the
     script directly (action: { service:
     script.dog_tv_bedroom }), the intent BLOCKS for the
     full 15 seconds before the TTS response can play — the
     satellite shows a red error ring, the phone Assist UI looks
     broken, and the next utterance can't fire until the previous
     one's script returns.

     script.turn_on with a target fires the script
     asynchronously: the intent returns immediately, TTS plays,
     the satellite goes back to listening, and the 15-second boot
     wait runs in the background where nobody's waiting on it.
     The same pattern applies to any intent that fires a multi-
     second script (fill-a-bag, multi-step macros, anything with
     a meaningful delay: or wait_template:).

  2. Use the single-mapping form for the action
     block, not a list.  The single-mapping form
     (action: { service: …, target: … }
     in flow style, or two indented keys in block style) is what
     intent_script expects.  The list-of-actions form
     (action: on its own line, hyphens below) used to
     fail silently on older HA versions; current HA generally
     accepts both, but the single mapping is the safer default and
     plays nicer with older docs / older installs you might
     migrate from.

  3. service: vs action: as the
     key name — either works on current HA.  HA
     2024.8+ aliased action: as a synonym for
     service: across most config blocks, including
     intent_script.  Pre-2024.8 installs only accept
     service:.  If you're on a current install the
     two are interchangeable; if you maintain configs that need to
     work across versions, service: is the lowest-
     common-denominator choice.

The TTS speech.text field is what the satellite says
back to confirm the command landed. Keep it short and unambiguous
so you can tell at the speaker whether the command was matched
even before walking into the room to check the TV.

[ Step 7 — Reload and Test ]

  1. Developer Tools → YAML → All YAML → Reload
     (or restart Home Assistant outright if you'd rather)
  2. Open Developer Tools → Assist (the chat
     bubble icon in the sidebar)
  3. Type the trigger phrase:

         dog tv bedroom

  4. Confirm the TV turns on and a video plays within ~15–20
     seconds
  5. Now test by voice from your Voice satellite

Use the Assist chat for testing, not the Sentence Parser.
The Sentence Parser in Developer Tools only validates that the intent
matches — it does NOT execute the action. Always test
end-to-end via the Assist chat bubble (or by speaking to the
satellite); the parser will lie to you about whether anything
actually runs.

For voice-vs-typed mismatches (works in chat, doesn't work by
voice), pull up the Voice Assistant debug log
under Settings → Voice Assistants → (your pipeline)
→ Debug. The log shows the raw STT transcription, so you
can see exactly what the STT engine thought you said — nine
times out of ten the fix is adding that transcription as another
sentence alternate in dog_tv.yaml.

[ Troubleshooting ]

  "No media player exposed" error
    Built-in HassMediaSearchAndPlay intent is
    intercepting. Don't start the trigger phrase with "play".

  "Unknown intent" error
    Custom sentences not loaded. Verify the file path
    (/config/custom_sentences/en/dog_tv.yaml), reload
    Conversation under Developer Tools → YAML, or restart HA
    once after creating the directory structure for the first time.

  TV turns on but no video plays
    15-second delay was too short. Watch the TV boot once with a
    stopwatch and pad another 3–5s. Alternatively, switch
    from delay-based waiting to a wait_template
    polling for media_player.bedroom_tv state to
    leave standby.

  Red ring on the Voice satellite mid-command
    Script is running synchronously and blocking the intent
    response past the satellite's timeout. Use
    script.turn_on with target: for
    async firing (see step 6).

  "Unexpected error occurred"
    Syntax mismatch inside intent_script. Most common
    on older HA installs: the list-of-actions form (hyphenated
    actions under action:) silently failing. Switch
    to the single-mapping form — action: with
    two indented keys (service: +
    target:), no leading -. Pre-2024.8
    installs also require service: as the key name
    (not the newer action: alias).

  Video URL no longer plays
    YouTube went private / age-restricted / region-locked / DMCA'd.
    Open the URL in a browser to verify, then swap it in
    scripts.yaml. The remaining URLs in the list
    continue to work in the meantime.

  Works in Assist chat but not by voice
    STT is transcribing differently than you're typing. Check the
    Voice Assistant debug log (Settings → Voice Assistants
    → Debug) for the actual transcription, then add that
    exact phrasing as another sentence alternate.

  ADB connection drops after TV power-cycle
    Some TVs revoke the "always allow from this computer" ADB
    grant on factory-reset-equivalent reboots. Re-accept the
    prompt on the TV screen, or set a static IP on the TV so the
    fingerprint stays bound to the same address.

[ Adding More TVs / More Videos ]

Adding another TV.

Repeat steps 4–6 for each TV, creating a new script and
intent pair per location. The intent name, the script name, and
the trigger sentence should all carry the room label.

For non-Android TVs (LG webOS, Samsung Tizen, Roku) you can't use
the ADB deep-link path — switch the action to
media_player.select_source targeting the YouTube app
already installed on that TV:
dog_tv_living_room:
  alias: Dog TV Living Room
  sequence:
    - action: media_player.turn_on
      target:
        entity_id: media_player.living_room_tv
    - delay:
        seconds: 5
    - action: media_player.select_source
      target:
        entity_id: media_player.living_room_tv
      data:
        source: YouTube
  mode: single
You lose per-URL control on the webOS path — it just opens
the YouTube app and resumes whatever was last playing. For Roku,
the same shape works with source: "YouTube" via the
Roku integration's select_source.

Adding more videos.

Just append URLs to the videos list inside the
variables block:
- variables:
    dog_tv_url: >
      {% set videos = [
        'https://www.youtube.com/watch?v=VIDEO_ID_1',
        'https://www.youtube.com/watch?v=VIDEO_ID_2',
        'https://www.youtube.com/watch?v=VIDEO_ID_3'
      ] %}
      {{ videos | random }}
The random filter weights each entry equally. If you
want certain videos to play more often (e.g. the 10-hour ones
weighted heavier than the 1-hour ones because they last the whole
nap), build a weighted list manually — each URL repeated N
times for an N-weight — or replace the Jinja with a more
elaborate template using
{{ range(0, videos|length) | random }} and an
external weights array.

Adding non-Dog-TV variants.

The whole pattern is just "voice command → turn on device
→ deep-link a URL." Swap the video list for kids' lullabies,
ambient cooking shows, plant-care livestreams, anything with a
deep-linkable YouTube URL. Each variant gets its own script,
sentence, and intent — or share one script with a
field parameter to pick the list at call time.

[ See Also ]