Custom Integration: Linksys Velop

Just a heads up. I’ve pushed a new release today. The only real things in there are preparing for the HASS 2021.11 release. They shouldn’t break anything mind you so just raise an issue on Github if you spot anything.

This is the new functionality added. Essentially I’ve made all binary sensors and sensors have the diagnostic category.
The Mesh device will also have the configuration_url set so that it’ll link to the primary node.

2 Likes

Another new version…

  • Add entity category for switches
  • Add UPnP Discovery Support
  • Set the API command timeout correctly on setup
  • Prevent duplicate config entries being created

You may have spotted that there’s a couple of big things hiding in there (UPnP support and preventing duplicates). You shouldn’t really notice these but they mean the following…

  1. For those that remove the integration, or are new users, the primary node should now be automatically discovered for you. When you configure you will still need to supply the password, timer details and select your device trackers.
  2. Existing users shouldn’t notice anything re: UPnP.

Switches have now been given the configuration category which should help with the layouts in Lovelace.

For those interested in what happens when adding the integration for new or existing users and when duplicates are prevented, I’ve written up what I’ve tested: -

  • New (manual) = created with unique_id
  • New (SSDP) = primary node automatically populated, created with unique_id
  • Existing (no unique_id, SSDP enabled) = unique_id populated when discovered
  • Existing (no unique_id, no SSDP) = no unique_id
  • Existing (no unique_id, no SSDP, try to add duplicate) = unique_id populated, duplicate aborted
  • Existing (unique_id, try to add duplicate) = duplicate aborted

The unique_id in each case is the serial number of the primary node. This was made more awkward by the fact that I missed adding the unique_id in the initial releases. Hopefully no-one should even notice a difference mind you. This essentially means that if you replace your primary node, then you’ll probably be safer removing the integration and re-adding (unless you fancy manually modifying the YAML file for config entries).

1 Like

Hello
I really liked your Lovelace layout and would like to use it, but when I copy the yaml and paste it in my lovelace I get all kinds of errors. I guess most of them are errors on intendations, but it is hard to see them all with so much code. Is there a way to just copy into the file and it works? I have installed all the cards you have used as well

Hi, whereabouts are you trying to paste the code?

In the editor for raw configuration. I tried pasting it at the end of my file

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