Vizio TV Integration

@kbrown01 Thank you so much for everything you have shared here. We frequently have our remotes stolen and hidden away by our Toddler so I was looking for something that we can use when we can’t find a remote. Our two Android TVs were super easy, I just copied the example text from Home Assistant, and it worked perfectly but getting my Mother-in-law’s Vizio to work was more difficult. Your contributions here got me started and lead me to the path on what I needed to do.

Since my setup is much simpler and I think most people are coming to this post for questions about their Vizio Setup, here is what I did.

what I added to the configurations.yaml file:

vizio:
  - host: 'xxx.xxx.xxx.xxx:7345'
    access_token: **********

rest_command:
  vizio_key:
    url: 'https://xxx.xxx.xxx.xxx:7345/key_command/'
    method: PUT
    content_type: "application/json"
    headers:
      AUTH: **********
    payload: '{"KEYLIST": [{"CODESET": {{ codeset | int }},"CODE": {{ code | int }},"ACTION":"KEYPRESS"}]}'
    verify_ssl: false

The code for the card. I started with copying the sample code for the Android TVs and edited it to use the API calls instead:

type: vertical-stack
cards:
  - type: entities
    entities:
      - entity: media_player.VizioTV
    title: Mamita's TV Control
  - square: true
    columns: 3
    type: grid
    cards:
      - type: button
        show_icon: false
        tap_action:
          action: none
        hold_action:
          action: none
      - show_name: true
        show_icon: true
        type: button
        icon: mdi:arrow-up-bold
        tap_action:
          action: call-service
          service: rest_command.vizio_key
          data:
            codeset: 3
            code: 8
          target:
            entity_id: media_player.guest_room_tv
        hold_action:
          action: none
      - type: button
        show_icon: false
        tap_action:
          action: none
        hold_action:
          action: none
      - type: button
        icon: mdi:arrow-left-bold
        tap_action:
          action: call-service
          service: rest_command.vizio_key
          data:
            codeset: 3
            code: 1
          target:
            entity_id: media_player.guest_room_tv
        hold_action:
          action: none
      - type: button
        icon: mdi:circle
        tap_action:
          action: call-service
          service: rest_command.vizio_key
          data:
            codeset: 3
            code: 2
          target:
            entity_id: media_player.guest_room_tv
        hold_action:
          action: call-service
          service: remote.send_command
          data:
            command: DPAD_CENTER
            hold_secs: 0.5
          target:
            entity_id: media_player.guest_room_tv
      - type: button
        icon: mdi:arrow-right-bold
        tap_action:
          action: call-service
          service: rest_command.vizio_key
          data:
            codeset: 3
            code: 7
          target:
            entity_id: media_player.guest_room_tv
        hold_action:
          action: none
      - type: button
        icon: mdi:arrow-left
        tap_action:
          action: call-service
          service: rest_command.vizio_key
          data:
            codeset: 4
            code: 0
          target:
            entity_id: media_player.guest_room_tv
        hold_action:
          action: none
      - type: button
        icon: mdi:arrow-down-bold
        tap_action:
          action: call-service
          service: rest_command.vizio_key
          data:
            codeset: 3
            code: 0
          target:
            entity_id: media_player.guest_room_tv
        hold_action:
          action: none
      - type: button
        icon: mdi:home-outline
        tap_action:
          action: call-service
          service: rest_command.vizio_key
          data:
            codeset: 4
            code: 3
          target:
            entity_id: media_player.guest_room_tv
        hold_action:
          action: none
  - square: false
    columns: 3
    type: grid
    cards:
      - show_name: true
        show_icon: true
        type: button
        icon: mdi:skip-previous
        tap_action:
          action: call-service
          service: rest_command.vizio_key
          data:
            codeset: 2
            code: 11
          target:
            entity_id: media_player.guest_room_tv
        hold_action:
          action: none
      - show_name: true
        show_icon: true
        type: button
        icon: mdi:play
        tap_action:
          action: call-service
          service: rest_command.vizio_key
          data:
            codeset: 2
            code: 3
          target:
            entity_id: media_player.guest_room_tv
        hold_action:
          action: none
      - show_name: true
        show_icon: true
        type: button
        icon: mdi:skip-next
        tap_action:
          action: call-service
          service: rest_command.vizio_key
          data:
            codeset: 2
            code: 10
          target:
            entity_id: media_player.guest_room_tv
        hold_action:
          action: call-service
          service: remote.send_command
          data:
            command: MEDIA_FAST_FORWARD
          target:
            entity_id: media_player.guest_room_tv
      - type: button
        icon: mdi:volume-off
        tap_action:
          action: call-service
          service: rest_command.vizio_key
          data:
            codeset: 5
            code: 4
          target:
            entity_id: media_player.guest_room_tv
        hold_action:
          action: none
      - type: button
        icon: mdi:volume-medium
        tap_action:
          action: call-service
          service: rest_command.vizio_key
          data:
            codeset: 5
            code: 0
          target:
            entity_id: media_player.guest_room_tv
        hold_action:
          action: none
      - type: button
        icon: mdi:volume-high
        tap_action:
          action: call-service
          service: rest_command.vizio_key
          data:
            codeset: 5
            code: 1
          target:
            entity_id: media_player.guest_room_tv
        hold_action:
          action: none
  - square: false
    columns: 4
    type: grid
    cards:
      - type: button
        icon: mdi:youtube
        tap_action:
          action: call-service
          service: remote.turn_on
          data:
            activity: https://www.youtube.com
          target:
            entity_id: media_player.guest_room_tv
        hold_action:
          action: none
      - type: button
        icon: mdi:netflix
        tap_action:
          action: call-service
          service: remote.turn_on
          data:
            activity: https://www.netflix.com/title
          target:
            entity_id: media_player.guest_room_tv
        hold_action:
          action: none
      - type: picture
        image: >-
          https://upload.wikimedia.org/wikipedia/commons/thumb/1/11/Amazon_Prime_Video_logo.svg/450px-Amazon_Prime_Video_logo.svg.png
        tap_action:
          action: call-service
          service: remote.turn_on
          data:
            activity: https://app.primevideo.com
          target:
            entity_id: media_player.guest_room_tv
        hold_action:
          action: none
      - type: picture
        image: >-
          https://upload.wikimedia.org/wikipedia/commons/thumb/3/3e/Disney%2B_logo.svg/440px-Disney%2B_logo.svg.png
        tap_action:
          action: call-service
          service: remote.turn_on
          data:
            activity: https://www.disneyplus.com
          target:
            entity_id: media_player.guest_room_tv
        hold_action:
          action: none
  - type: entity
    entity: media_player.VizioTV
  - type: media-control
    entity: media_player.VizioTV

