Custom Integration: Linksys Velop

You’ll need to ensure that it is indented one level under the views item in that file.
Alternatively you could do the following…

  1. Create a new view
  2. Add a manual card as the first item on the view and use the following code
Code for 1st card
type: custom:button-card
color_type: blank-card
  1. Add another manual card with the following code
Code for 2nd card
type: custom:stack-in-card
cards:
  - type: custom:button-card
    entity: binary_sensor.velop_mesh_wan_status
    show_name: false
    icon: hass:web
    tap_action:
      action: none
    custom_fields:
      attr_dns_servers: '[[[ return entity.attributes.dns ]]]'
      attr_public_ip: '[[[ return entity.attributes.ip ]]]'
      attr_speedtest_latest: |
        [[[
          var entity_speedtest = states['sensor.velop_mesh_speedtest_latest']          
          var d = new Date(entity_speedtest.state)
          return d.toLocaleString()
        ]]]
      attr_speedtest_details: |
        [[[
          var round2 = (num) => Math.round(num * 100) / 100
          var spacing_internal = 5
          var spacing_external = 30
          var icon_size = 22
          var entity_speedtest = states['sensor.velop_mesh_speedtest_latest']
          var latency = entity_speedtest.attributes.latency
          var download_bandwidth = round2(entity_speedtest.attributes.download_bandwidth / 1000)
          var upload_bandwidth = round2(entity_speedtest.attributes.upload_bandwidth / 1000)        

          return `<span style="margin-right: ${spacing_external}px;">
                    <ha-icon icon="hass:swap-horizontal" style="width: ${icon_size}px;"></ha-icon>
                    <span>${latency}ms</span>
                  </span>
                  <span style="margin-right: ${spacing_external}px;">
                    <ha-icon icon="hass:cloud-download-outline" style="width: ${icon_size}px;"></ha-icon>
                    <span>${download_bandwidth} Mbps</span>
                  </span>
                  <span>
                    <ha-icon icon="hass:cloud-upload-outline" style="width: ${icon_size}px;"></ha-icon>
                    <span>${upload_bandwidth} Mbps</span>
                  </span>
                  `
        ]]]
    state:
      - value: 'on'
        color: darkcyan
      - value: 'off'
        color: darkred
    styles:
      card:
        - padding: 16px
      grid:
        - grid-template-areas: >-
            "attr_dns_servers . attr_public_ip" "i i i" "attr_speedtest_details
            attr_speedtest_details attr_speedtest_details"
            "attr_speedtest_latest attr_speedtest_latest attr_speedtest_latest"
        - grid-template-rows: 5% 1fr 15% 5%
        - grid-template-columns: 1fr min-content 1fr
      custom_fields:
        attr_dns_servers:
          - justify-self: self-start
        attr_public_ip:
          - justify-self: self-end
    extra_styles: |
      div[id^="attr_"] { font-size: smaller; color: var(--disabled-text-color);
      }
      div[id^="attr_speedtest_"] { margin-top: 10px; }
      #attr_speedtest_latest::before { content: 'As at:' }
      #attr_public_ip::before { content: 'Public IP: ' }
      #attr_dns_servers::before { content: 'DNS: ' }
  - type: entities
    entities:
      - type: conditional
        conditions:
          - entity: binary_sensor.velop_mesh_speedtest_status
            state: 'on'
        row:
          type: divider
      - type: conditional
        conditions:
          - entity: binary_sensor.velop_mesh_speedtest_status
            state: 'on'
        row:
          type: custom:button-card
          entity: binary_sensor.velop_mesh_speedtest_status
          show_icon: false
          show_name: false
          show_label: true
          label: '[[[ return entity.attributes.status ]]]'
          tap_action:
            action: none
          styles:
            card:
              - box-shadow: none
              - padding: 4px
        card_mod:
          style:
            hui-attribute-row$:
              hui-generic-entity-row$: |
                state-badge, .info.pointer.text-content { display: none; }
              hui-generic-entity-row: >
                div { text-align: center !important; width: 100%; margin: 0;
                padding: 4px; }
      - type: divider
      - type: custom:stack-in-card
        mode: horizontal
        keep:
          margin: true
        card_mod:
          style: |
            ha-card { box-shadow: none }
        cards:
          - type: custom:button-card
            entity: binary_sensor.velop_mesh_check_for_updates_status
            tap_action:
              action: call-service
              service: linksys_velop.check_updates
            name: Check for Update
            icon: hass:update
            state:
              - value: 'on'
                color: darkcyan
                icon: hass:refresh
                spin: true
              - value: 'off'
                color: var(--primary-text-color)
            styles:
              card:
                - margin-bottom: 3px
              name:
                - white-space: normal
                - font-size: smaller
                - color: |
                    [[[
                      var ret = 'var(--primary-text-color)'
                      if (entity.state == 'on') {
                        ret = 'darkcyan'
                      }
                      return ret
                    ]]]
          - type: custom:button-card
            entity: switch.velop_mesh_guest_wi_fi
            tap_action:
              action: none
            name: Guest<br />Wi-Fi
            state:
              - value: 'on'
                color: darkcyan
              - value: 'off'
                color: var(--primary-text-color)
            styles:
              card:
                - margin-bottom: 3px
              name:
                - font-size: smaller
                - color: |
                    [[[
                      var ret = 'var(--primary-text-color)'
                      if (entity.state == 'on') {
                        ret = 'darkcyan'
                      }
                      return ret
                    ]]]
          - type: custom:button-card
            entity: switch.velop_mesh_parental_control
            tap_action:
              action: toggle
            name: Parental<br />Control
            state:
              - value: 'on'
                color: darkcyan
              - value: 'off'
                color: var(--primary-text-color)
            styles:
              card:
                - margin-bottom: 3px
              name:
                - font-size: smaller
                - color: |
                    [[[
                      var ret = 'var(--primary-text-color)'
                      if (entity.state == 'on') {
                        ret = 'darkcyan'
                      }
                      return ret
                    ]]]
          - type: custom:button-card
            entity: binary_sensor.velop_mesh_speedtest_status
            tap_action:
              action: call-service
              service: linksys_velop.start_speedtest
            name: Speedtest
            icon: hass:refresh
            state:
              - value: 'on'
                color: darkcyan
                spin: true
              - value: 'off'
                color: var(--primary-text-color)
            styles:
              card:
                - margin-bottom: 3px
              name:
                - font-size: smaller
                - color: |
                    [[[
                      var ret = 'var(--primary-text-color)'
                      if (entity.state == 'on') {
                        ret = 'darkcyan'
                      }
                      return ret
                    ]]]
      - type: divider
      - type: custom:fold-entity-row
        padding: 0
        clickable: true
        head:
          type: custom:template-entity-row
          entity: sensor.velop_mesh_online_devices
          tap_action:
            action: fire-dom-event
            fold_row: true
          name: >-
            {% set friendly_name = state_attr(config.entity, 'friendly_name') %}
            {% if friendly_name %}
              {{ friendly_name.split(':')[1].strip() }}
            {% endif %}
          card_mod:
            style: |
              state-badge { display: none; }
              state-badge + div { margin-left: 8px !important; }
              .info.pointer { font-weight: 500; }
              .state { margin-right: 10px; }
        entities:
          - type: custom:hui-element
            card_type: markdown
            card_mod:
              style:
                .: |
                  ha-card { border-radius: 0px; box-shadow: none; }
                  ha-markdown { padding: 16px 0px 0px !important; }
                ha-markdown$: >
                  table { width: 100%; border-collapse: separate;
                  border-spacing: 0px; }

                  tbody tr:nth-child(2n+1) { background-color:
                  var(--table-row-background-color); }

                  thead tr th, tbody tr td { padding: 4px 10px; }
            content: >
              {% set devices = state_attr('sensor.velop_mesh_online_devices',
              'devices') %} | # | Name | IP | Type |

              |:---:|---|---|:---:| {%- for device in devices -%}
                {% set idx = loop.index %}
                {%- for device_name, device_details in device.items() -%}
                  {%- set device_ip = device_details.keys() | list | first -%}
                  {%- set connection_type = device_details.values() | list | first | lower -%}
                  {%- if connection_type == "wired" -%}
                    {%- set connection_icon = "ethernet" -%}
                  {% elif connection_type == "wireless" -%}
                    {%- set connection_icon = "wifi" -%}
                  {% elif connection_type == "unknown" -%}
                    {%- set connection_icon = "help" -%}
                  {% else -%}
                    {%- set connection_icon = "" -%}
                  {%- endif %}
              {{ "| {} | {} | {} | {} |".format(idx, device_name, device_ip,
              '<ha-icon icon="hass:' ~ connection_icon ~ '"></ha-icon>') }}
                {%- endfor %}
              {%- endfor %}
      - type: custom:fold-entity-row
        padding: 0
        clickable: true
        head:
          type: custom:template-entity-row
          entity: sensor.velop_mesh_offline_devices
          tap_action:
            action: fire-dom-event
            fold_row: true
          name: >-
            {% set friendly_name = state_attr(config.entity, 'friendly_name') %}
            {% if friendly_name %}
              {{ friendly_name.split(':')[1].strip() }}
            {% endif %}
          card_mod:
            style: |
              state-badge { display: none; }
              state-badge + div { margin-left: 8px !important; }
              .info.pointer { font-weight: 500; }
              .state { margin-right: 10px; }
        entities:
          - type: custom:hui-element
            card_type: markdown
            card_mod:
              style:
                .: |
                  ha-card { border-radius: 0px; box-shadow: none; }
                  ha-markdown { padding: 16px 0px 0px !important; }
                ha-markdown$: >
                  table { width: 100%; border-collapse: separate;
                  border-spacing: 0px; }

                  tbody tr:nth-child(2n+1) { background-color:
                  var(--table-row-background-color); }

                  thead tr th, tbody tr td { padding: 4px 10px; }
            content: >
              {% set devices = state_attr('sensor.velop_mesh_offline_devices',
              'devices') %}

              | # | Name |

              |:---:|---|

              {% for device in devices %} {{ "| {} | {} |".format(loop.index,
              device) }}

              {% endfor %}
  1. Add another manual card with following code
