Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Z Wave Graph for Home Assistant
panel_custom:
- name: zwavegraph2
sidebar_title: Z-Wave Graph
sidebar_icon: mdi:access-point-network
url_path: zwave
<!--
https://gist.github.com/AdamNaj/cbf4d792a22f443fe9d354e4dca4de00
Version 1.0:
- based on the brilliant code by @NigelL - with cosmetic changes mostly about clarity and shaping of nodes based on their function
Version 2.0: (02 July 2019)
- you can now pan the graph by dragging it
- you can now zoom the graph with your mouse wheel
- the graph initially is scaled to fill the full screen width
- added minimap to visualize which part of the graph you can see at the oment on the screen
- added 2 more tree layouts (click on the top-legend) - they didn't necessarily help me make the graph more manageable for me, but may be useful to others in their topology
- added the ability to show all node connections if someone wants to see the full picture of their Z-Wave mesh
- fixed the broken new line in the node tooltips
- you can now click on the node to see the entity dialog
Version 2.1: (20 September 2019)
- added Tools to graph legends so you can easily navigate to Z-Wave Network Management
- fixed (hopefully) the problem with the graph requiring page reload then navigating to it
Version 2.2: (04 October 2019)
- ability to turn off node grouping. Having the nodes grouped requires editing locations defined in the zwcfg_*.cfg
Version 2.3: (03 February 2020)
- Graph background reflects theme background color after page reload
- Fixed problem where some removed nodes lingering in the device registry could cause wrong node info card to be displayed after clicking on nodes with higher ids
-->
<dom-module id='ha-panel-zwavegraph2'>
<template>
<style include="ha-style">
.thumb {
border: 1px solid #ddd;
position: absolute;
bottom: 5px;
right: 5px;
margin: 1px;
padding: 1px;
overflow: hidden;
}
#miniSvg {
z-index: 110;
background: white;
}
#scopeContainer {
z-index: 120;
}
.content {
overflow: hidden;
position: absolute;
left: 0px;
top: 64px;
bottom: 0px;
right: 0px;
padding: 8px;
}
svg>.output {
fill: #3598DB;
stroke: #2470A2;
}
.node>rect {
stroke: black;
}
.node.layer-1>rect,
.edgePath.layer-1>path {
fill: #3598DB;
stroke: #2470A2;
}
.node.layer-1>polygon,
.node.layer-1>rect,
.edgePath.layer-1>path {
fill: #3598DB;
stroke: #2470A2;
}
.node.layer-1 text {
fill: #1E5B84;
}
.node.layer-2>polygon,
.node.layer-2>rect,
.edgePath.layer-2>path {
stroke: #1D8548;
}
.node.layer-2>rect,
.edgePath.layer-2>path {
fill: #1BBC9B;
}
.node.layer-2 text {
fill: #11512C;
}
.node.layer-3>polygon,
.node.layer-3>rect,
.edgePath.layer-3>path {
stroke: #1D8548;
}
.node.layer-3>rect,
.edgePath.layer-3>path {
fill: #2DCC70;
}
.node.layer-3 text {
fill: #1D8548;
}
.node.layer-4>polygon,
.node.layer-4>rect,
.edgePath.layer-4>path {
stroke: #D25400;
}
.node.layer-4>rect,
.edgePath.layer-4>path {
fill: #F1C40F;
}
.node.layer-5>polygon,
.node.layer-4 text {
fill: #D25400;
}
.node.layer-5>polygon,
.node.layer-5>rect,
.edgePath.layer-5>path {
stroke: #D25400;
}
.node.layer-5>rect,
.edgePath.layer-5>path {
fill: #E77E23;
}
.node.layer-5 text {
fill: #D25400;
}
.node.Error>polygon,
.node.Error>rect {
fill: #ff7676;
stroke: darkred;
}
.node.Error text {
fill: darkred;
}
.node.unset>rect {
stroke: #666;
}
.node.unset>polygon,
.node.unset>rect {
stroke: #666;
fill: lightgray;
}
.cluster>rect {
stroke: lightgray;
fill: #f8f8f8;
stroke-width: 1px;
stroke-linecap: round;
}
.cluster>.label {
/* stroke: gray; */
fill: lightgray;
}
.node.unset text {
fill: #666;
}
.node text {
font-size: 12px;
}
.edgePath.layer-1>path {
fill: transparent;
}
.edgePath path {
stroke: #333;
fill: #333;
}
.node>polygon {
opacity: 0.7;
}
.node>rect {
stroke-width: 1px;
stroke-linecap: round;
}
</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>Z-Wave Graph</div>
</app-toolbar>
</app-header>
<div class="content" style="background: var(--primary-background-color);">
<svg id="svg" width="100%" height="100%"></svg>
<svg id="scopeContainer" class="thumb">
<g>
<rect id="scope" fill="red" fill-opacity="0.03" stroke="red" stroke-width="1px" stroke-opacity="0.3" x="0"
y="0" width="0" height="0" />
<line id="line1" stroke="red" stroke-width="1px" x1="0" y1="0" x2="0" y2="0" />
<line id="line2" stroke="red" stroke-width="1px" x1="0" y1="0" x2="0" y2="0" />
</g>
</svg>
<svg id="miniSvg" class="thumb" style="background: var(--primary-background-color);"></svg>
</div>
</app-header-layout>
</template>
</dom-module>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/dagre-d3/0.6.1/dagre-d3.js"></script>
<script src="https://d3js.org/topojson.v2.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/svg-pan-zoom@3.5.0/dist/svg-pan-zoom.min.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,
controls: {
type: Object
},
controlsLoaded: {
type: Boolean,
value: false
},
settings: {
type: Boolean,
value: false
},
};
}
ready() {
super.ready();
this.$.svg.innerHTML = ""
var that = this;
setTimeout(function() {
that.paintGraph("network-simplex", "relevant", "z-wave");
}, 100);
}
paintGraph(ranker, edgeVisibility, grouping) {
var legends = [{
shape: "rect",
color: "#3598DB",
stroke: "#2470A2",
textcolor: "#2470A2",
text: "Hub"
},
{
shape: "rect",
color: "#1BBC9B",
stroke: "#1D8548",
textcolor: "#11512C",
text: "1 hop"
},
{
shape: "rect",
color: "#2DCC70",
stroke: "#1D8548",
textcolor: "#1D8548",
text: "2 hops"
},
{
shape: "rect",
color: "#F1C40F",
stroke: "#D25400",
textcolor: "#D25400",
text: "3 hops"
},
{
shape: "rect",
color: "E77E23",
stroke: "#D25400",
textcolor: "#D25400",
text: "4 hops"
},
{
shape: "rect",
color: "crimson",
stroke: "darkred",
textcolor: "darkred",
text: "Failed Node"
},
{
shape: "rect",
color: "lightgray",
stroke: "#666666",
textcolor: "#666666",
text: "Unconnected"
}
];
var layout = [{
shape: "rect",
color: "#3598DB",
stroke: "#2470A2",
textcolor: "#2470A2",
text: "Network Simplex",
ranker: "network-simplex",
cursor: "pointer"
},
{
shape: "rect",
color: "#3598DB",
stroke: "#2470A2",
textcolor: "#2470A2",
text: "Tight Tree",
ranker: "tight-tree",
cursor: "pointer"
},
{
shape: "rect",
color: "#3598DB",
stroke: "#2470A2",
textcolor: "#2470A2",
text: "Longest Path",
ranker: "longest-path",
cursor: "pointer"
}
];
var edgesLegend = [{
shape: "rect",
color: "#3598DB",
stroke: "#2470A2",
textcolor: "#2470A2",
text: "Relevant Neighbors",
edges: "relevant",
cursor: "pointer"
},
{
shape: "rect",
color: "#3598DB",
stroke: "#2470A2",
textcolor: "#2470A2",
text: "All Neighbors",
edges: "all",
cursor: "pointer"
}
];
var groupingLegend = [{
shape: "rect",
color: "#3598DB",
stroke: "#2470A2",
textcolor: "#2470A2",
text: "Z-Wave Locations",
grouping: "z-wave",
cursor: "pointer"
},
{
shape: "rect",
color: "#3598DB",
stroke: "#2470A2",
textcolor: "#2470A2",
text: "Ungrouped",
grouping: "ungrouped",
cursor: "pointer"
}
];
var links = [{
shape: "rect",
color: "#3598DB",
stroke: "#2470A2",
textcolor: "#2470A2",
text: "Network Management",
cursor: "hand",
url: "/config/zwave",
}, ];
this.ranker = ranker;
this.edgeVisibility = edgeVisibility;
this.grouping = grouping;
var data = this.listNodes(this.hass);
var g = new dagreD3.graphlib.Graph({
compound: true
}).setGraph({});
g.graph().rankDir = "BT";
//g.graph().rankDir = 'RL';
g.graph().nodesep = 10;
g.graph().ranker = ranker;
// 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;
// Add our custom shape (a house)
render.shapes().house = this.renderHouse;
render.shapes().battery = this.renderBattery;
var groups = [];
var nodes = data["nodes"];
// Set the parents to define which nodes belong to which cluster
// add nodes to graph
for (var i = 0; i < nodes.length; i++) {
var node = nodes[i];
g.setNode(node.id, node);
if (this.grouping !== "ungrouped" && node.location != "" && node.location != undefined) {
g.setNode(node.location, {
label: node.location,
clusterLabelPos: 'bottom',
class: "group",
entityId: node.entity_id
});
g.setParent(node.id, node.location);
}
}
// add edges to graph
for (var i = 0; i < data["edges"].length; i++) {
var edge = g.setEdge(
data["edges"][i].from,
data["edges"][i].to, {
label: "",
arrowhead: "undirected",
style: data["edges"][i].style,
class: data["edges"][i].class,
curve: d3.curveBundle.beta(0.2)
//curve: d3.curveBasis
})
}
// Run the renderer. This is what draws the final graph.
render(inner, g);
// create battery state gradients
for (let layer = 0; layer < legends.length; layer++) {
for (let percent = 0; percent <= 100; percent += 10) {
var grad = svg.append("defs").append("linearGradient").attr("id", "fill-" + (layer + 1) + "-" + percent)
.attr("x1", "0%").attr("x2", "0%").attr("y1", "0%").attr("y2", "100%");
grad.append("stop").attr("offset", (100 - percent - 10) + "%").style("stop-color", "white");
grad.append("stop").attr("offset", (100 - percent) + "%").style("stop-color", legends[layer].color);
}
}
// 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;
});
inner.selectAll("g.node")
.attr("layer", function (d) {
return g.node(d).layer;
})
.attr("fill", function (d) {
if (g.node(d).battery_level === 100) {
return "url(#fill-" + g.node(d).layer + "-100)";
}
if (g.node(d).battery_level !== undefined) {
return "url(#fill-" + g.node(d).layer + "-" + Math.floor(g.node(d).battery_level / 10 % 10) + "0)";
}
});
inner.selectAll("g.edgePath")
.attr("layer", function (d) {
return g.edges(d).layer;
});
var that = this;
var handleClick = function (d, i, nodeList) { // Add interactivity
var nodeId = nodeList[i].id;
var node = nodes.find(function(element) {
return element.id == nodeId;
});
that.fire('hass-more-info', {
entityId: node.entity_id
});
};
// append handlers
svg.selectAll(".node")
.on("mouseover", this.handleMouseOver)
.on("mouseout", this.handleMouseOut)
.on("click", handleClick);
this.addLegend(this.$, svg, legends, 5, 20, "Node Colors", ranker, this.edgeVisibility, this.grouping);
this.addLegend(this.$, svg, layout, 150, 20, "Tree Layout", ranker, this.edgeVisibility, this.grouping);
this.addLegend(this.$, svg, edgesLegend, 320, 20, "Neighbors", ranker, this.edgeVisibility, this.grouping);
this.addLegend(this.$, svg, groupingLegend, 510, 20, "Grouping", ranker, this.edgeVisibility, this.grouping);
this.addLegend(this.$, svg, links, 700, 20, "Tools", ranker, this.edgeVisibility, this.grouping);
this.$.miniSvg.innerHTML = this.$.svg.innerHTML;
var panZoomGraph = svgPanZoom(this.$.svg);
this.bindThumbnail(this.$);
}
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 && s.entity.attributes["capabilities"] !== undefined
});
let result = {
"edges": [],
"nodes": []
};
let hubNode = 0;
let neighbors = {};
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;
}
neighbors[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;
// create node
let entity = {
"id": id,
"entity_id": node.entity_id,
"label": "[" + id + (node.attributes["is_zwave_plus"] ? "+" : "") + "] " + (node.attributes[
"friendly_name"] + " (" + node.attributes["averageResponseRTT"] + "ms)").replace(/ /g, "\n"),
"class": "unset layer-7",
"layer": 7,
"rx": "6",
"ry": "6",
"neighbors": neighbors[id],
"battery_level": batlev,
"mains": batlev,
"location": node.attributes["location"],
"failed": node.attributes["is_failed"],
"title": "<b>" + node.attributes["node_name"] + "</b>\n" +
"\n Entity ID: " + node.entity_id +
"\n Node: " + id + (node.attributes["is_zwave_plus"] ? "+" : "") +
"\n Product Name: " + node.attributes["product_name"] +
"\n Average Request RTT: " + node.attributes["averageResponseRTT"] + "ms" +
"\n Power source: " + (batlev != undefined ? "battery (" + batlev + "%)" : "mains") +
"\n " + entities.length + " entities" +
"\n Neighbors: " + node.attributes['neighbors'],
"forwards": (node.attributes.is_awake && node.attributes.is_ready && !node.attributes.is_failed &&
node.attributes.capabilities.includes("listening")),
};
entity["shape"] = id === hubNode ? "house" : (entity.forwards || batlev === undefined ? "rect" : "battery");
if (node.attributes["is_failed"]) {
entity.label = "FAILED: " + entity.label;
entity["font.multi"] = true;
entity["title"] = "<b>FAILED: </b>" + entity.title;
entity["group"] = "Failed";
entity["failed"] = true;
entity["class"] = "Error";
}
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];
let layers = [];
while (previousRow.length > 0) {
layer = layer + 1;
let nextRow = [];
let layerMembers = []
layers[layer] = layerMembers;
for (let target in previousRow) {
// assign node to layer
result.nodes.filter((n) => {
return ((n.id == previousRow[target]) && (n.group = "unset"))
})
.every((d) => {
d.class = "layer-" + layer;
d.layer = layer;
if (d.failed) {
d.class = d.class + " Error"
}
if (d.neighbors !== undefined) {
d.neighbors.forEach((n) => {
d.class = d.class + " neighbor-" + n
});
}
})
if (result.nodes.filter((n) => {
return ((n.id == previousRow[target]) && (n.forwards))
}).length > 0) {
let row = neighbors[previousRow[target]];
for (let node in row) {
if (neighbors[row[node]] !== undefined) {
if (!mappedNodes.includes(row[node])) {
layerMembers.push(row[node]);
result.edges.push({
"from": row[node],
"to": previousRow[target],
"style": "",
"class": "layer-" + (layer + 1) + " node-" + row[node] + " node-" + previousRow[target],
"layer": layer,
});
nextRow.push(row[node]);
} else {
// uncomment to show edges regardless of rows - mess!
if (this.edgeVisibility === "all") {
result.edges.push({
"from": row[node],
"to": previousRow[target],
"style": "stroke-dasharray: 5, 5; fill:transparent; ", //"stroke: #ddd; stroke-width: 1px; fill:transparent; stroke-dasharray: 5, 5;",
"class": "layer-" + (layer + 1) + " node-" + row[node] + " node-" + previousRow[target]
});
}
}
}
}
}
}
for (let idx in nextRow) {
mappedNodes.push(nextRow[idx]);
}
previousRow = nextRow;
}
}
return result;
}
// Add our custom shape (a house)
renderHouse(parent, bbox, node) {
var w = bbox.width,
h = bbox.height,
points = [{
x: 0,
y: 0
},
{
x: w,
y: 0
},
{
x: w,
y: -h
},
{
x: w / 2,
y: -h * 3 / 2
},
{
x: 0,
y: -h
}
],
shapeSvg = parent.insert("polygon", ":first-child")
.attr("points", points.map(function (d) {
return d.x + "," + d.y;
}).join(" "))
.attr("transform", "translate(" + (-w / 2) + "," + (h * 3 / 4) + ")");
node.intersect = function (point) {
return dagreD3.intersect.polygon(node, points, point);
};
return shapeSvg;
};
renderBattery(parent, bbox, node) {
var w = bbox.width,
h = bbox.height,
points = [{
x: 0,
y: 0
}, // bottom left
{
x: w,
y: 0
}, // bottom line
{
x: w,
y: -h
}, // right line
{
x: w * 7 / 10,
y: -h
}, // top right
{
x: w * 7 / 10,
y: -h * 20 / 17
}, // battery tip - right
{
x: w * 3 / 10,
y: -h * 20 / 17
}, // battery tip
{
x: w * 3 / 10,
y: -h
}, // battery tip - left
{
x: 0,
y: -h
}, // top left
{
x: 0,
y: -h
} // left line
],
shapeSvg = parent.insert("polygon", ":first-child")
.attr("points", points.map(function (d) {
return d.x + "," + d.y;
}).join(" "))
.attr("transform", "translate(" + (-w / 2) + "," + (h * 2 / 4) + ")");
node.intersect = function (point) {
return dagreD3.intersect.polygon(node, points, point);
};
return shapeSvg;
};
handleMouseOver(d, i, nodeList) { // Add interactivity
var svg;
for (let nodeNum in nodeList) {
let node = nodeList[nodeNum];
if (node.style !== undefined && node.id !== d) {
node.style.opacity = 0.1;
svg = node.ownerSVGElement
}
}
// Use D3 to select element, change color and size
svg.querySelectorAll(".edgePath")
.forEach(function (node) {
node.style.opacity = "0.3"
});
var edges = svg.querySelectorAll(".edgePath.node-" + d);
for (let i = 0; i < edges.length; i++) {
edges[i].style.opacity = "1"
edges[i].style['stroke-width'] = "2";
};
var neighbors = svg.querySelectorAll(".node.neighbor-" + d);
for (let i = 0; i < neighbors.length; i++) {
neighbors[i].style.opacity = "0.7"
};
};
handleMouseOut(d, i, nodeList) { // Add interactivity
var svg;
for (let nodeNum in nodeList) {
let node = nodeList[nodeNum];
if (node.style !== undefined && node.id !== d) {
node.style.opacity = 1;
svg = node.ownerSVGElement
}
}
// Use D3 to select element, change color and size
svg.querySelectorAll(".edgePath")
.forEach(function (node) {
node.style.opacity = "1";
node.style['stroke-width'] = "1";
});
};
addLegend($, svg, legends, startX, startY, title, ranker, edges, grouping) {
var that = this;
var handleClick = function (d, i, nodeList) {
if (nodeList[0].dataset.url !== undefined) {
window.location = nodeList[0].dataset.url;
}
var ranker = nodeList[0].dataset.ranker || that.ranker;
var edges = nodeList[0].dataset.edges || that.edgeVisibility;
var grouping = nodeList[0].dataset.grouping || that.grouping;
svg.selectAll("*").remove();
// Destroy svgpanzoom
svgPanZoom($.svg).destroy();
svgPanZoom($.miniSvg).destroy();
that.paintGraph(ranker, edges, grouping);
}
var shape = svg.append('text')
.attr('x', startX)
.attr('y', startY + 5)
.text(title)
.attr('width', 10)
.attr('height', 10)
.style("font-weight", "800");
for (var counter = 0; counter < legends.length; counter++) {
var isLink = legends[counter].url !== undefined;
if (isLink) {
var text = svg.append('text')
.attr("x", startX)
.attr("y", startY + 10 + 20 * (counter + 1))
.attr("class", "textselected")
.attr('data-url', legends[counter].url)
.text(legends[counter].text)
.style("text-anchor", "start")
.style("fill", legends[counter].textcolor)
.style("font-size", 15)
.style("text-decoration", "underline")
.style("cursor", legends[counter].cursor)
.on("click", handleClick);
} else {
var shape = svg.append(legends[counter].shape)
.attr('x', startX)
.attr('y', startY + 20 * (counter + 1))
.attr('width', 10)
.attr('height', 10)
.style("stroke", legends[counter].stroke)
.style("fill", legends[counter].color)
.style("cursor", legends[counter].cursor);
var text = svg.append('text')
.attr("x", startX + 20)
.attr("y", startY + 10 + 20 * (counter + 1))
.attr("class", "textselected")
.text(legends[counter].text)
.style("text-anchor", "start")
.style("fill", legends[counter].textcolor)
.style("font-size", 15)
.style("cursor", legends[counter].cursor);
var dataLabel, dataValue, dataState;
if (legends[counter].ranker) {
dataLabel = 'data-ranker';
dataValue = legends[counter].ranker;
dataState = ranker;
}
if (legends[counter].edges) {
dataLabel = 'data-edges';
dataValue = legends[counter].edges;
dataState = edges;
}
if (legends[counter].grouping) {
dataLabel = 'data-grouping';
dataValue = legends[counter].grouping;
dataState = grouping;
}
if (dataLabel !== undefined) {
shape.attr(dataLabel, dataValue)
.on("click", handleClick);
text.attr(dataLabel, dataValue)
.on("click", handleClick);
if (dataValue !== dataState) {
shape.style("fill", "transparent");
}
}
}
}
}
bindThumbnail($) {
var beforePanMain = function (oldPan, newPan) {
var stopHorizontal = false,
stopVertical = false,
gutterWidth = 100,
gutterHeight = 100
// Computed variables
,
sizes = this.getSizes(),
leftLimit = -((sizes.viewBox.x + sizes.viewBox.width) * sizes.realZoom) + gutterWidth,
rightLimit = sizes.width - gutterWidth - (sizes.viewBox.x * sizes.realZoom),
topLimit = -((sizes.viewBox.y + sizes.viewBox.height) * sizes.realZoom) + gutterHeight,
bottomLimit = sizes.height - gutterHeight - (sizes.viewBox.y * sizes.realZoom);
customPan = {};
customPan.x = Math.max(leftLimit, Math.min(rightLimit, newPan.x));
customPan.y = Math.max(topLimit, Math.min(bottomLimit, newPan.y));
return customPan;
};
var main = svgPanZoom($.svg, {
zoomEnabled: true,
controlIconsEnabled: true,
fit: true,
center: true,
beforePan: beforePanMain
});
var thumb = svgPanZoom($.miniSvg, {
zoomEnabled: false,
panEnabled: false,
controlIconsEnabled: false,
dblClickZoomEnabled: false,
preventMouseEventsDefault: true,
});
var resizeTimer;
var interval = 300; //msec
window.addEventListener('resize', function (event) {
if (resizeTimer !== false) {
clearTimeout(resizeTimer);
}
resizeTimer = setTimeout(function () {
main.resize();
thumb.resize();
}, interval);
});
main.setOnZoom(function (level) {
thumb.updateThumbScope();
});
main.setOnPan(function (point) {
thumb.updateThumbScope();
});
var _updateThumbScope = function ($, main, thumb, scope, line1, line2) {
var mainPanX = main.getPan().x,
mainPanY = main.getPan().y,
mainWidth = main.getSizes().width,
mainHeight = main.getSizes().height,
mainZoom = main.getSizes().realZoom,
thumbPanX = thumb.getPan().x,
thumbPanY = thumb.getPan().y,
thumbZoom = thumb.getSizes().realZoom;
if (mainZoom === 0) {
return;
}
var thumByMainZoomRatio = thumbZoom / mainZoom;
var scopeX = thumbPanX - mainPanX * thumByMainZoomRatio;
var scopeY = thumbPanY - mainPanY * thumByMainZoomRatio;
var scopeWidth = mainWidth * thumByMainZoomRatio;
var scopeHeight = mainHeight * thumByMainZoomRatio;
$.scope.setAttribute("x", scopeX + 1);
$.scope.setAttribute("y", scopeY + 1);
$.scope.setAttribute("width", scopeWidth - 2);
$.scope.setAttribute("height", scopeHeight - 2);
};
thumb.updateThumbScope = function () {
var scope = $.scope;
var line1 = $.line1;
var line2 = $.line2;
_updateThumbScope($, main, thumb, scope, line1, line2);
}
thumb.updateThumbScope($);
var _updateMainViewPan = function (clientX, clientY, scopeContainer, main, thumb) {
var dim = scopeContainer.getBoundingClientRect(),
mainWidth = main.getSizes().width,
mainHeight = main.getSizes().height,
mainZoom = main.getSizes().realZoom,
thumbWidth = thumb.getSizes().width,
thumbHeight = thumb.getSizes().height,
thumbZoom = thumb.getSizes().realZoom;
var thumbPanX = clientX - dim.left - thumbWidth / 2;
var thumbPanY = clientY - dim.top - thumbHeight / 2;
var mainPanX = -thumbPanX * mainZoom / thumbZoom;
var mainPanY = -thumbPanY * mainZoom / thumbZoom;
main.pan({
x: mainPanX,
y: mainPanY
});
};
var updateMainViewPan = function (evt) {
if (evt.which == 0 && evt.button == 0) {
return false;
}
_updateMainViewPan(evt.clientX, evt.clientY, scopeContainer, main, thumb);
}
var scopeContainer = $.scopeContainer;
scopeContainer.addEventListener('click', function (evt) {
updateMainViewPan(evt);
});
scopeContainer.addEventListener('mousemove', function (evt) {
updateMainViewPan(evt);
});
}
fire(type, detail, options) {
options = options || {};
detail = (detail === null || detail === undefined) ? {} : detail;
const event = new Event(type, {
bubbles: options.bubbles === undefined ? true : options.bubbles,
cancelable: Boolean(options.cancelable),
composed: options.composed === undefined ? true : options.composed
});
event.detail = detail;
const node = options.node || this;
node.dispatchEvent(event);
return event;
}
}
customElements.define(HaPanelZWave.is, HaPanelZWave);
</script>
@jazzyisj