There are some inconsisties with entity names I don’t fully understand, but it works.

I also noticed the API Documentation Listed codeset 2 as Transport, which I figured must be for navigation within a video (I worked for a TV service for a couple years and transport sounded like a term they would use). Here is what I observed experimenting with codeset 2:
Codeset: Code: Action:

2 0 Seek Forward
2 1 Seek Backward
2 2 Pause
2 3 Play
2 4 Unknown
2 5 Skip Forward
2 6 Skip Backward
2 7 Skip Backward
2 8 Skip Forward
2 9 Pause
2 10 Next
2 11 Previous

I’m not 100% on what each of these actions are. I just recorded what I observed in Youtube playing a mix of music videos. I was most interested in the skip to next/previous item actions so I stopped testing after that.

I haven’t gotten the links to directly open apps to work yet. I tried adding a second rest_command into the configruations.yaml, but can’t get it to work correctly. I keep getting errors I don’t understand even though I think it should be possible. Ultimately I want something that looks kind of like this:

rest_command:
  vizio_key:
    url: 'https://xxx.xxx.xxx.xxx:7345/key_command/'
    method: PUT
    content_type: "application/json"
    headers:
      AUTH: **********
    payload: '{"KEYLIST": [{"CODESET": {{ codeset | int }},"CODE": {{ code | int }},"ACTION":"KEYPRESS"}]}'
    verify_ssl: false
  vizio_launchApp:
    url: 'https://192.168.0.216:735/app/launch/'
    method: PUT
    content_type: "application/json"
    headers:
      AUTH: 'Zwytkualvu'
    payload: '{"VALUE": [{"MESSAGE": {{ message | string }},"NAME_SPACE":{{ name_space | int }},"APP_ID":{{ app_id | string }}}]}'
    verify_ssl: false

I just can’t figure what’s wrong with my formatting.

Again, thank you so much! I wouldn’t be anywhere near where I am now without your guidance here.

Maybe this helps. I do not use app/launch, I use media_player.select_source.
I wrote Vizio and they published a URL that contains all of the “app names” when launching an app (like Netflix, etc.).

So I created a sensor that contains all that information. It uses the link provided by Vizio to get the data and processes it with JQ to format it into something better.

