Z-Wave graph (without the python)


#63

When I add that update, I just get a blank screen. This part seems to break it:

g.node(19).x=750;
g.node(19).y=20;

I made some changes to moves things around on mine,

<dom-module id='ha-panel-zwavegraph2'>
  <template>
    <style include="ha-style">
      .node.Layer1 > rect, .node.Layer1 > circle
      {
        fill: lightblue;
      }

      .node.Layer2 > rect, .node.Layer2 > circle
      {
        fill: yellow;
      }

      .node.Layer3 > rect, .node.Layer3 > circle
      {
        fill: green;
      }

      .node.Layer4 > rect, .node.Layer4 > circle
      {
        fill: orange;
      }

      .node.Layer5 > rect, .node.Layer5 > circle
      {
        fill: grey;
      }

      .node.Error > rect, .node.Error > circle
      {
        fill: red;
      }

      .node.unset > rect, .node.unset > circle
      {
        fill: white;
      }

      .node > rect, .node > circle
      {
        stroke: black;
      }

      .node text
      {
        font-size: 12px;
      }

      .edgePath path {
        stroke: #333;
        fill: #333;
        stroke-width: 1.5px;
      }

    </style>

    <app-header-layout has-scrolling-region>
      <app-header slot="header" fixed>
        <app-toolbar>
          <ha-menu-button narrow='[[narrow]]' show-menu='[[showMenu]]'></ha-menu-button>
          <div main-title>Zwave Graph</div>
        </app-toolbar>
      </app-header>

      <div class="content">
        <svg id="svg"></svg>
      </div>
    </app-header-layout>

  </template>

</dom-module>
<script src="https://d3js.org/d3.v4.min.js" charset="utf-8"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/dagre-d3/0.6.1/dagre-d3.js"></script>
<script>
class HaPanelZWave extends Polymer.Element {
  static get is() { return 'ha-panel-zwavegraph2'; }

  static get properties() {
    return {
      // Home Assistant object
      hass: Object,
      // If should render in narrow mode
      narrow: {
        type: Boolean,
        value: false,
      },
      // If sidebar is currently shown
      showMenu: {
        type: Boolean,
        value: false,
      },
      // Home Assistant panel info99
      // panel.config contains config passed to register_panel serverside
      panel: Object,
    };
  }


  ready() {
    super.ready();

    var data=this.listNodes(this.hass);


    var g = new dagreD3.graphlib.Graph().setGraph({});
    g.graph().rankDir="BT";
    g.graph().nodeSep=15;

    for (var i = 0; i < data["nodes"].length; i++) {
      var node=data["nodes"][i];
      g.setNode(node.id, node);
    }

    for (var i =0; i< data["edges"].length; i++)
    {
      g.setEdge(data["edges"][i].from, data["edges"][i].to, {label:"", arrowhead: "undirected"})
    }

    // Create the renderer
    var render = new dagreD3.render();

    var svg=d3.select(this.$.svg);
    var inner = svg.append("g").attr("transform", "translate(20,200)scale(1)");

    g.graph().minlen = 0;

    // Run the renderer. This is what draws the final graph.
    render(inner, g);


    // Add the title element to be used for a tooltip (SVG functionality)
    inner.selectAll("g.node")
        .append("title").html(function(d) {return g.node(d).title;});
    svg.attr('height', g.graph().height + 250);
    svg.attr('width', g.graph().width + 250);

    var legends=[{shape: "rect", color:"lightblue", text:"Hub"},
                 {shape: "rect", color:"yellow", text:"1 hop"},
                 {shape: "rect", color:"green", text:"2 hops"},
                 {shape: "rect", color:"orange", text:"3 hops"},
                 {shape: "rect", color:"grey", text:"4 hops"},
                 {shape: "rect", color:"red", text:"Failed Node"},
                 {shape: "rect", color:"white", text:"Unconnected"}];

    this.addLegend(svg, legends, 5, 20);


    legends = [{shape: "circle", text: "Mains power", color: "black"},
               {shape: "rect", text: "Battery power", color: "black"}]

    this.addLegend(svg, legends, svg.attr("width")*0.5, 20)



  }