This comment has been minimized.

Copy link

jazzyisj commented Apr 4, 2019

Any chance you can add some sort of versioning comment in the HTML file so one knows if there has been an update? Thank you for this BTW, it's awesome.

@alandtse

This comment has been minimized.

Copy link

alandtse commented Jul 13, 2019

BTW this is one of my favorite panels for HA. I wonder if there's a way to package it for HACS.

@johntdyer

This comment has been minimized.

Copy link

johntdyer commented Aug 5, 2019

+1

@ktownsend-personal

This comment has been minimized.

Copy link

ktownsend-personal commented Aug 11, 2019

I like it, but having some trouble. In the iOS app it only shows the thumbnail view and the rest of the screen is blank. I got that same result in the web browser at first, but now it's showing the map, however clicking a node shows details for a completely different node. Hovering the node appears to give correct info, so it looks like the click handling has a bug.

@ktownsend-personal

This comment has been minimized.

Copy link

ktownsend-personal commented Aug 15, 2019

I noticed the click handling is off by 1 when all nodes are in a single list (before neighbors are evaluated), then when the hierarchy is drawn the click handling is off way more than that. I suspect the indexing of the list doesn't match the indexing of the nodes.

@ualex73

This comment has been minimized.

Copy link

ualex73 commented Aug 24, 2019

