Z-Wave graph (without the python)

Battery powered devices are not mesh routers. They will only have one connection and I’m guessing you will have to catch it when it is awake to see that?

I can’t get this to work in Chrome; the Chrome Developer Console responds with:

The FetchEvent for "https://my.server/api/panel_custom/zwavegraph2"
resulted in a network error response: the promise was rejected.

Everything works fine in Safari.

EDIT: cache problem. Cleared the Chrome cache and everything shows up. Awesome!

Fixed and improved today:

  • 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.

Make sure to clear browser cache after uploading new version.

3 Likes

Nice additions @AdamNaj! Thanks to you and of course @NigelL’s great work on this. Super helpful component.

Thanks!

It works now.

1 Like

This is very nice thank you @NigelL & @AdamNaj

1 Like

What’s the latest JS file for this - is the first post edited as the newest?

The Gist is getting refreshed as I change things. Go ahead and re-download it if needed.

3 Likes

Dropping in to say I’ve just replaced the old PHP version with @AdamNaj’s updated code and everything worked without a hitch! Thanks everyone!

1 Like

This is a nice panel, thanks for your work. However there is a bug when loading the panel after excluding nodes from the network. If you remove a node the object (and entities) still exist, but some attributes are missing. In this case, capabilities is undefined and blows up:

zwavegraph:406 Uncaught TypeError: Cannot read property 'filter' of undefined
    at HTMLElement.listNodes (zwavegraph:406)
    at HTMLElement.ready (zwavegraph:195)
    at HTMLElement._enableProperties (app-0fd67575.js:1436)
    at HTMLElement.connectedCallback (app-0fd67575.js:1047)
    at HTMLElement.connectedCallback (app-0fd67575.js:1055)
    at Object.then (7dd1bce433b735e38b5d.chunk.js:5)

It’s probably an HA bug that these objects are still around. Anyways, I worked around it by checking if node.attributes.capabilities is undefined and just continued to the next (I’m no JS expert).

Updated the Gist to (hopefully) deal with the problem by filtering the nodes without “capabilities” earlier in the flow.

1 Like

Thanks, the update works fine.

This doesn’t seem to work now that I’ve upgraded to the 0.84.1 release. I’m getting a large dump of the text and a script error…

Still working for me.
Can you paste the script error?

Mine is working on 84.1. But I did notice my nodes moved around.

Where can I find latest code ? Don’t u have GitHub?

It’s just a Gist to be placed in config/panels as @NigelL described in his initial post.

I can try to fix it, but I’d need the errors that your browser is showing.

I’m not sure how helpful the log will be, but here it is!

The only thing I redacted was my hostname at the very end.