Code for 3rd card
type: custom:auto-entities
card:
  type: vertical-stack
card_param: cards
filter:
  include:
    - entity_id: /^binary_sensor\.velop_(?!(mesh)).*_status/
      options:
        type: custom:config-template-card
        variables:
          ID_CONNECTED_DEVICES: >
            "sensor." + "this.entity_id".split(".")[1].split("_").slice(0,
            -1).join("_") + "_connected_devices"
          ID_MODEL: >
            "sensor." +
            "this.entity_id".split(".")[1].split("_").slice(0,-1).join("_") +
            "_model"
          ID_PARENT: >
            "sensor." +
            "this.entity_id".split(".")[1].split("_").slice(0,-1).join("_") +
            "_parent"
          ID_SERIAL: >
            "sensor." +
            "this.entity_id".split(".")[1].split("_").slice(0,-1).join("_") +
            "_serial"
          ID_UPDATE_AVAILABLE: >
            "binary_sensor." +
            "this.entity_id".split(".")[1].split("_").slice(0,-1).join("_") +
            "_update_available"
          CONNECTED_DEVICES_TEXT: |
            (entity_id) => {
              var ret = `
            | # | Name | IP | Type |
            |:---:|---|---|:---:|
            `
              if (states[entity_id].attributes.devices) {
                states[entity_id].attributes.devices.forEach((device, idx) => {
                  var connection_icon
                  switch (device.type.toLowerCase()) {
                    case "wireless":
                      connection_icon = "wifi"
                      break
                    case "wired":
                      connection_icon = "ethernet"
                      break
                    case "unknown":
                      connection_icon = "help"
                      break
                  }
                  ret += "| " + (idx + 1) + " | " + device.name + " | " + device.ip + " | <ha-icon icon='hass:" + connection_icon + "'></ha-icon> |\n"
                })
              }
              return ret
            }
        entities:
          - this.entity_id
          - ${ID_CONNECTED_DEVICES}
          - ${ID_MODEL}
          - ${ID_PARENT}
          - ${ID_SERIAL}
          - ${ID_UPDATE_AVAILABLE}
        card:
          type: custom:stack-in-card
          cards:
            - type: custom:button-card
              entity: this.entity_id
              aspect_ratio: 3/1
              size: 100%
              show_entity_picture: true
              show_last_changed: true
              show_state: true
              entity_picture: ${'/local/velop_nodes/' + states[ID_MODEL].state + '.png'}
              name: |
                [[[
                  var ret = entity.attributes.friendly_name
                  if (ret) {
                    ret = ret.replace("Velop", "").split(":")[0].trim()
                  }
                  return ret || "N/A"
                ]]]
              state_display: |
                [[[
                  return `<ha-icon 
                    icon="hass:checkbox-blank-circle"
                    style="width: 24px; height: 24px;">
                    </ha-icon>`
                ]]]
              custom_fields:
                attr_label_model: Model
                attr_model: ${states[ID_MODEL].state}
                attr_label_serial: Serial
                attr_serial: ${states[ID_SERIAL].state}
                attr_parent: >-
                  ${(states[ID_PARENT].state && states[ID_PARENT].state !=
                  'unknown') ? 'Connected to ' + states[ID_PARENT].state :
                  'N/A'}
                attr_label_ip: IP Address
                attr_ip: '[[[ return entity.attributes.ip || ''N/A'' ]]]'
                attr_update: |
                  [[[
                    var ret
                    var entity_update = 'binary_sensor.' + entity.entity_id.split('.')[1].split('_').slice(0, -1).join('_') + '_update_available'
                    var update_available = states[entity_update].state
                    if (update_available == 'on') {
                      ret = `<ha-icon
                          icon="hass:package-up"
                          style="width: 24px; height: 24px;"
                        >
                        </ha-icon>`
                    }
                    return ret
                  ]]]
              extra_styles: >
                div[id^="attr_"] { justify-self: end; } div[id^="attr_label_"] {
                justify-self: start; margin-left: 20px; } #label, #attr_parent {
                padding-top: 25px; font-size: smaller; }
              styles:
                card:
                  - padding: 16px
                grid:
                  - grid-template-areas: >-
                      "n n n" "i attr_label_model attr_model" "i
                      attr_label_serial attr_serial" "i attr_label_ip attr_ip"
                      "l l attr_parent"
                  - grid-template-rows: 1fr 1fr 1fr 1fr 1fr
                  - grid-template-columns: 15% 1fr max-content
                name:
                  - font-size: larger
                  - justify-self: start
                  - padding-bottom: 20px
                label:
                  - justify-self: start
                custom_fields:
                  attr_parent:
                    - justify-self: end
                  attr_update:
                    - position: absolute
                    - top: 8px
                    - right: 48px
                    - color: darkred
                state:
                  - position: absolute
                  - top: 8px
                  - right: 16px
                  - color: |-
                      [[[
                        return (entity.state == 'on' ? 'darkcyan' : 'darkred')
                      ]]]
            - type: entities
              card_mod:
                style: |
                  #states { padding-left: 8px; padding-right: 8px; }
              entities:
                - type: divider
                - type: custom:fold-entity-row
                  padding: 0
                  clickable: true
                  group_config:
                    card_mod:
                      style:
                        hui-generic-entity-row:
                          $: |
                            state-badge { display: none; }
                            state-badge + div { margin-left: 8px !important; }
                  head:
                    type: custom:template-entity-row
                    entity: ${ID_CONNECTED_DEVICES}
                    tap_action:
                      action: fire-dom-event
                      fold_row: true
                    name: >-
                      {% set name = state_attr(config.entity, 'friendly_name')
                      %} {% if name %}
                        {{ name.split(':')[1].strip() }}
                      {% endif %}
                    card_mod:
                      style: |
                        state-badge { display: none; }
                        state-badge + div { margin-left: 8px !important; }
                        .info.pointer { font-weight: 500; }
                        .state { margin-right: 10px; }
                  entities:
                    - type: custom:hui-element
                      card_type: markdown
                      card_mod:
                        style:
                          .: |
                            ha-card { border-radius: 0px; box-shadow: none; }
                            ha-markdown { padding: 16px 0px 0px !important; }
                          ha-markdown$: >
                            table { width: 100%; border-collapse: collapse; }

                            tbody tr:nth-child(2n+1) { background-color:
                            var(--table-row-background-color); }

                            thead tr th, tbody tr td { padding: 4px 10px; }
                      content: ${CONNECTED_DEVICES_TEXT(ID_CONNECTED_DEVICES)}