  addLegend(svg, legends, startX, startY)
  {
    for(var counter=0;counter < legends.length;counter++)
    {
      if (legends[counter].shape == "circle")
      {
        svg.append(legends[counter].shape)
          .attr('cx', startX + 5 )
          .attr('cy',startY + 5 + 20 * counter)
          .attr('r', 5)
          .style("stroke", "black")
          .style("fill", legends[counter].color);
      }
      else
      {
        svg.append(legends[counter].shape)
          .attr('x',startX)
          .attr('y',startY + 20 * counter)
          .attr('width', 10)
          .attr('height', 10)
          .style("stroke", "black")
          .style("fill", legends[counter].color);
      }
      svg.append('text')
        .attr("x", startX + 20)
        .attr("y", startY + 10 + 20*counter)
        .text(legends[counter].text)
        .attr("class", "textselected")
        .style("text-anchor", "start")
        .style("font-size", 15);

    }

  }

  listNodes(hass) {
    let states=new Array();
    for (let state in hass.states)
    {
      states.push({name:state, entity:hass.states[state]});
    }
    let zwaves = states.filter((s) => {return s.name.indexOf("zwave.") ==0});
    let result= {"edges":[], "nodes":[]};

    let hubNode=0;
    let neighbours={};

    for (let b in zwaves)
    {
       let id=zwaves[b].entity.attributes["node_id"];
       let node = zwaves[b].entity;
       if (node.attributes["capabilities"].filter(
                        (s) => {return s =="primaryController"}).length > 0)
       {
         hubNode=id;
       }
       neighbours[id]=node.attributes['neighbors'];

       let entities = states.filter((s) => {
                    return ((s.name.indexOf("zwave.") == -1) &&
                            (s.entity.attributes["node_id"] == id)) });
       let batlev=node.attributes.battery_level;
       let entity={"id":  id,
                   "label": (node.attributes["node_name"] + " (" + node.attributes["averageResponseRTT"]+"ms)").replace(/ /g, "\n"),
                   "class": "unset",
                   "shape": batlev != undefined ? "rect" : "circle",
                   "title": "<b>"+node.attributes["node_name"]+"</b>" +
                            "<br />Node: " + id + (node.attributes["is_zwave_plus"] ? "+" : "") +
                            "<br />Product Name: " + node.attributes["product_name"] +
                            "<br />Average Request RTT: " + node.attributes["averageResponseRTT"]+"ms" +
                            "<br />Power source: " + (batlev != undefined ? "battery (" + batlev +"%)" : "mains") +
                            "<br />" + entities.length + " entities",
                   "forwards": (node.attributes.is_awake && node.attributes.is_ready && !node.attributes.is_failed &&
                                node.attributes.capabilities.includes("listening"))
                  };

       if (node.attributes["is_failed"])
       {
         entity.label = "FAILED: "+entity.label;
         entity["font.multi"]=true;
         entity["title"]="<b>FAILED: </b>"+entity.title;
         entity["group"]="Failed";
       }

       if (hubNode == id)
       {
         entity.label="ZWave Hub";
         entity.borderWidth= 2;
         entity.fixed=true;
       }

       result.nodes.push(entity);
    }


    if (hubNode > 0)
    {
      let layer=0;
      let previousRow=[hubNode];
      let mappedNodes=[hubNode];
      while (previousRow.length > 0)
      {
        layer = layer+1;
        let nextRow=[];
        for (let target in previousRow)
        {
          result.nodes.filter((n) => {return ((n.id ==previousRow[target]) && (n.group="unset"))})
                      .every((d) => {d.class="Layer" + layer;})

          if (result.nodes.filter((n) => {return ((n.id == previousRow[target]) && (n.forwards))}).length > 0)
          {
            let row=neighbours[previousRow[target]];
            for (let node in row)
            {

              if (!mappedNodes.includes(row[node]))
              {
                result.edges.push({"from":row[node], "to":previousRow[target]});
                nextRow.push(row[node]);
              }
            }
          }
        }

        for (let idx in nextRow)
        {
          mappedNodes.push(nextRow[idx]);
        }
        previousRow = nextRow;
      }
    }
    return result;
   }


}
customElements.define(HaPanelZWave.is, HaPanelZWave);
</script>

Specifically widened the legend and moved the second column over (it was cut off),
svg.attr(‘height’, g.graph().height + 250);
svg.attr(‘width’, g.graph().width + 250);
this.addLegend(svg, legends, svg.attr(“width”)*0.5, 20)

And pushed the graph down futher, it was still underneath my legend.
var inner = svg.append(“g”).attr(“transform”, “translate(20,200)scale(1)”);


#64

Sorry about the error. I’ve removed the two lines from that version of the code, so other people can get it now and it should work.

And I’m glad you could adapt the code to work for you :slight_smile:


#66

Mains powered Aeotec Multisensors 6 are incorrectly shown as battery operated.
16
35