2018-12-13 18:47:11 ERROR (MainThread) [frontend.js.latest.201812110] data:text/javascript;charset=utf-8,%0Aclass%20HaPanelZWave%20extends%20Polymer.Element%20%7B%0A%20%20static%20get%20is()%20%7B%20return%20'ha-panel-zwavegraph2'%3B%20%7D%0A%0A%20%20static%20get%20properties()%20%7B%0A%20%20%20%20return%20%7B%0A%20%20%20%20%20%20%2F%2F%20Home%20Assistant%20object%0A%20%20%20%20%20%20hass%3A%20Object%2C%0A%20%20%20%20%20%20%2F%2F%20If%20should%20render%20in%20narrow%20mode%0A%20%20%20%20%20%20narrow%3A%20%7B%0A%20%20%20%20%20%20%20%20type%3A%20Boolean%2C%0A%20%20%20%20%20%20%20%20value%3A%20false%2C%0A%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20%2F%2F%20If%20sidebar%20is%20currently%20shown%0A%20%20%20%20%20%20showMenu%3A%20%7B%0A%20%20%20%20%20%20%20%20type%3A%20Boolean%2C%0A%20%20%20%20%20%20%20%20value%3A%20false%2C%0A%20%20%20%20%20%20%7D%2C%0A%20%20%20%20%20%20%2F%2F%20Home%20Assistant%20panel%20info99%0A%20%20%20%20%20%20%2F%2F%20panel.config%20contains%20config%20passed%20to%20register_panel%20serverside%0A%20%20%20%20%20%20panel%3A%20Object%2C%0A%20%20%20%20%7D%3B%0A%20%20%7D%0A%0A%0A%20%20ready()%20%7B%0A%20%20%20%20super.ready()%3B%0A%0A%20%20%20%20var%20data%3Dthis.listNodes(this.hass)%3B%0A%0A%0A%20%20%20%20var%20g%20%3D%20new%20dagreD3.graphlib.Graph().setGraph(%7B%7D)%3B%0A%20%20%20%20g.graph().rankDir%3D%22BT%22%3B%0A%20%20%20%20g.graph().nodeSep%3D15%3B%0A%0A%20%20%20%20for%20(var%20i%20%3D%200%3B%20i%20%3C%20data%5B%22nodes%22%5D.length%3B%20i%2B%2B)%20%7B%0A%20%20%20%20%20%20var%20node%3Ddata%5B%22nodes%22%5D%5Bi%5D%3B%0A%20%20%20%20%20%20g.setNode(node.id%2C%20node)%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20for%20(var%20i%20%3D0%3B%20i%3C%20data%5B%22edges%22%5D.length%3B%20i%2B%2B)%0A%20%20%20%20%7B%0A%20%20%20%20%20%20g.setEdge(data%5B%22edges%22%5D%5Bi%5D.from%2C%20data%5B%22edges%22%5D%5Bi%5D.to%2C%20%7Blabel%3A%22%22%2C%20arrowhead%3A%20%22undirected%22%7D)%0A%20%20%20%20%7D%0A%0A%20%20%20%20%2F%2F%20Create%20the%20renderer%0A%20%20%20%20var%20render%20%3D%20new%20dagreD3.render()%3B%0A%0A%20%20%20%20var%20svg%3Dd3.select(this.%24.svg)%3B%0A%20%20%20%20var%20inner%20%3D%20svg.append(%22g%22).attr(%22transform%22%2C%20%22translate(20%2C120)scale(1)%22)%3B%0A%0A%20%20%20%20g.graph().minlen%20%3D%200%3B%0A%0A%20%20%20%20%2F%2F%20Run%20the%20renderer.%20This%20is%20what%20draws%20the%20final%20graph.%0A%20%20%20%20render(inner%2C%20g)%3B%0A%0A%0A%20%20%20%20%2F%2F%20Add%20the%20title%20element%20to%20be%20used%20for%20a%20tooltip%20(SVG%20functionality)%0A%20%20%20%20inner.selectAll(%22g.node%22)%0A%20%20%20%20%20%20%20%20.append(%22title%22).html(function(d)%20%7Breturn%20g.node(d).title%3B%7D)%3B%0A%20%20%20%20svg.attr('height'%2C%20g.graph().height%20%2B%20140)%3B%0A%20%20%20%20svg.attr('width'%2C%20g.graph().width%20%2B%20140)%3B%0A%0A%20%20%20%20var%20legends%3D%5B%7Bshape%3A%20%22rect%22%2C%20color%3A%22lightblue%22%2C%20text%3A%22Hub%22%7D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7Bshape%3A%20%22rect%22%2C%20color%3A%22yellow%22%2C%20text%3A%221%20hop%22%7D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7Bshape%3A%20%22rect%22%2C%20color%3A%22green%22%2C%20text%3A%222%20hops%22%7D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7Bshape%3A%20%22rect%22%2C%20color%3A%22orange%22%2C%20text%3A%223%20hops%22%7D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7Bshape%3A%20%22rect%22%2C%20color%3A%22grey%22%2C%20text%3A%224%20hops%22%7D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7Bshape%3A%20%22rect%22%2C%20color%3A%22red%22%2C%20text%3A%22Failed%20Node%22%7D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7Bshape%3A%20%22rect%22%2C%20color%3A%22white%22%2C%20text%3A%22Unconnected%22%7D%5D%3B%0A%0A%20%20%20%20this.addLegend(svg%2C%20legends%2C%205%2C%2020)%3B%0A%0A%0A%20%20%20%20legends%20%3D%20%5B%7Bshape%3A%20%22circle%22%2C%20text%3A%20%22Mains%20power%22%2C%20color%3A%20%22black%22%7D%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7Bshape%3A%20%22rect%22%2C%20text%3A%20%22Battery%20power%22%2C%20color%3A%20%22black%22%7D%5D%0A%20%20%20%20%0A%20%20%20%20this.addLegend(svg%2C%20legends%2C%20svg.attr(%22width%22)*.75%2C%2020)%0A%0A%0A%0A%20%20%7D%0A%0A%20%20addLegend(svg%2C%20legends%2C%20startX%2C%20startY)%0A%20%20%7B%0A%20%20%20%20for(var%20counter%3D0%3Bcounter%20%3C%20legends.length%3Bcounter%2B%2B)%0A%20%20%20%20%7B%0A%20%20%20%20%20%20if%20(legends%5Bcounter%5D.shape%20%3D%3D%20%22circle%22)%0A%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20svg.append(legends%5Bcounter%5D.shape)%0A%20%20%20%20%20%20%20%20%20%20.attr('cx'%2C%20startX%20%2B%205%20)%0A%20%20%20%20%20%20%20%20%20%20.attr('cy'%2CstartY%20%2B%205%20%2B%2020%20*%20counter)%0A%20%20%20%20%20%20%20%20%20%20.attr('r'%2C%205)%0A%20%20%20%20%20%20%20%20%20%20.style(%22stroke%22%2C%20%22black%22)%0A%20%20%20%20%20%20%20%20%20%20.style(%22fill%22%2C%20legends%5Bcounter%5D.color)%3B%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20else%0A%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20svg.append(legends%5Bcounter%5D.shape)%0A%20%20%20%20%20%20%20%20%20%20.attr('x'%2CstartX)%0A%20%20%20%20%20%20%20%20%20%20.attr('y'%2CstartY%20%2B%2020%20*%20counter)%0A%20%20%20%20%20%20%20%20%20%20.attr('width'%2C%2010)%0A%20%20%20%20%20%20%20%20%20%20.attr('height'%2C%2010)%0A%20%20%20%20%20%20%20%20%20%20.style(%22stroke%22%2C%20%22black%22)%0A%20%20%20%20%20%20%20%20%20%20.style(%22fill%22%2C%20legends%5Bcounter%5D.color)%3B%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20svg.append('text')%0A%20%20%20%20%20%20%20%20.attr(%22x%22%2C%20startX%20%2B%2020)%0A%20%20%20%20%20%20%20%20.attr(%22y%22%2C%20startY%20%2B%2010%20%2B%2020*counter)%0A%20%20%20%20%20%20%20%20.text(legends%5Bcounter%5D.text)%0A%20%20%20%20%20%20%20%20.attr(%22class%22%2C%20%22textselected%22)%0A%20%20%20%20%20%20%20%20.style(%22text-anchor%22%2C%20%22start%22)%0A%20%20%20%20%20%20%20%20.style(%22font-size%22%2C%2015)%3B%0A%0A%20%20%20%20%7D%0A%0A%20%20%7D%0A%0A%20%20listNodes(hass)%20%7B%0A%20%20%20%20let%20states%3Dnew%20Array()%3B%0A%20%20%20%20for%20(let%20state%20in%20hass.states)%0A%20%20%20%20%7B%0A%20%20%20%20%20%20states.push(%7Bname%3Astate%2C%20entity%3Ahass.states%5Bstate%5D%7D)%3B%0A%20%20%20%20%7D%0A%20%20%20%20let%20zwaves%20%3D%20states.filter((s)%20%3D%3E%20%7Breturn%20s.name.indexOf(%22zwave.%22)%20%3D%3D0%7D)%3B%0A%20%20%20%20let%20result%3D%20%7B%22edges%22%3A%5B%5D%2C%20%22nodes%22%3A%5B%5D%7D%3B%0A%0A%20%20%20%20let%20hubNode%3D0%3B%0A%20%20%20%20let%20neighbours%3D%7B%7D%3B%0A%0A%20%20%20%20for%20(let%20b%20in%20zwaves)%0A%20%20%20%20%7B%0A%20%20%20%20%20%20%20let%20id%3Dzwaves%5Bb%5D.entity.attributes%5B%22node_id%22%5D%3B%20%0A%20%20%20%20%20%20%20let%20node%20%3D%20zwaves%5Bb%5D.entity%3B%0A%20%20%20%20%20%20%20if%20(node.attributes%5B%22capabilities%22%5D.filter(%0A%09%09%09(s)%20%3D%3E%20%7Breturn%20s%20%3D%3D%22primaryController%22%7D).length%20%3E%200)%20%0A%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20hubNode%3Did%3B%0A%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20neighbours%5Bid%5D%3Dnode.attributes%5B'neighbors'%5D%3B%0A%0A%20%20%20%20%20%20%20let%20entities%20%3D%20states.filter((s)%20%3D%3E%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20return%20((s.name.indexOf(%22zwave.%22)%20%3D%3D%20-1)%20%26%26%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20(s.entity.attributes%5B%22node_id%22%5D%20%3D%3D%20id))%20%7D)%3B%0A%20%20%20%20%20%20%20let%20batlev%3Dnode.attributes.battery_level%3B%20%20%20%20%20%20%20%0A%20%20%20%20%20%20%20let%20entity%3D%7B%22id%22%3A%20%20id%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22label%22%3A%20(node.attributes%5B%22node_name%22%5D%20%2B%20%22%20(%22%20%2B%20node.attributes%5B%22averageResponseRTT%22%5D%2B%22ms)%22).replace(%2F%20%2Fg%2C%20%22%5Cn%22)%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22class%22%3A%20%22unset%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22shape%22%3A%20batlev%20!%3D%20undefined%20%3F%20%22rect%22%20%3A%20%22circle%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22title%22%3A%20%22%3Cb%3E%22%2Bnode.attributes%5B%22node_name%22%5D%2B%22%3C%2Fb%3E%22%20%2B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22%3Cbr%20%2F%3ENode%3A%20%22%20%2B%20id%20%2B%20(node.attributes%5B%22is_zwave_plus%22%5D%20%3F%20%22%2B%22%20%3A%20%22%22)%20%2B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22%3Cbr%20%2F%3EProduct%20Name%3A%20%22%20%2B%20node.attributes%5B%22product_name%22%5D%20%2B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22%3Cbr%20%2F%3EAverage%20Request%20RTT%3A%20%22%20%2B%20node.attributes%5B%22averageResponseRTT%22%5D%2B%22ms%22%20%2B%20%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22%3Cbr%20%2F%3EPower%20source%3A%20%22%20%2B%20(batlev%20!%3D%20undefined%20%3F%20%22battery%20(%22%20%2B%20batlev%20%2B%22%25)%22%20%3A%20%22mains%22)%20%2B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22%3Cbr%20%2F%3E%22%20%2B%20entities.length%20%2B%20%22%20entities%22%2C%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%22forwards%22%3A%20(node.attributes.is_awake%20%26%26%20node.attributes.is_ready%20%26%26%20!node.attributes.is_failed%20%26%26%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20node.attributes.capabilities.includes(%22listening%22))%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%3B%0A%0A%20%20%20%20%20%20%20if%20(node.attributes%5B%22is_failed%22%5D)%0A%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20entity.label%20%3D%20%22FAILED%3A%20%22%2Bentity.label%3B%0A%20%20%20%20%20%20%20%20%20entity%5B%22font.multi%22%5D%3Dtrue%3B%0A%20%20%20%20%20%20%20%20%20entity%5B%22title%22%5D%3D%22%3Cb%3EFAILED%3A%20%3C%2Fb%3E%22%2Bentity.title%3B%0A%20%20%20%20%20%20%20%20%20entity%5B%22group%22%5D%3D%22Failed%22%3B%0A%20%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%20if%20(hubNode%20%3D%3D%20id)%0A%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20entity.label%3D%22ZWave%20Hub%22%3B%0A%20%20%20%20%20%20%20%20%20entity.borderWidth%3D%202%3B%0A%20%20%20%20%20%20%20%20%20entity.fixed%3Dtrue%3B%0A%20%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%20result.nodes.push(entity)%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20%0A%20%20%20%20if%20(hubNode%20%3E%200)%0A%20%20%20%20%7B%0A%20%20%20%20%20%20let%20layer%3D0%3B%0A%20%20%20%20%20%20let%20previousRow%3D%5BhubNode%5D%3B%0A%20%20%20%20%20%20let%20mappedNodes%3D%5BhubNode%5D%3B%0A%20%20%20%20%20%20while%20(previousRow.length%20%3E%200)%0A%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20layer%20%3D%20layer%2B1%3B%0A%20%20%20%20%20%20%20%20let%20nextRow%3D%5B%5D%3B%0A%20%20%20%20%20%20%20%20for%20(let%20target%20in%20previousRow)%0A%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20result.nodes.filter((n)%20%3D%3E%20%7Breturn%20((n.id%20%3D%3DpreviousRow%5Btarget%5D)%20%26%26%20(n.group%3D%22unset%22))%7D)%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20.every((d)%20%3D%3E%20%7Bd.class%3D%22Layer%22%20%2B%20layer%3B%7D)%0A%0A%20%20%20%20%20%20%20%20%20%20if%20(result.nodes.filter((n)%20%3D%3E%20%7Breturn%20((n.id%20%3D%3D%20previousRow%5Btarget%5D)%20%26%26%20(n.forwards))%7D).length%20%3E%200)%0A%20%20%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20let%20row%3Dneighbours%5BpreviousRow%5Btarget%5D%5D%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20for%20(let%20node%20in%20row)%0A%20%20%20%20%20%20%20%20%20%20%20%20%7B%0A%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20if%20(!mappedNodes.includes(row%5Bnode%5D))%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20result.edges.push(%7B%22from%22%3Arow%5Bnode%5D%2C%20%22to%22%3ApreviousRow%5Btarget%5D%7D)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20nextRow.push(row%5Bnode%5D)%3B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20%20%20for%20(let%20idx%20in%20nextRow)%0A%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20mappedNodes.push(nextRow%5Bidx%5D)%3B%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20previousRow%20%3D%20nextRow%3B%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D%0A%20%20%20%20return%20result%3B%0A%20%20%20%7D%0A%0A%0A%7D%0AcustomElements.define(HaPanelZWave.is%2C%20HaPanelZWave)%3B%0A%0A%2F%2F%23%20sourceURL%3Dhttps%3A%2F%2F[HOSTNAME]2Fapi%2Fpanel_custom%2Fzwavegraph2.js%0A:0:0 Script error.

Okay, I dug into this a little more. I don’t believe it’s an issue with your script, but instead with my NGINX reverse proxy not correctly handling websockets – which is odd since it seems to work in other instances.

I’ll dig into it a bit more and see what I can figure out.

Thanks!