##
## Vizio Apps
##
- sensor:
      scan_interval: 36000
      unique_id: sensor.vizio_apps
      name: Vizio Apps
      command: "curl -s 'http://scfs.vizio.com/appservice/vizio_apps_prod.json' | jq '{\"apps\": [ .[] | {id: .\"id\", name: .\"name\", icon: .\"mobileAppInfo\".\"app_icon_image_url\", sort: .\"mobileAppInfo\".\"featured_sort\" } ]}' "
      value_template: "OK"
      json_attributes:
          - apps

If you create that sensor, look at what you get:

So I can then process that sensor with auto-entities and which overall results in a “scrollable” pad that have images of all the apps, clicking on one will take you to it. Now this uses decluttering so the actual vizio TV name (as I have 4) is a variable:

                    - type: custom:stack-in-card
                      mode: vertical
                      card_mod:
                        style: 'ha-card {overflow-y: scroll!important; height: 450px}'
                      cards:
                        - type: custom:auto-entities
                          card:
                            type: grid
                            square: false
                            columns: 5
                          card_param: cards
                          filter:
                            template: >-
                              {% for app in
                              state_attr("sensor.vizio_apps","apps") -%}
                                {{
                                  {
                                    'type': 'picture',
                                    'image': app.icon,
                                    'tap_action':
                                    {
                                        'action': 'call-service',
                                        'service': 'media_player.select_source',
                                        'data':
                                        {
                                          'source': app.name,
                                          'entity_id': 'media_player.[[vizio]]'
                                        }
                                    }
                                  }
                                 }},
                               {%- endfor %}

And in the overall result, I get this:

image

Now I plug that into my remote as part of the stack with the condition that the Vizio is on Smartcast, show it. In this you will also see that by TV, I left a row with 5 logos to be the quick buttons. Overall, in Vizio Smartcast mode, mine looks like this now:

As for the actual key codes, I used this to understand it all:

And note … the Android TV integration is nice. This is so much more. Everyone is always struggling to launch an app. This launches every app that exist on Vizio Smartcast and has nothing custom, since Vizio freely gives that information as well as the logos. Especially because it includes DirecTV also.

1 Like

port 7345 typo?

I would say so, should be as you said. 7345

Along with the “735”->“7345” problem, I think the payload format is entirely wrong for the /app/launch endpoint.
Here’s what I have working, at least for the few that I care about (though some others don’t work, and I don’t know why not)

  payload: '{"VALUE": {"MESSAGE": "{{ message | string }}", "NAME_SPACE": {{ name_space | int }}, "APP_ID": "{{ app_id | string }}"}}'

And the button calls them like this

          data:
            message: ""
            name_space: 2
            app_id: 4

This works for Hulu (2, 3) and Prime Video (2, 4), and that’s good enough for me at the moment.
But the numbers I found for Netflix, for example (3, 1) do not work, so if I ever care about that again, I’ll probably switch over to the media_player.select_source approach that @kbrown01 uses, unless I see a better list of name_space and app_id codes out there.
EDIT: I figured out the problem. Sample code above now works for Netflix, YouTube and others in non-“2” NAME_SPACE. See next reply.

Ah, with a little more fiddling (and comparing to exactly what pyvizio does) I see there need to be no quotes around the value for NAME_SPACE. For whatever reason, NAME_SPACE has to be an integer, while APP_ID is a string.
The reason a bunch of things were working for me might be that it defaults to a value of 2 for NAME_SPACE if it receives it but can’t process it properly (e.g. it arrives as a string when it shouldn’t be).

I’ll edit my previous reply so the sample code is correct.

Just a general heads up to post in this thread for the Vizio TV Integration:

With the latest firmware update to my television, the media player “turn on” and “turn off” actions no longer map to Power On and Power Off. Instead, it’s just a toggle. I have not yet figured out a work-around, but I’m sure more of you will be discovering this if you have firmware set to auto update.

I’m having an issue where the integration wizard just spins and never completed. Any idea on how to debug this one?

I"m stuck on the input selection problem, on one of my TV’s it’s the Hdmi-1 / Hdmi1 problem, I have another Vizio that just throws this error.

2024-10-11 16:59:15.426 ERROR (MainThread) [custom_components.vizio.media_player] Exception in input selection: int() argument must be a string, a bytes-like object or a real number, not 'NoneType'

I also ran into an issue where this TV would pair in Home Assistant but the device and entity would not create - I had to update the this to get past it -

            if VIZIO_MUTE in audio_settings:
                mute_value = audio_settings[VIZIO_MUTE]
    
            if isinstance(mute_value, str):
                self._attr_is_volume_muted = mute_value.lower() == VIZIO_MUTE_ON
            elif isinstance(mute_value, int):
        # Assuming 1 represents muted and 0 represents unmuted
                self._attr_is_volume_muted = mute_value == 1
            else:
                self._attr_is_volume_muted = None
        else:
            self._attr_is_volume_muted = None