I got a similar problem, when I first click on the "Z-Wave Graph" it only show the thumbnail (Safari & Firefox) ... I need to manually refresh the screen to see the map (which is correct) ... is there something I overlooked?

@rjroch

This comment has been minimized.

Copy link

rjroch commented Nov 4, 2019

I set this up on my HassOS/HA 0.101.2 VM, and when I click "Z-Wave Graph" to load, I get a blank white pane (and occasionally a blank white rectangle in the lower right corner of the browser) and the following error in my HA logs:
2019-11-04 11:26:41 ERROR (MainThread) [frontend.js.latest.201910251] 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%2Fhass.io.fqdn%3A8443%2Fapi%2Fpanel_custom%2Fzwavegraph2.js%0A:0:0 Script error.

hass.io.fqdn obfuscates my true HA URL.

@jriker1

This comment has been minimized.

Copy link

jriker1 commented Nov 23, 2019

Installed this into my HA latest revision as of now. I get several of these in the logs. Any thoughts? It does come up:

2019-11-22 20:28:07 ERROR (MainThread) [frontend.js.latest.201910251] https://<HA_URL>:8123/api/panel_custom/zwavegraph2:803:11 Uncaught TypeError: Cannot read property 'querySelectorAll' of undefined

@discordianfish

This comment has been minimized.

