├── .gitignore ├── LICENSE ├── README.md ├── example ├── zwave-map.png └── zwave-map.svg └── zwave-map.php /.gitignore: -------------------------------------------------------------------------------- 1 | GraphViz.php 2 | HASS 3 | OUT 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Magnus Månsson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HomeAssistant-Zwave-ConnectionMap 2 | Draws a map of the Z-wave mesh network using Graphviz. 3 | 4 | This should be compatible with anything based on OpenZWave that has a OZW_Log.txt and a zwcfg_0xfac5e970.xml file I guess, but I haven't tried it on anything else but [Home Assistant](https://home-assistant.io/). 5 | 6 | ![Example graph](https://raw.githubusercontent.com/magma1447/HomeAssistant-Zwave-ConnectionMap/master/example/zwave-map.png) 7 | 8 | ## Installation 9 | Start by installing required packages. The below command is based on Debian Jessie. 10 | `apt-get install php5-cli php-pear graphviz` 11 | 12 | Either fetch GraphViz.php from [GitHub](https://github.com/pear/Image_GraphViz/blob/trunk/Image/GraphViz.php) (tip: press raw) or install it via pear, `pear install Image_GraphViz`. 13 | 14 | In current stable (stretch) the first package would be *php7-cli*. 15 | 16 | Note that it doesn't have to be installed on the same server as your Home Assistant. You can install it somewhere else and just copy the two required files that are needed to generate the connection graph. 17 | 18 | ## Usage 19 | The controller is hard coded as Node 001. If this isn't correct, it can be changed in the source code around line 12. It might make sense to run a z-wave network heal before running this tool. 20 | `php -f zwave-map.php ` 21 | 22 | -------------------------------------------------------------------------------- /example/zwave-map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/magma1447/HomeAssistant-Zwave-ConnectionMap/7ca1bfb6714f060538000e53ee90a069d20e53d5/example/zwave-map.png -------------------------------------------------------------------------------- /example/zwave-map.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | G 11 | 12 | 13 | 1 14 | 15 | Aeotec ZW090 Z-Stick Gen5 EU 16 | (1) 17 | 18 | 19 | 2 20 | 21 | Fibaro Wall Plug Server Room AC 22 | (2) 23 | 24 | 25 | 1->2 26 | 27 | 28 | 29 | 30 | 31 | 3 32 | 33 | Fibaro Wall Plug Server Room Air Extractor 34 | (3) 35 | 36 | 37 | 1->3 38 | 39 | 40 | 41 | 42 | 43 | 4 44 | 45 | IR1 46 | (4) 47 | 48 | 49 | 1->4 50 | 51 | 52 | 53 | 54 | 55 | 5 56 | 57 | IR Living room 58 | (5) 59 | 60 | 61 | 1->5 62 | 63 | 64 | 65 | 66 | 67 | 8 68 | 69 | raingauge 70 | (8) 71 | 72 | 73 | 1->8 74 | 75 | 76 | 77 | 78 | 18 79 | 80 | t3 81 | (18) 82 | 83 | 84 | 1->18 85 | 86 | 87 | 88 | 89 | 90 | 23 91 | 92 | t7 93 | (23) 94 | 95 | 96 | 1->23 97 | 98 | 99 | 100 | 101 | 102 | 2->3 103 | 104 | 105 | 106 | 107 | 108 | 2->4 109 | 110 | 111 | 112 | 113 | 114 | 14 115 | 116 | infraheater 117 | (14) 118 | 119 | 120 | 2->14 121 | 122 | 123 | 124 | 125 | 126 | 3->4 127 | 128 | 129 | 130 | 131 | 132 | 3->5 133 | 134 | 135 | 136 | 137 | 138 | 7 139 | 140 | Torpargrund sensor 141 | (7) 142 | 143 | 144 | 3->7 145 | 146 | 147 | 148 | 149 | 3->8 150 | 151 | 152 | 153 | 154 | 3->14 155 | 156 | 157 | 158 | 159 | 160 | 3->18 161 | 162 | 163 | 164 | 165 | 166 | 4->14 167 | 168 | 169 | 170 | 171 | 172 | 4->18 173 | 174 | 175 | 176 | 177 | 178 | 5->14 179 | 180 | 181 | 182 | 183 | 184 | 15 185 | 186 | hall_multisensor 187 | (15) 188 | 189 | 190 | 5->15 191 | 192 | 193 | 194 | 195 | 196 | 16 197 | 198 | t1 199 | (16) 200 | 201 | 202 | 5->16 203 | 204 | 205 | 206 | 207 | 208 | 17 209 | 210 | t2 211 | (17) 212 | 213 | 214 | 5->17 215 | 216 | 217 | 218 | 219 | 220 | 5->18 221 | 222 | 223 | 224 | 225 | 226 | 19 227 | 228 | t4 229 | (19) 230 | 231 | 232 | 5->19 233 | 234 | 235 | 236 | 237 | 238 | 22 239 | 240 | t6 241 | (22) 242 | 243 | 244 | 5->22 245 | 246 | 247 | 248 | 249 | 250 | 5->23 251 | 252 | 253 | 254 | 255 | 256 | 24 257 | 258 | t8 259 | (24) 260 | 261 | 262 | 5->24 263 | 264 | 265 | 266 | 267 | 268 | 9 269 | 270 | Aeotec ZW130 WallMote Quad 271 | (9) 272 | 273 | 274 | 14->7 275 | 276 | 277 | 278 | 279 | 14->8 280 | 281 | 282 | 283 | 284 | 14->15 285 | 286 | 287 | 288 | 289 | 290 | 14->16 291 | 292 | 293 | 294 | 295 | 296 | 14->17 297 | 298 | 299 | 300 | 301 | 302 | 14->18 303 | 304 | 305 | 306 | 307 | 308 | 14->19 309 | 310 | 311 | 312 | 313 | 314 | 14->22 315 | 316 | 317 | 318 | 319 | 320 | 14->23 321 | 322 | 323 | 324 | 325 | 326 | 14->24 327 | 328 | 329 | 330 | 331 | 332 | 15->7 333 | 334 | 335 | 336 | 337 | 15->16 338 | 339 | 340 | 341 | 342 | 343 | 15->17 344 | 345 | 346 | 347 | 348 | 349 | 15->18 350 | 351 | 352 | 353 | 354 | 355 | 15->24 356 | 357 | 358 | 359 | 360 | 361 | 16->7 362 | 363 | 364 | 365 | 366 | 16->17 367 | 368 | 369 | 370 | 371 | 372 | 16->23 373 | 374 | 375 | 376 | 377 | 378 | 16->24 379 | 380 | 381 | 382 | 383 | 384 | 17->7 385 | 386 | 387 | 388 | 389 | 17->18 390 | 391 | 392 | 393 | 394 | 395 | 17->23 396 | 397 | 398 | 399 | 400 | 401 | 17->24 402 | 403 | 404 | 405 | 406 | 407 | 18->7 408 | 409 | 410 | 411 | 412 | 18->23 413 | 414 | 415 | 416 | 417 | 418 | 18->24 419 | 420 | 421 | 422 | 423 | 424 | 19->7 425 | 426 | 427 | 428 | 429 | 19->8 430 | 431 | 432 | 433 | 434 | 19->23 435 | 436 | 437 | 438 | 439 | 440 | 19->24 441 | 442 | 443 | 444 | 445 | 446 | 21 447 | 448 | t5 449 | (21) 450 | 451 | 452 | 21->22 453 | 454 | 455 | 456 | 457 | 458 | 22->7 459 | 460 | 461 | 462 | 463 | 22->24 464 | 465 | 466 | 467 | 468 | 469 | 23->7 470 | 471 | 472 | 473 | 474 | 23->24 475 | 476 | 477 | 478 | 479 | 480 | 24->7 481 | 482 | 483 | 484 | 485 | 486 | -------------------------------------------------------------------------------- /zwave-map.php: -------------------------------------------------------------------------------- 1 | Node as $v) { 70 | $id = (int) $v['id']; 71 | $name = $v['name']; 72 | $name = reset($name); // No clue why I get an array back 73 | 74 | if(empty($name)) { 75 | $name = "{$v->Manufacturer['name']} {$v->Manufacturer->Product['name']}"; 76 | } 77 | echo "{$id} => {$name}\n"; 78 | 79 | $nodes[$id]['name'] = $name; 80 | } 81 | } 82 | 83 | 84 | 85 | /* 86 | 2017-10-14 12:02:31.336 Info, Node001, Neighbors of this node are: 87 | 2017-10-14 12:02:31.336 Info, Node001, Node 2 88 | 2017-10-14 12:02:31.336 Info, Node001, Node 3 89 | */ 90 | echo "Reading OZW log\n"; 91 | $buf = file_get_contents($ozwLog); 92 | $buf = explode(PHP_EOL, $buf); 93 | foreach($buf as $line) { 94 | 95 | if(preg_match('/.*Info, Node([0-9]{3}), +Neighbors of this node are:$/', $line, $matches) === 1) { 96 | $node = (int) $matches[1]; 97 | 98 | echo $line . PHP_EOL; 99 | 100 | if(!isset($nodes[$node])) { 101 | die("Node {$node} not found in xml\n"); 102 | } 103 | 104 | $nodes[$node]['neighbors'] = array(); 105 | } 106 | else if(preg_match('/.*Info, Node([0-9]{3}), +Node ([0-9]+)$/', $line, $matches) === 1) { 107 | $node = (int) $matches[1]; 108 | $neighbor = (int) $matches[2]; 109 | 110 | echo $line . PHP_EOL; 111 | 112 | if(!isset($nodes[$node])) { 113 | die("Node {$node} not initialized\n"); 114 | } 115 | if(!isset($nodes[$node]['neighbors'])) { 116 | echo "WARNING: {$node} -> {$neighbor} listed before a list header, ignoring\n"; 117 | continue; 118 | } 119 | 120 | if(!in_array($neighbor, $nodes[$node]['neighbors'])) { 121 | $nodes[$node]['neighbors'][] = $neighbor; 122 | } 123 | } 124 | } 125 | 126 | 127 | echo "Calculating hops\n"; 128 | $nodes[$controllerId]['hops'] = 0; // The controller obviously has 0 hops 129 | // Z-wave supports max 4 hops 130 | for($maxHops = 1 ; $maxHops <= 4 ; $maxHops++) { 131 | foreach($nodes as $id => $n) { 132 | if(isset($n['hops'])) { 133 | continue; 134 | } 135 | 136 | if(!isset($n['neighbors'])) { // Should not happen, this is a workaround 137 | echo " WARNING: Node {$id} has no neighbors\n"; 138 | $nodes[$id]['hops'] = 5; 139 | continue; 140 | } 141 | 142 | $hops = FALSE; 143 | foreach($n['neighbors'] as $neighbor) { 144 | if(!isset($nodes[$neighbor]['hops'])) { 145 | continue; 146 | } 147 | if($hops === FALSE || $nodes[$neighbor]['hops']+1 < $hops) { 148 | $hops = $nodes[$neighbor]['hops']+1; 149 | } 150 | } 151 | if($hops !== FALSE && $hops <= $maxHops) { 152 | $nodes[$id]['hops'] = $hops; 153 | echo " {$id} has {$hops} hops to the controller\n"; 154 | } 155 | } 156 | } 157 | 158 | // Set hops to FALSE for nodes without neighbors 159 | foreach($nodes as $id => $n) { 160 | if(isset($nodes[$id]['hops']) && $nodes[$id]['hops'] !== 5) { 161 | continue; 162 | } 163 | $nodes[$id]['hops'] = FALSE; 164 | } 165 | 166 | 167 | 168 | echo "Rendering graph\n"; 169 | $gv = new Image_GraphViz(); 170 | foreach($nodes as $id => $n) { 171 | $attributes = array( 172 | 'label' => "{$n['name']}\n({$id})", 173 | 'color' => GetNodeColor($n['hops']), 174 | ); 175 | if($id === $controllerId) { 176 | $attributes = array_merge($attributes, array( 177 | 'fontcolor' => 'white', 178 | 'fillcolor', 'gray50', 179 | 'color' => 'black', 180 | 'style' => 'bold,filled', 181 | ) 182 | ); 183 | } 184 | $gv->addNode($id, $attributes); 185 | } 186 | 187 | $addedBidirectionals = array(); 188 | foreach($nodes as $id => $n) { 189 | if(empty($n['neighbors'])) { 190 | echo " WARNING: Node {$id} still doesn't have any neighbors (on battery?)\n"; 191 | continue; 192 | } 193 | 194 | 195 | foreach($n['neighbors'] as $neighbor) { 196 | $direction = (!empty($nodes[$neighbor]['neighbors']) && in_array($id, $nodes[$neighbor]['neighbors'])) ? 'both' : 'forward'; 197 | 198 | // If bidirectional, check that it's not already added 199 | if($direction == 'both') { 200 | $n1 = min(array($id, $neighbor)); 201 | $n2 = max(array($id, $neighbor)); 202 | if(isset($addedBidirectionals["{$n1}:{$n2}"])) { 203 | continue; 204 | } 205 | $addedBidirectionals["{$n1}:{$n2}"] = TRUE; 206 | } 207 | 208 | 209 | 210 | $attributes = array('dir' => $direction); 211 | 212 | // Set color depending on number of hops to the controller 213 | if($n['hops'] === FALSE || $nodes[$neighbor]['hops'] === FALSE) { 214 | $attributes['color'] = GetEdgeColor(FALSE); 215 | } else { 216 | $hops = min(array($nodes[$id]['hops'], $nodes[$neighbor]['hops'])); 217 | $attributes['color'] = GetEdgeColor($hops+1); 218 | } 219 | 220 | // Dash connections that aren't the shortest path 221 | // If the difference is 1, it's the shortest path for one of them 222 | if($n['hops'] === FALSE || $nodes[$neighbor]['hops'] === FALSE) { 223 | $hopDiff = FALSE; 224 | } else { 225 | $hopDiff = abs($n['hops'] - $nodes[$neighbor]['hops']); 226 | } 227 | if($hopDiff === 1 && $direction == 'both') { 228 | $attributes['style'] = 'solid'; 229 | } 230 | else if($hopDiff === 1 && $direction == 'forward' && $n['hops'] > $nodes[$neighbor]['hops']) { 231 | $attributes['style'] = 'solid'; 232 | } 233 | else if($hopDiff !== FALSE) { 234 | $attributes['style'] = 'dashed'; 235 | } 236 | else { 237 | $attributes['style'] = 'dotted'; 238 | } 239 | 240 | $gv->addEdge(array($id => $neighbor), $attributes); 241 | 242 | 243 | } 244 | } 245 | $ext = pathinfo($imageFilename, PATHINFO_EXTENSION); 246 | $image = $gv->fetch($ext); 247 | file_put_contents($imageFilename, $image); 248 | echo "Image saved as {$imageFilename}\n"; 249 | 250 | --------------------------------------------------------------------------------