Would love to dig into pyvizio to see how we can get this integration modernized.

I updated the input selection to try to get past my NoneType Error only was not able to get it resolved.

    async def async_select_source(self, source: str) -> None:
        """Select input source."""
        _LOGGER.debug("Selecting source: %s", source)
        # Fail early if the source is invalid or unavailable
        if not self._available_inputs or source not in self._available_inputs:
            _LOGGER.warning("Invalid or unavailable source selected: %s", source)
            return
        try:
            # Fetch the latest input list from the device
            fetched_inputs = await self._device.get_inputs_list(log_api_exception=False)
            if not fetched_inputs:
                _LOGGER.error("No inputs fetched from the device.")
                return
            _LOGGER.debug("Fetched %d inputs from the device.", len(fetched_inputs))
            # Find the matching input item by source name
            curr_input_item = next((input_ for input_ in fetched_inputs if input_.name == source), None)
            if not curr_input_item:
                _LOGGER.error("No matching input found for source: %s", source)
                return
            # Use id if available, otherwise fall back to name
            input_identifier = curr_input_item.id or curr_input_item.name
            if not input_identifier:
                _LOGGER.error("No valid id or name for input item: %s", curr_input_item)
                _LOGGER.debug("Invalid input item details: %s", curr_input_item)
                return
            # Set the input and confirm success
            _LOGGER.info("Setting input using: %s", input_identifier)
            await self._device.set_input(input_identifier, log_api_exception=False)
            _LOGGER.info("Successfully set input to: %s", input_identifier)
        except TypeError as e:
            _LOGGER.error("TypeError encountered while selecting input: %s", str(e))
        except ValueError as e:
            _LOGGER.error("ValueError encountered while selecting input: %s", str(e))
        except Exception as e:
            _LOGGER.error("Unexpected error selecting input: %s", str(e))
024-10-11 18:17:28.893 DEBUG (MainThread) [custom_components.vizio.media_player] Selecting source: HDMI-1
2024-10-11 18:17:29.051 DEBUG (MainThread) [custom_components.vizio.media_player] Fetched 5 inputs from the device.
2024-10-11 18:17:29.052 INFO (MainThread) [custom_components.vizio.media_player] Setting input using: 1118785733
2024-10-11 18:17:29.184 INFO (MainThread) [custom_components.vizio.media_player] Successfully set input to: 1118785733
2024-10-11 18:17:36.104 DEBUG (MainThread) [custom_components.vizio.media_player] Selecting source: COMP
2024-10-11 18:17:36.235 DEBUG (MainThread) [custom_components.vizio.media_player] Fetched 5 inputs from the device.
2024-10-11 18:17:36.235 INFO (MainThread) [custom_components.vizio.media_player] Setting input using: 3372802964
2024-10-11 18:17:36.347 INFO (MainThread) [custom_components.vizio.media_player] Successfully set input to: 3372802964
2024-10-11 18:17:41.051 DEBUG (MainThread) [custom_components.vizio.media_player] Selecting source: TV
2024-10-11 18:17:41.088 DEBUG (MainThread) [custom_components.vizio.media_player] Fetched 5 inputs from the device.
2024-10-11 18:17:41.088 INFO (MainThread) [custom_components.vizio.media_player] Setting input using: 3845225872
2024-10-11 18:17:41.202 INFO (MainThread) [custom_components.vizio.media_player] Successfully set input to: 3845225872
2024-10-11 18:17:46.397 DEBUG (MainThread) [custom_components.vizio.media_player] Selecting source: HDMI-1
2024-10-11 18:17:46.431 DEBUG (MainThread) [custom_components.vizio.media_player] Fetched 5 inputs from the device.
2024-10-11 18:17:46.431 INFO (MainThread) [custom_components.vizio.media_player] Setting input using: 1118785733
2024-10-11 18:17:46.604 INFO (MainThread) [custom_components.vizio.media_player] Successfully set input to: 1118785733

Even though it says the inputs changed when I run the CLI the inputs do not actually change.

I’m trying to integrate an older Vizio smartcast, and I am running into the same problem.

Either media player power on/off, is just a toggle, or the opposite action is performed. For example, ‘media player turn on’ results in the tv powering off half the time. Or ‘media player turn off’ will turn the tv ON half the time…