Copy link

discordianfish commented Dec 10, 2019

Just added a new z-wave (fibaro dimmer 2) which works and shows up as connected in the network management UI, but shows as grey/unconnected in the z-wave graph view. Any ideas why that might be?

@Tokynet

This comment has been minimized.

Copy link

Tokynet commented Jan 1, 2020

I set this up on my HassOS/HA 0.101.2 VM, and when I click "Z-Wave Graph" to load, I get a blank white pane (and occasionally a blank white rectangle in the lower right corner of the browser) and the following error in my HA logs:
`2019-11-04 11:26:41 ERROR (MainThread) [frontend.js.latest.201910251] data:text/javascript;charset=utf-8,%0Aclass%20HaPanelZWave%20extends%20Polymer.

hass.io.fqdn obfuscates my true HA URL.

@rjroch do you have a proxy'ing service in front of your installation? Try accessing your HA installation directly (from within your home network and to the server's IP) and see if it works that way.

@rjroch

This comment has been minimized.

Copy link

rjroch commented Jan 1, 2020

I believe I tried both. Eventually, without any change on my part, it just started working even with nginx in the mix.

@jhelmink

This comment has been minimized.

Copy link

jhelmink commented Feb 6, 2020

Thanks for this.
Would also suggest adding a note in the HTML comment about where to save the html.
<homeassistant installation directory>/config/panels/zwavegraph2.html

@robinsmidsrod

This comment has been minimized.

Copy link

robinsmidsrod commented Feb 7, 2020

Any chance of getting this into HACS (Home Assistant Community Store)? Would make it so much easier to keep up to date.

@rpitera

This comment has been minimized.

Copy link

rpitera commented Feb 7, 2020

Probably not; HACs doesn't support gists. I just subscribe here at the top and that way anytime someone posts I'm reminded to check versions.

@aneisch

This comment has been minimized.

Copy link

aneisch commented Feb 7, 2020

@ludeeus any idea if a custom-panels category in HACS would be possible? Is that something on the roadmap? Not sure how widely they are used. Custom Panel

@ludeeus

This comment has been minimized.

Copy link

ludeeus commented Feb 7, 2020

Custom panels can easily be packaged as a integration (see 'uilogs' for an example) so will probably not add that as a separate one.

@aneisch

This comment has been minimized.

Copy link

aneisch commented Feb 7, 2020

@AdamNaj interested in making a repo out of this so it can be added to HACS? If not I may go ahead and fork it to do so.

@Tokynet

This comment has been minimized.

Copy link

Tokynet commented Feb 7, 2020

@alandtse

This comment has been minimized.

Copy link

alandtse commented Feb 7, 2020

@AdamNaj you can also just ask @ludeeus to open a repo on custom_components

@Veldkornet

This comment has been minimized.

Copy link

Veldkornet commented Apr 30, 2020

I seem to be getting the below recently every time I open up HomeAssistant (not even clicking on the panel)
2020-04-30 19:07:45 ERROR (MainThread) [frontend.js.latest.202004271] http://hassio.local:8123/hacsfiles/zha-network-card/zha-network-card.js:3:11 Uncaught ReferenceError: transpose is not defined

@stboch

This comment has been minimized.

Copy link

stboch commented Apr 30, 2020

@Veldkornet this doesn't appear to be related to this, looks like it is related to zha-network-card custom-component. Check that you are using the latest version in HACS, if it isn't loading you will probably need to delete it from the www/community directory in your config.

I seem to be getting the below recently every time I open up HomeAssistant (not even clicking on the panel)
2020-04-30 19:07:45 ERROR (MainThread) [frontend.js.latest.202004271] http://hassio.local:8123/hacsfiles/zha-network-card/zha-network-card.js:3:11 Uncaught ReferenceError: transpose is not defined

@Veldkornet

This comment has been minimized.

Copy link

Veldkornet commented Apr 30, 2020

Arg, you’re right, sorry :) It’s obviously been a long day.

@TheDK

This comment has been minimized.

Copy link

TheDK commented May 28, 2020

Screenshot

Hi, I have a question about the screenshot above: As you can see there is a node on my network going through 4 hubs (ZWave maximum), but it is displayed in black, with the text not legible. In the config only 4 hops are defined, so that might be the problem?

@phillipzada

This comment has been minimized.

Copy link

phillipzada commented May 31, 2020

Hi,

N00b question - can anyone assist please. Not sure why there is no color in a default installation - no custom themes.

image

Updated: For anyone who has this issue - its a Edge (Chromium) issue, using chrome works fine.

@Kurisutian

This comment has been minimized.

Copy link

Kurisutian commented Jun 20, 2020

@AdamNaj will the graph still work with the new OZW Stack? Or will there be any version which works with it? Just curious ;)

@flashbacck

This comment has been minimized.

Copy link

flashbacck commented Jun 28, 2020

Thanks for this.
Would also suggest adding a note in the HTML comment about where to save the html.
<homeassistant installation directory>/config/panels/zwavegraph2.html

Thank you for this note. Just quoting it in case any new person misses it

@mkarnebeek

This comment has been minimized.

Copy link

mkarnebeek commented Jul 2, 2020

home-assistant/core#36464 (0.112) seems it deprecated something this is using. I'm using it as

panel_custom:
  - name: zwavegraph2
    sidebar_title: Z-Wave Graph
    sidebar_icon: mdi:access-point-network
    url_path: zwave_graph

Do I need to use it differently?

@stboch

This comment has been minimized.

Copy link

stboch commented Jul 2, 2020

@mkarnebeek
home-assistant just flat out deprecated custom_panels which is what this is.
So this will need a complete rewrite or home-assistant adds back support.

Someone more skilled than me could move it to a custom lovelace card perhaps?

@TheDK

This comment has been minimized.

Copy link

TheDK commented Jul 2, 2020

Man, that sucks! The Graph for ZWave was just great, hopefully there will be another version...

@ludipq

This comment has been minimized.

Copy link

ludipq commented Jul 2, 2020

Still works fine for me after upgrading to 0.112.0

I do get this line in the log though:
WARNING (MainThread) [homeassistant.components.panel_custom] HTML custom panels have been deprecated

My config looks like this:

panel_custom:
  - name: zwavegraph2
    sidebar_title: Z-Wave Graph
    sidebar_icon: mdi:access-point-network
    url_path: zwave
@OverZealous

This comment has been minimized.

Copy link

OverZealous commented Jul 2, 2020

I feel like I'm missing something, because I'm on 0.112 and mine still works. I'm using Hassio on an RPi, and my panel config looks almost exactly like this one from mkarnebeek

@maxxxxpower

This comment has been minimized.

Copy link

maxxxxpower commented Jul 2, 2020

Mine does not work since using the new OpenZWave Beta Integration. However when bringing up the graph in the ZWave discord, I was told that Plus devices do not always follow a static route so the graph would be inaccurate anyway. Above my head, I'm not up on the tech behind Zwave/Plus.

@GirzzlyAK

This comment has been minimized.

Copy link

GirzzlyAK commented Jul 11, 2020

I feel like I'm missing something, because I'm on 0.112 and mine still works. I'm using Hassio on an RPi, and my panel config looks almost exactly like this one from mkarnebeek

I think it's because you're probably using the zwave integration, not the new OZW beta based on v1.6. You'd know if you were on the OZW beta - you'd have to purposely install it. A lot changed and a LOT still doesn't work (I understand). And there is currently no upgrade path from one to the next. Not sure exactly what that means, other than a complete rebuild of our zwave networks using MQTT nomenclature instead of zwave. I'm waiting to see what shakes out.

@TheDK

This comment has been minimized.

Copy link

TheDK commented Jul 11, 2020

Correct - Graph still works with old Zwave integration...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.