Thank you som much. It works like a charm. :slight_smile:

The only thing I don’t get to work is the pictures of the nodes on card 3. I see in line 61 that there is a line with entity_picture. I uploaded an image to my www folder and changed line 61 to /www/velop_nodes.png/
But I still don’t get an image on the card.

Ah yes. I should probably have been a bit more explicit about that.
The images should be placed in a folder called velop_nodes in the www folder and named according to the model number and in PNG format. Hopefully the below screenshot shows what I mean. You may also need to force reload the page (shortcut for that typically depends on browser but CTRL + F5 normally does it).

image

Great. Thank you for the clarification. :slight_smile:

Hi, I’m really new to Home Assistant and I really don’t know how to install your custom integration.
I got 5 nodes that I would love to integrate into my setup. Can you please guide me as to how to install your integration.
Thank you in advance.

Got it working. thank you so much for this integration.

Unfortunately, I am not able to log in. Logging in via https://linksysremotemanagement.com works. If I use the same password for “192.168.178.54”, I cannot log in: Continuous loop.


Did the integration find your router automatically?

Are there any logs in Configuration --> Logs?

Based on the screenshot I would guess that you’re router is actually 192.168.178.1 rather than .54 - you could try that if the router wasn’t automatically found.

Well, I use the three Linksys WHW0303 Velop in bridged function, i.e. one of them is connected to the FritzBox 6591 cable (this is the router).