#67

Same here. Worked in IE but not in Chrome. same error

static/custom-elements-es5-adapter.js:13:615 Uncaught TypeError: Class constructor HaPanelZWave cannot be invoked without 'new'


#68

@NigelL nice work! Kudos to @OmenWild and @happyleaves as well. I have a working prototype of a Hassio addon based on @OmenWild python code, but also thought this could be done directly from JS without running python to pull the zwave information. Found your project searching the community forums.

Looks like I need another project to learn to develop hassio addons. :wink:

regards,
Tim


#69

i just installed this and have a problem. If i restart the server and go in, everything shows up but unconnected. If I wait a bit and go back in, nothing shows up at all. any ideas? I’m running Hass.io v.80.3


#70

I’ve been having this same issue. Since sometime in October.


#71

Working for me on 0.82.1 using Firefox.


#72

This has been working great for me until 0.83.2 and now I get NOTHING on the screen

I just get this in the log:

Dec 02 09:39:43 docker c292dbc3479e[1188]: 2018-12-02 09:39:43 ERROR (MainThread) [frontend.js.latest.201811211] :0:0 Script error.
Dec 02 09:39:43 docker c292dbc3479e[1188]: 2018-12-02 09:39:43 ERROR (MainThread) [frontend.js.latest.201811211] :0:0 Script error.


#73

I’m not a JavaScript developer by any stretch, but building at the shoulders of giants like @NigelL, I’ve refined the panel slightly and made it a bit more self explanatory. Plus I am lazy so I wanted it to do some of the work for me… I hope it helps someone to use my improvements from the Gist here: https://gist.github.com/AdamNaj/cbf4d792a22f443fe9d354e4dca4de00

What I’ve done:

  • refined the colors
  • battery sensors now have a battery shape, hub has a home shape :slight_smile: - I’m a stickler for details
  • when you hover over a node - it will dim the nodes not connected to it in any way but put more emphasis on the connections from the currently highlighted node to make it easier to see them. Some screenshots of my improvements in action:

Initial graph:

Thanks again @NigelL - I wouldn’t be able to see my dependencies if not for your brilliant code!

btw. the graph might highlight nodes that do not have an edge between them - it just means that the other node reports that it has the currently highlighted node as their neighbor.

Tested to work on Home Assistant 0.82.3

FIXED: (posting here since as a new user I cannot respond more than 3 times)

  • Failed node should now report as red.
  • Mains power devices should no longer show as a battery shape even if they have a battery
  • Battery powered devices should have the background level reflect battery state.

#74

Graph with node selected:


#75

Neat!

At first I thought my second hop battery powered devices weren’t displaying any text but it was just the theme I had chosen.

Also while the Aeotec multisensors are battery devices they are being powered from USB (and are capable of being a mesh repeater in this case) , should they still show as a battery device?


#76

From what I could see in the code the detection of whether the device is marked as being battery or mains powered is when it reports battery level. Since I think your devices still do that - it means that they will show as battery powered.


#77

I tried your code and I like it but…

I’m still not getting the red indication for a failed node:

ex


#78

There are some devices such as smoke alarms that are mains powered and have a backup battery (and therefore report battery level). I think the correct parameter to look at is whether the devices have capability “routing”. That should be a better indicator than battery level.


#79

I agree, my thermostats can run on battery and mains power (C wire) but since it reports a battery level, even while powered by mains, it gets flagged as a “battery powered” device.


#80

I’m trying to make some changes, such as getting the panel to work in Chromium on Linux. Do I really need to restart Home Assistant for changes to take effect or is there some better way?

Edit: Figured it out. Ctrl+F5 in the browser will do it.


#81

I’m too stupid to get this running where do I have to save this file exactly? I’m on latest Hass version and I get configuration error in the Frontend but config check doesn’t show any error …could someone show me how to get this working please

Get this in logs

Log Details (ERROR)

Wed Dec 05 2018 23:16:47 GMT+0100 (Mitteleuropäische Normalzeit)

Unable to find webcomponent for zwavegraph2: /home/hass/.homeassistant/panels/zwavegraph2.html

But my HTML is located in this folder


#82

Put the zwavegraph2.html file in \config\panels\

Put this in your panel_custom section or include file:

- name: zwavegraph2
  sidebar_title: ZWave Graph2
  sidebar_icon: mdi:access-point-network
  url_path: zwave

#83

Ok …got it but my battery powered devices show up in grey …I’d like to see the neighbors…I thought this is aimed to be?