Unfortunately no automatically discovery.

192.168.178.1 or http://192.168.178.1

have unfortunately not solved the problem.

I see. I haven’t enabled bridged mode on any of the nodes. Currently my primary node is plugged into the back of the router. If none of the nodes are acting as a gateway then the auto-discovery won’t work. It may also be that the JNAP API responds differently.

If you click on those messages in the log screen (the ones in your screenshot) could you send over the logs?

and

Leave it with me. I have a spare node that I can switch into bridged mode and see how it responds.

1 Like

Thank you, I thank you very much for your efforts

I’ve switched a node to bridged mode now but it doesn’t look like it’s going to give you much information. It seems to respond to all the same queries but can’t distinguish between node types. However, this may just be to do with how I have it working (I’ve left my main mesh up and running - too disruptive to take it down and just plugged this one in to check how it responds).

If you’re up for running some commands against yours we can see if yours behaves the same. The commands can be executed using Postman and I can build you a collection to just run to get back the results.

Also, the password you’ll need to use is the one that you use to login directly to the node (not via the Internet based service). So in your case, if you were to navigate to 192.168.178.54 you should see a Linksys page that will allow you to login (you may have to click a link towards the bottom of the page first. It’s the password you use there that you’ll need to use for the integration.

I did add the bridge manually to mine HA instance but it didn’t show much to do with the mesh due to the aforementioned thing of not recognising the node types.

1 Like

I’m using the component without issue with my whole Velop setup running in Bridge Mode…if I can help here let me know what you need :+1:t2:

1 Like

It was my mistake. I didn’t use the router password (but the password from the web portal instead). It works fine now. Thank you very much for your efforts.

Cheers. Thanks for the offer. I may have to take you up on that offer at some point.

I’m pretty sure I know why my node in bridged mode wasn’t behaving as expected but unfortunately not a lot I can do to solve that without reconfiguring my network.

1 Like

That’s good then.

You did however find a bug which I’ll hopefully resolve soon - the config shouldn’t just sit spinning forever so I’ll try and recreate that and get it fixed in a future release.

1 Like

@Gav_in and @fastender - do either of you have access to a SSDP/UPnP browser? Just wondering if the Velop in bridge mode does announce itself.