├── .gitattributes ├── .gitignore ├── README.md ├── custom_components └── zigbee2mqtt_networkmap │ ├── __init__.py │ ├── manifest.json │ ├── services.yaml │ └── www │ ├── map.html │ ├── panzoom │ └── panzoom.min.js │ └── viz.js │ ├── LICENSE.txt │ ├── full.render.js │ └── viz.js ├── info.md └── map.gif /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask instance folder 57 | instance/ 58 | 59 | # Scrapy stuff: 60 | .scrapy 61 | 62 | # Sphinx documentation 63 | docs/_build/ 64 | 65 | # PyBuilder 66 | target/ 67 | 68 | # IPython Notebook 69 | .ipynb_checkpoints 70 | 71 | # pyenv 72 | .python-version 73 | 74 | # celery beat schedule file 75 | celerybeat-schedule 76 | 77 | # dotenv 78 | .env 79 | 80 | # virtualenv 81 | venv/ 82 | ENV/ 83 | 84 | # Spyder project settings 85 | .spyderproject 86 | 87 | # Rope project settings 88 | .ropeproject 89 | 90 | # ========================= 91 | # Operating System Files 92 | # ========================= 93 | 94 | # OSX 95 | # ========================= 96 | 97 | .DS_Store 98 | .AppleDouble 99 | .LSOverride 100 | 101 | # Thumbnails 102 | ._* 103 | 104 | # Files that might appear in the root of a volume 105 | .DocumentRevisions-V100 106 | .fseventsd 107 | .Spotlight-V100 108 | .TemporaryItems 109 | .Trashes 110 | .VolumeIcon.icns 111 | 112 | # Directories potentially created on remote AFP share 113 | .AppleDB 114 | .AppleDesktop 115 | Network Trash Folder 116 | Temporary Items 117 | .apdisk 118 | 119 | # Windows 120 | # ========================= 121 | 122 | # Windows image file caches 123 | Thumbs.db 124 | ehthumbs.db 125 | 126 | # Folder config file 127 | Desktop.ini 128 | 129 | # Recycle Bin used on file shares 130 | $RECYCLE.BIN/ 131 | 132 | # Windows Installer files 133 | *.cab 134 | *.msi 135 | *.msm 136 | *.msp 137 | 138 | # Windows shortcuts 139 | *.lnk 140 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![maintenance-status](https://img.shields.io/badge/maintenance-deprecated-red.svg) 2 | 3 | The frontend of Zigbee2mqtt offers the possibility to display the map. 4 | 5 | 6 | # ha_zigbee2mqtt_networkmap 7 | Custom Component for Homeassistant to show the [zigbee2mqtt](https://github.com/Koenkk/zigbee2mqtt) Networkmap with [viz.js](https://github.com/mdaines/viz.js/). 8 | 9 | [Forum link with Screenshot](https://community.home-assistant.io/t/zigbee2mqtt-show-the-networkmap-in-hassio/89116) 10 | 11 | ![map|658x499](map.gif) 12 | 13 | **Important:** you have to clear the browsercache after each update 14 | 15 | 16 | **Instructions** 17 | 1. Download or clone [https://github.com/rgruebel/ha_zigbee2mqtt_networkmap](https://github.com/rgruebel/ha_zigbee2mqtt_networkmap) 18 | 2. Copy "custom_components/zigbee2mqtt_networkmap" and content to your "custom_components" folder. 19 | 3. Add the following to your configuration.yaml. It is possible to update the map directly via button. If you want to use this functionality you also have to activate the webhook component. Otherwise you have to use the service "zigbee2mqtt_networkmap.update" 20 | 21 | webhook: 22 | 23 | zigbee2mqtt_networkmap: 24 | #topic: your topic (optional, default zigbee2mqtt) 25 | panel_iframe: 26 | networkmap: 27 | title: 'Zigbee Map' 28 | url: '/local/community/zigbee2mqtt_networkmap/map.html' 29 | icon: 'mdi:graphql' 30 | You can set the graphviz engine via URL Parameter: 31 | map.html?engine=circo (Default: circo, [Supported Engines](https://github.com/mdaines/viz.js/wiki/Supported-Graphviz-Features)) 32 | 33 | 4. Restart Homeassistant 34 | 5. Test if everything is working 35 | 36 | 37 | -------------------------------------------------------------------------------- /custom_components/zigbee2mqtt_networkmap/__init__.py: -------------------------------------------------------------------------------- 1 | import homeassistant.loader as loader 2 | import os 3 | from distutils.dir_util import copy_tree 4 | from datetime import datetime 5 | from aiohttp import web 6 | 7 | # Updated by NG to handle waiting for the MQTT message received. Error checking and more. 8 | # Update Log 9 | # - Error checking 10 | # - Ajax/Timed checking for update mqtt message 11 | # - No need to reload page to get update, as it will come thru the ajax check request 12 | 13 | DOMAIN = 'zigbee2mqtt_networkmap' 14 | 15 | DEPENDENCIES = ['mqtt'] 16 | 17 | CONF_TOPIC = 'topic' 18 | DEFAULT_TOPIC = 'zigbee2mqtt' 19 | 20 | 21 | async def async_setup(hass, config): 22 | fromDirectory = hass.config.path('custom_components', 'zigbee2mqtt_networkmap', 'www') 23 | toDirectory = hass.config.path('www', 'community', 'zigbee2mqtt_networkmap') 24 | 25 | copy_tree(fromDirectory, toDirectory) 26 | 27 | """Set up the zigbee2mqtt_networkmap component.""" 28 | mqtt = hass.components.mqtt 29 | topic = config[DOMAIN].get(CONF_TOPIC, DEFAULT_TOPIC) 30 | entity_id = 'zigbee2mqtt_networkmap.map_last_update' 31 | tmpVar = type('', (), {})() #tmpVar.received_update and update_data 32 | 33 | async def handle_webhook_trigger_update(hass, webhook_id, request): 34 | """Handle trigger update webhook callback.""" 35 | await update_service(None) 36 | return web.json_response({"success": "ok"}) 37 | 38 | async def handle_webhook_check_update(hass, webhook_id, request): 39 | """Handle check update webhook callback.""" 40 | return web.json_response({"success": "ok", "update_received": bool(tmpVar.received_update), "update_received_data": tmpVar.update_data, "last_update": tmpVar.last_update }) 41 | 42 | 43 | # Register the Webhook for trigger update 44 | webhook_trigger_update_id = hass.components.webhook.async_generate_id() 45 | hass.components.webhook.async_register( 46 | DOMAIN, 'zigbee2mqtt_networkmap-webhook_trigger_update', webhook_trigger_update_id, handle_webhook_trigger_update) 47 | 48 | # Register the Webhook for trigger update 49 | webhook_check_update_id = hass.components.webhook.async_generate_id() 50 | hass.components.webhook.async_register( 51 | DOMAIN, 'zigbee2mqtt_networkmap-webhook_check_update', webhook_check_update_id, handle_webhook_check_update) 52 | 53 | f = open(hass.config.path('www', 'community', 'zigbee2mqtt_networkmap', 'settings.js'), "w") 54 | f.write("var webhook_trigger_update_id = '{}';\n".format(webhook_trigger_update_id)) 55 | f.write("var webhook_check_update_id = '{}';".format(webhook_check_update_id)) 56 | f.close() 57 | 58 | 59 | # Listener to be called when we receive a message. 60 | async def message_received(msg): 61 | """Handle new MQTT messages.""" 62 | # Save Response as JS variable in source.js 63 | payload = msg.payload.replace('\n', ' ').replace( 64 | '\r', '').replace("'", r"\'") 65 | last_update = datetime.now() 66 | f = open(hass.config.path( 67 | 'www', 'community', 'zigbee2mqtt_networkmap', 'source.js'), "w") 68 | f.write("var last_update = new Date('" + 69 | last_update.strftime('%Y/%m/%d %H:%M:%S')+"');\nvar graph = \'"+payload+"\'") 70 | f.close() 71 | hass.states.async_set(entity_id, last_update) 72 | tmpVar.received_update = True 73 | tmpVar.update_data = payload 74 | tmpVar.last_update = last_update.strftime('%Y/%m/%d %H:%M:%S') 75 | 76 | # Subscribe our listener to the networkmap topic. 77 | await mqtt.async_subscribe(topic+'/bridge/networkmap/graphviz', message_received) 78 | 79 | # Set the initial state. 80 | hass.states.async_set(entity_id, None) 81 | tmpVar.received_update = False 82 | tmpVar.update_data = None 83 | tmpVar.last_update = None 84 | 85 | # Service to publish a message on MQTT. 86 | async def update_service(call): 87 | """Service to send a message.""" 88 | tmpVar.received_update = False 89 | tmpVar.update_data = None 90 | tmpVar.last_update = None 91 | mqtt.async_publish(topic+'/bridge/networkmap/routes', 'graphviz') 92 | 93 | hass.services.async_register(DOMAIN, 'update', update_service) 94 | return True 95 | -------------------------------------------------------------------------------- /custom_components/zigbee2mqtt_networkmap/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "zigbee2mqtt_networkmap", 3 | "name": "zigbee2mqtt Networkmap", 4 | "version": "0.0.1", 5 | "documentation": "https://github.com/rgruebel/ha_zigbee2mqtt_networkmap", 6 | "dependencies": ["mqtt"], 7 | "codeowners": [ 8 | "@rgruebel" 9 | ], 10 | "requirements": [] 11 | } 12 | -------------------------------------------------------------------------------- /custom_components/zigbee2mqtt_networkmap/services.yaml: -------------------------------------------------------------------------------- 1 | update: 2 | description: Update the Networkmap. 3 | -------------------------------------------------------------------------------- /custom_components/zigbee2mqtt_networkmap/www/map.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 |
42 | 43 |
44 | 57 | 58 | 218 | 219 | 220 | 221 | -------------------------------------------------------------------------------- /custom_components/zigbee2mqtt_networkmap/www/panzoom/panzoom.min.js: -------------------------------------------------------------------------------- 1 | (function (f) { if (typeof exports === "object" && typeof module !== "undefined") { module.exports = f() } else if (typeof define === "function" && define.amd) { define([], f) } else { var g; if (typeof window !== "undefined") { g = window } else if (typeof global !== "undefined") { g = global } else if (typeof self !== "undefined") { g = self } else { g = this } g.panzoom = f() } })(function () { var define, module, exports; return function () { function r(e, n, t) { function o(i, f) { if (!n[i]) { if (!e[i]) { var c = "function" == typeof require && require; if (!f && c) return c(i, !0); if (u) return u(i, !0); var a = new Error("Cannot find module '" + i + "'"); throw a.code = "MODULE_NOT_FOUND", a } var p = n[i] = { exports: {} }; e[i][0].call(p.exports, function (r) { var n = e[i][1][r]; return o(n || r) }, p, p.exports, r, e, n, t) } return n[i].exports } for (var u = "function" == typeof require && require, i = 0; i < t.length; i++)o(t[i]); return o } return r }()({ 1: [function (require, module, exports) { "use strict"; var wheel = require("wheel"); var animate = require("amator"); var eventify = require("ngraph.events"); var kinetic = require("./lib/kinetic.js"); var preventTextSelection = require("./lib/textSelectionInterceptor.js")(); var Transform = require("./lib/transform.js"); var makeSvgController = require("./lib/svgController.js"); var makeDomController = require("./lib/domController.js"); var defaultZoomSpeed = .065; var defaultDoubleTapZoomSpeed = 1.75; var doubleTapSpeedInMS = 300; module.exports = createPanZoom; function createPanZoom(domElement, options) { options = options || {}; var panController = options.controller; if (!panController) { if (domElement instanceof SVGElement) { panController = makeSvgController(domElement) } if (domElement instanceof HTMLElement) { panController = makeDomController(domElement) } } if (!panController) { throw new Error("Cannot create panzoom for the current type of dom element") } var owner = panController.getOwner(); var storedCTMResult = { x: 0, y: 0 }; var isDirty = false; var transform = new Transform; if (panController.initTransform) { panController.initTransform(transform) } var filterKey = typeof options.filterKey === "function" ? options.filterKey : noop; var realPinch = typeof options.realPinch === "boolean" ? options.realPinch : false; var bounds = options.bounds; var maxZoom = typeof options.maxZoom === "number" ? options.maxZoom : Number.POSITIVE_INFINITY; var minZoom = typeof options.minZoom === "number" ? options.minZoom : 0; var boundsPadding = typeof options.boundsPadding === "number" ? options.boundsPadding : .05; var zoomDoubleClickSpeed = typeof options.zoomDoubleClickSpeed === "number" ? options.zoomDoubleClickSpeed : defaultDoubleTapZoomSpeed; var beforeWheel = options.beforeWheel || noop; var speed = typeof options.zoomSpeed === "number" ? options.zoomSpeed : defaultZoomSpeed; validateBounds(bounds); if (options.autocenter) { autocenter() } var frameAnimation; var lastTouchEndTime = 0; var touchInProgress = false; var panstartFired = false; var mouseX; var mouseY; var pinchZoomLength; var smoothScroll; if ("smoothScroll" in options && !options.smoothScroll) { smoothScroll = rigidScroll() } else { smoothScroll = kinetic(getPoint, scroll, options.smoothScroll) } var moveByAnimation; var zoomToAnimation; var multitouch; var paused = false; listenForEvents(); var api = { dispose: dispose, moveBy: internalMoveBy, moveTo: moveTo, centerOn: centerOn, zoomTo: publicZoomTo, zoomAbs: zoomAbs, smoothZoom: smoothZoom, getTransform: getTransformModel, showRectangle: showRectangle, pause: pause, resume: resume, isPaused: isPaused }; eventify(api); return api; function pause() { releaseEvents(); paused = true } function resume() { if (paused) { listenForEvents(); paused = false } } function isPaused() { return paused } function showRectangle(rect) { var clientRect = owner.getBoundingClientRect(); var size = transformToScreen(clientRect.width, clientRect.height); var rectWidth = rect.right - rect.left; var rectHeight = rect.bottom - rect.top; if (!Number.isFinite(rectWidth) || !Number.isFinite(rectHeight)) { throw new Error("Invalid rectangle") } var dw = size.x / rectWidth; var dh = size.y / rectHeight; var scale = Math.min(dw, dh); transform.x = -(rect.left + rectWidth / 2) * scale + size.x / 2; transform.y = -(rect.top + rectHeight / 2) * scale + size.y / 2; transform.scale = scale } function transformToScreen(x, y) { if (panController.getScreenCTM) { var parentCTM = panController.getScreenCTM(); var parentScaleX = parentCTM.a; var parentScaleY = parentCTM.d; var parentOffsetX = parentCTM.e; var parentOffsetY = parentCTM.f; storedCTMResult.x = x * parentScaleX - parentOffsetX; storedCTMResult.y = y * parentScaleY - parentOffsetY } else { storedCTMResult.x = x; storedCTMResult.y = y } return storedCTMResult } function autocenter() { var w; var h; var left = 0; var top = 0; var sceneBoundingBox = getBoundingBox(); if (sceneBoundingBox) { left = sceneBoundingBox.left; top = sceneBoundingBox.top; w = sceneBoundingBox.right - sceneBoundingBox.left; h = sceneBoundingBox.bottom - sceneBoundingBox.top } else { var ownerRect = owner.getBoundingClientRect(); w = ownerRect.width; h = ownerRect.height } var bbox = panController.getBBox(); if (bbox.width === 0 || bbox.height === 0) { return } var dh = h / bbox.height; var dw = w / bbox.width; var scale = Math.min(dw, dh); transform.x = -(bbox.left + bbox.width / 2) * scale + w / 2 + left; transform.y = -(bbox.top + bbox.height / 2) * scale + h / 2 + top; transform.scale = scale } function getTransformModel() { return transform } function getPoint() { return { x: transform.x, y: transform.y } } function moveTo(x, y) { transform.x = x; transform.y = y; keepTransformInsideBounds(); triggerEvent("pan"); makeDirty() } function moveBy(dx, dy) { moveTo(transform.x + dx, transform.y + dy) } function keepTransformInsideBounds() { var boundingBox = getBoundingBox(); if (!boundingBox) return; var adjusted = false; var clientRect = getClientRect(); var diff = boundingBox.left - clientRect.right; if (diff > 0) { transform.x += diff; adjusted = true } diff = boundingBox.right - clientRect.left; if (diff < 0) { transform.x += diff; adjusted = true } diff = boundingBox.top - clientRect.bottom; if (diff > 0) { transform.y += diff; adjusted = true } diff = boundingBox.bottom - clientRect.top; if (diff < 0) { transform.y += diff; adjusted = true } return adjusted } function getBoundingBox() { if (!bounds) return; if (typeof bounds === "boolean") { var ownerRect = owner.getBoundingClientRect(); var sceneWidth = ownerRect.width; var sceneHeight = ownerRect.height; return { left: sceneWidth * boundsPadding, top: sceneHeight * boundsPadding, right: sceneWidth * (1 - boundsPadding), bottom: sceneHeight * (1 - boundsPadding) } } return bounds } function getClientRect() { var bbox = panController.getBBox(); var leftTop = client(bbox.left, bbox.top); return { left: leftTop.x, top: leftTop.y, right: bbox.width * transform.scale + leftTop.x, bottom: bbox.height * transform.scale + leftTop.y } } function client(x, y) { return { x: x * transform.scale + transform.x, y: y * transform.scale + transform.y } } function makeDirty() { isDirty = true; frameAnimation = window.requestAnimationFrame(frame) } function zoomByRatio(clientX, clientY, ratio) { if (isNaN(clientX) || isNaN(clientY) || isNaN(ratio)) { throw new Error("zoom requires valid numbers") } var newScale = transform.scale * ratio; if (newScale < minZoom) { if (transform.scale === minZoom) return; ratio = minZoom / transform.scale } if (newScale > maxZoom) { if (transform.scale === maxZoom) return; ratio = maxZoom / transform.scale } var size = transformToScreen(clientX, clientY); transform.x = size.x - ratio * (size.x - transform.x); transform.y = size.y - ratio * (size.y - transform.y); var transformAdjusted = keepTransformInsideBounds(); if (!transformAdjusted) transform.scale *= ratio; triggerEvent("zoom"); makeDirty() } function zoomAbs(clientX, clientY, zoomLevel) { var ratio = zoomLevel / transform.scale; zoomByRatio(clientX, clientY, ratio) } function centerOn(ui) { var parent = ui.ownerSVGElement; if (!parent) throw new Error("ui element is required to be within the scene"); var clientRect = ui.getBoundingClientRect(); var cx = clientRect.left + clientRect.width / 2; var cy = clientRect.top + clientRect.height / 2; var container = parent.getBoundingClientRect(); var dx = container.width / 2 - cx; var dy = container.height / 2 - cy; internalMoveBy(dx, dy, true) } function internalMoveBy(dx, dy, smooth) { if (!smooth) { return moveBy(dx, dy) } if (moveByAnimation) moveByAnimation.cancel(); var from = { x: 0, y: 0 }; var to = { x: dx, y: dy }; var lastX = 0; var lastY = 0; moveByAnimation = animate(from, to, { step: function (v) { moveBy(v.x - lastX, v.y - lastY); lastX = v.x; lastY = v.y } }) } function scroll(x, y) { cancelZoomAnimation(); moveTo(x, y) } function dispose() { releaseEvents() } function listenForEvents() { owner.addEventListener("mousedown", onMouseDown); owner.addEventListener("dblclick", onDoubleClick); owner.addEventListener("touchstart", onTouch); owner.addEventListener("keydown", onKeyDown); wheel.addWheelListener(owner, onMouseWheel); makeDirty() } function releaseEvents() { wheel.removeWheelListener(owner, onMouseWheel); owner.removeEventListener("mousedown", onMouseDown); owner.removeEventListener("keydown", onKeyDown); owner.removeEventListener("dblclick", onDoubleClick); owner.removeEventListener("touchstart", onTouch); if (frameAnimation) { window.cancelAnimationFrame(frameAnimation); frameAnimation = 0 } smoothScroll.cancel(); releaseDocumentMouse(); releaseTouches(); triggerPanEnd() } function frame() { if (isDirty) applyTransform() } function applyTransform() { isDirty = false; panController.applyTransform(transform); triggerEvent("transform"); frameAnimation = 0 } function onKeyDown(e) { var x = 0, y = 0, z = 0; if (e.keyCode === 38) { y = 1 } else if (e.keyCode === 40) { y = -1 } else if (e.keyCode === 37) { x = 1 } else if (e.keyCode === 39) { x = -1 } else if (e.keyCode === 189 || e.keyCode === 109) { z = 1 } else if (e.keyCode === 187 || e.keyCode === 107) { z = -1 } if (filterKey(e, x, y, z)) { return } if (x || y) { e.preventDefault(); e.stopPropagation(); var clientRect = owner.getBoundingClientRect(); var offset = Math.min(clientRect.width, clientRect.height); var moveSpeedRatio = .05; var dx = offset * moveSpeedRatio * x; var dy = offset * moveSpeedRatio * y; internalMoveBy(dx, dy) } if (z) { var scaleMultiplier = getScaleMultiplier(z); var ownerRect = owner.getBoundingClientRect(); publicZoomTo(ownerRect.width / 2, ownerRect.height / 2, scaleMultiplier) } } function onTouch(e) { beforeTouch(e); if (e.touches.length === 1) { return handleSingleFingerTouch(e, e.touches[0]) } else if (e.touches.length === 2) { pinchZoomLength = getPinchZoomLength(e.touches[0], e.touches[1]); multitouch = true; startTouchListenerIfNeeded() } } function beforeTouch(e) { if (options.onTouch && !options.onTouch(e)) { return } e.stopPropagation(); e.preventDefault() } function beforeDoubleClick(e) { if (options.onDoubleClick && !options.onDoubleClick(e)) { return } e.preventDefault(); e.stopPropagation() } function handleSingleFingerTouch(e) { var touch = e.touches[0]; var offset = getOffsetXY(touch); mouseX = offset.x; mouseY = offset.y; smoothScroll.cancel(); startTouchListenerIfNeeded() } function startTouchListenerIfNeeded() { if (!touchInProgress) { touchInProgress = true; document.addEventListener("touchmove", handleTouchMove); document.addEventListener("touchend", handleTouchEnd); document.addEventListener("touchcancel", handleTouchEnd) } } function handleTouchMove(e) { if (e.touches.length === 1) { e.stopPropagation(); var touch = e.touches[0]; var offset = getOffsetXY(touch); var dx = offset.x - mouseX; var dy = offset.y - mouseY; if (dx !== 0 && dy !== 0) { triggerPanStart() } mouseX = offset.x; mouseY = offset.y; var point = transformToScreen(dx, dy); internalMoveBy(point.x, point.y) } else if (e.touches.length === 2) { multitouch = true; var t1 = e.touches[0]; var t2 = e.touches[1]; var currentPinchLength = getPinchZoomLength(t1, t2); var scaleMultiplier = 1; if (realPinch) { scaleMultiplier = currentPinchLength / pinchZoomLength } else { var delta = 0; if (currentPinchLength < pinchZoomLength) { delta = 1 } else if (currentPinchLength > pinchZoomLength) { delta = -1 } scaleMultiplier = getScaleMultiplier(delta) } mouseX = (t1.clientX + t2.clientX) / 2; mouseY = (t1.clientY + t2.clientY) / 2; publicZoomTo(mouseX, mouseY, scaleMultiplier); pinchZoomLength = currentPinchLength; e.stopPropagation(); e.preventDefault() } } function handleTouchEnd(e) { if (e.touches.length > 0) { var offset = getOffsetXY(e.touches[0]); mouseX = offset.x; mouseY = offset.y } else { var now = new Date; if (now - lastTouchEndTime < doubleTapSpeedInMS) { smoothZoom(mouseX, mouseY, zoomDoubleClickSpeed) } lastTouchEndTime = now; touchInProgress = false; triggerPanEnd(); releaseTouches() } } function getPinchZoomLength(finger1, finger2) { return Math.sqrt((finger1.clientX - finger2.clientX) * (finger1.clientX - finger2.clientX) + (finger1.clientY - finger2.clientY) * (finger1.clientY - finger2.clientY)) } function onDoubleClick(e) { beforeDoubleClick(e); var offset = getOffsetXY(e); smoothZoom(offset.x, offset.y, zoomDoubleClickSpeed) } function onMouseDown(e) { if (touchInProgress) { e.stopPropagation(); return false } var isLeftButton = e.button === 1 && window.event !== null || e.button === 0; if (!isLeftButton) return; smoothScroll.cancel(); var offset = getOffsetXY(e); var point = transformToScreen(offset.x, offset.y); mouseX = point.x; mouseY = point.y; document.addEventListener("mousemove", onMouseMove); document.addEventListener("mouseup", onMouseUp); preventTextSelection.capture(e.target || e.srcElement); return false } function onMouseMove(e) { if (touchInProgress) return; triggerPanStart(); var offset = getOffsetXY(e); var point = transformToScreen(offset.x, offset.y); var dx = point.x - mouseX; var dy = point.y - mouseY; mouseX = point.x; mouseY = point.y; internalMoveBy(dx, dy) } function onMouseUp() { preventTextSelection.release(); triggerPanEnd(); releaseDocumentMouse() } function releaseDocumentMouse() { document.removeEventListener("mousemove", onMouseMove); document.removeEventListener("mouseup", onMouseUp); panstartFired = false } function releaseTouches() { document.removeEventListener("touchmove", handleTouchMove); document.removeEventListener("touchend", handleTouchEnd); document.removeEventListener("touchcancel", handleTouchEnd); panstartFired = false; multitouch = false } function onMouseWheel(e) { if (beforeWheel(e)) return; smoothScroll.cancel(); var scaleMultiplier = getScaleMultiplier(e.deltaY); if (scaleMultiplier !== 1) { var offset = getOffsetXY(e); publicZoomTo(offset.x, offset.y, scaleMultiplier); e.preventDefault() } } function getOffsetXY(e) { var offsetX, offsetY; var ownerRect = owner.getBoundingClientRect(); offsetX = e.clientX - ownerRect.left; offsetY = e.clientY - ownerRect.top; return { x: offsetX, y: offsetY } } function smoothZoom(clientX, clientY, scaleMultiplier) { var fromValue = transform.scale; var from = { scale: fromValue }; var to = { scale: scaleMultiplier * fromValue }; smoothScroll.cancel(); cancelZoomAnimation(); zoomToAnimation = animate(from, to, { step: function (v) { zoomAbs(clientX, clientY, v.scale) } }) } function publicZoomTo(clientX, clientY, scaleMultiplier) { smoothScroll.cancel(); cancelZoomAnimation(); return zoomByRatio(clientX, clientY, scaleMultiplier) } function cancelZoomAnimation() { if (zoomToAnimation) { zoomToAnimation.cancel(); zoomToAnimation = null } } function getScaleMultiplier(delta) { var scaleMultiplier = 1; if (delta > 0) { scaleMultiplier = 1 - speed } else if (delta < 0) { scaleMultiplier = 1 + speed } return scaleMultiplier } function triggerPanStart() { if (!panstartFired) { triggerEvent("panstart"); panstartFired = true; smoothScroll.start() } } function triggerPanEnd() { if (panstartFired) { if (!multitouch) smoothScroll.stop(); triggerEvent("panend") } } function triggerEvent(name) { api.fire(name, api) } } function noop() { } function validateBounds(bounds) { var boundsType = typeof bounds; if (boundsType === "undefined" || boundsType === "boolean") return; var validBounds = isNumber(bounds.left) && isNumber(bounds.top) && isNumber(bounds.bottom) && isNumber(bounds.right); if (!validBounds) throw new Error("Bounds object is not valid. It can be: " + "undefined, boolean (true|false) or an object {left, top, right, bottom}") } function isNumber(x) { return Number.isFinite(x) } function isNaN(value) { if (Number.isNaN) { return Number.isNaN(value) } return value !== value } function rigidScroll() { return { start: noop, stop: noop, cancel: noop } } function autoRun() { if (typeof document === "undefined") return; var scripts = document.getElementsByTagName("script"); if (!scripts) return; var panzoomScript; Array.from(scripts).forEach(function (x) { if (x.src && x.src.match(/\bpanzoom(\.min)?\.js/)) { panzoomScript = x } }); if (!panzoomScript) return; var query = panzoomScript.getAttribute("query"); if (!query) return; var globalName = panzoomScript.getAttribute("name") || "pz"; var started = Date.now(); tryAttach(); function tryAttach() { var el = document.querySelector(query); if (!el) { var now = Date.now(); var elapsed = now - started; if (elapsed < 2e3) { setTimeout(tryAttach, 100); return } console.error("Cannot find the panzoom element", globalName); return } var options = collectOptions(panzoomScript); console.log(options); window[globalName] = createPanZoom(el, options) } function collectOptions(script) { var attrs = script.attributes; var options = {}; for (var i = 0; i < attrs.length; ++i) { var attr = attrs[i]; var nameValue = getPanzoomAttributeNameValue(attr); if (nameValue) { options[nameValue.name] = nameValue.value } } return options } function getPanzoomAttributeNameValue(attr) { if (!attr.name) return; var isPanZoomAttribute = attr.name[0] === "p" && attr.name[1] === "z" && attr.name[2] === "-"; if (!isPanZoomAttribute) return; var name = attr.name.substr(3); var value = JSON.parse(attr.value); return { name: name, value: value } } } autoRun() }, { "./lib/domController.js": 2, "./lib/kinetic.js": 3, "./lib/svgController.js": 4, "./lib/textSelectionInterceptor.js": 5, "./lib/transform.js": 6, amator: 7, "ngraph.events": 9, wheel: 10 }], 2: [function (require, module, exports) { module.exports = makeDomController; function makeDomController(domElement) { var elementValid = domElement instanceof HTMLElement; if (!elementValid) { throw new Error("svg element is required for svg.panzoom to work") } var owner = domElement.parentElement; if (!owner) { throw new Error("Do not apply panzoom to the detached DOM element. ") } domElement.scrollTop = 0; owner.setAttribute("tabindex", 1); var api = { getBBox: getBBox, getOwner: getOwner, applyTransform: applyTransform }; return api; function getOwner() { return owner } function getBBox() { return { left: 0, top: 0, width: domElement.clientWidth, height: domElement.clientHeight } } function applyTransform(transform) { domElement.style.transformOrigin = "0 0 0"; domElement.style.transform = "matrix(" + transform.scale + ", 0, 0, " + transform.scale + ", " + transform.x + ", " + transform.y + ")" } } }, {}], 3: [function (require, module, exports) { module.exports = kinetic; function kinetic(getPoint, scroll, settings) { if (typeof settings !== "object") { settings = {} } var minVelocity = typeof settings.minVelocity === "number" ? settings.minVelocity : 5; var amplitude = typeof settings.amplitude === "number" ? settings.amplitude : .25; var lastPoint; var timestamp; var timeConstant = 342; var ticker; var vx, targetX, ax; var vy, targetY, ay; var raf; return { start: start, stop: stop, cancel: dispose }; function dispose() { window.clearInterval(ticker); window.cancelAnimationFrame(raf) } function start() { lastPoint = getPoint(); ax = ay = vx = vy = 0; timestamp = new Date; window.clearInterval(ticker); window.cancelAnimationFrame(raf); ticker = window.setInterval(track, 100) } function track() { var now = Date.now(); var elapsed = now - timestamp; timestamp = now; var currentPoint = getPoint(); var dx = currentPoint.x - lastPoint.x; var dy = currentPoint.y - lastPoint.y; lastPoint = currentPoint; var dt = 1e3 / (1 + elapsed); vx = .8 * dx * dt + .2 * vx; vy = .8 * dy * dt + .2 * vy } function stop() { window.clearInterval(ticker); window.cancelAnimationFrame(raf); var currentPoint = getPoint(); targetX = currentPoint.x; targetY = currentPoint.y; timestamp = Date.now(); if (vx < -minVelocity || vx > minVelocity) { ax = amplitude * vx; targetX += ax } if (vy < -minVelocity || vy > minVelocity) { ay = amplitude * vy; targetY += ay } raf = window.requestAnimationFrame(autoScroll) } function autoScroll() { var elapsed = Date.now() - timestamp; var moving = false; var dx = 0; var dy = 0; if (ax) { dx = -ax * Math.exp(-elapsed / timeConstant); if (dx > .5 || dx < -.5) moving = true; else dx = ax = 0 } if (ay) { dy = -ay * Math.exp(-elapsed / timeConstant); if (dy > .5 || dy < -.5) moving = true; else dy = ay = 0 } if (moving) { scroll(targetX + dx, targetY + dy); raf = window.requestAnimationFrame(autoScroll) } } } }, {}], 4: [function (require, module, exports) { module.exports = makeSvgController; function makeSvgController(svgElement) { var elementValid = svgElement instanceof SVGElement; if (!elementValid) { throw new Error("svg element is required for svg.panzoom to work") } var owner = svgElement.ownerSVGElement; if (!owner) { throw new Error("Do not apply panzoom to the root element. " + "Use its child instead (e.g. ). " + "As of March 2016 only FireFox supported transform on the root element") } owner.setAttribute("tabindex", 1); var api = { getBBox: getBBox, getScreenCTM: getScreenCTM, getOwner: getOwner, applyTransform: applyTransform, initTransform: initTransform }; return api; function getOwner() { return owner } function getBBox() { var bbox = svgElement.getBBox(); return { left: bbox.x, top: bbox.y, width: bbox.width, height: bbox.height } } function getScreenCTM() { return owner.getScreenCTM() } function initTransform(transform) { var screenCTM = svgElement.getScreenCTM(); transform.x = screenCTM.e; transform.y = screenCTM.f; transform.scale = screenCTM.a; owner.removeAttributeNS(null, "viewBox") } function applyTransform(transform) { svgElement.setAttribute("transform", "matrix(" + transform.scale + " 0 0 " + transform.scale + " " + transform.x + " " + transform.y + ")") } } }, {}], 5: [function (require, module, exports) { module.exports = createTextSelectionInterceptor; function createTextSelectionInterceptor() { var dragObject; var prevSelectStart; var prevDragStart; return { capture: capture, release: release }; function capture(domObject) { prevSelectStart = window.document.onselectstart; prevDragStart = window.document.ondragstart; window.document.onselectstart = disabled; dragObject = domObject; dragObject.ondragstart = disabled } function release() { window.document.onselectstart = prevSelectStart; if (dragObject) dragObject.ondragstart = prevDragStart } } function disabled(e) { e.stopPropagation(); return false } }, {}], 6: [function (require, module, exports) { module.exports = Transform; function Transform() { this.x = 0; this.y = 0; this.scale = 1 } }, {}], 7: [function (require, module, exports) { var BezierEasing = require("bezier-easing"); var animations = { ease: BezierEasing(.25, .1, .25, 1), easeIn: BezierEasing(.42, 0, 1, 1), easeOut: BezierEasing(0, 0, .58, 1), easeInOut: BezierEasing(.42, 0, .58, 1), linear: BezierEasing(0, 0, 1, 1) }; module.exports = animate; module.exports.makeAggregateRaf = makeAggregateRaf; module.exports.sharedScheduler = makeAggregateRaf(); function animate(source, target, options) { var start = Object.create(null); var diff = Object.create(null); options = options || {}; var easing = typeof options.easing === "function" ? options.easing : animations[options.easing]; if (!easing) { if (options.easing) { console.warn("Unknown easing function in amator: " + options.easing) } easing = animations.ease } var step = typeof options.step === "function" ? options.step : noop; var done = typeof options.done === "function" ? options.done : noop; var scheduler = getScheduler(options.scheduler); var keys = Object.keys(target); keys.forEach(function (key) { start[key] = source[key]; diff[key] = target[key] - source[key] }); var durationInMs = typeof options.duration === "number" ? options.duration : 400; var durationInFrames = Math.max(1, durationInMs * .06); var previousAnimationId; var frame = 0; previousAnimationId = scheduler.next(loop); return { cancel: cancel }; function cancel() { scheduler.cancel(previousAnimationId); previousAnimationId = 0 } function loop() { var t = easing(frame / durationInFrames); frame += 1; setValues(t); if (frame <= durationInFrames) { previousAnimationId = scheduler.next(loop); step(source) } else { previousAnimationId = 0; setTimeout(function () { done(source) }, 0) } } function setValues(t) { keys.forEach(function (key) { source[key] = diff[key] * t + start[key] }) } } function noop() { } function getScheduler(scheduler) { if (!scheduler) { var canRaf = typeof window !== "undefined" && window.requestAnimationFrame; return canRaf ? rafScheduler() : timeoutScheduler() } if (typeof scheduler.next !== "function") throw new Error("Scheduler is supposed to have next(cb) function"); if (typeof scheduler.cancel !== "function") throw new Error("Scheduler is supposed to have cancel(handle) function"); return scheduler } function rafScheduler() { return { next: window.requestAnimationFrame.bind(window), cancel: window.cancelAnimationFrame.bind(window) } } function timeoutScheduler() { return { next: function (cb) { return setTimeout(cb, 1e3 / 60) }, cancel: function (id) { return clearTimeout(id) } } } function makeAggregateRaf() { var frontBuffer = new Set; var backBuffer = new Set; var frameToken = 0; return { next: next, cancel: next, clearAll: clearAll }; function clearAll() { frontBuffer.clear(); backBuffer.clear(); cancelAnimationFrame(frameToken); frameToken = 0 } function next(callback) { backBuffer.add(callback); renderNextFrame() } function renderNextFrame() { if (!frameToken) frameToken = requestAnimationFrame(renderFrame) } function renderFrame() { frameToken = 0; var t = backBuffer; backBuffer = frontBuffer; frontBuffer = t; frontBuffer.forEach(function (callback) { callback() }); frontBuffer.clear() } function cancel(callback) { backBuffer.delete(callback) } } }, { "bezier-easing": 8 }], 8: [function (require, module, exports) { var NEWTON_ITERATIONS = 4; var NEWTON_MIN_SLOPE = .001; var SUBDIVISION_PRECISION = 1e-7; var SUBDIVISION_MAX_ITERATIONS = 10; var kSplineTableSize = 11; var kSampleStepSize = 1 / (kSplineTableSize - 1); var float32ArraySupported = typeof Float32Array === "function"; function A(aA1, aA2) { return 1 - 3 * aA2 + 3 * aA1 } function B(aA1, aA2) { return 3 * aA2 - 6 * aA1 } function C(aA1) { return 3 * aA1 } function calcBezier(aT, aA1, aA2) { return ((A(aA1, aA2) * aT + B(aA1, aA2)) * aT + C(aA1)) * aT } function getSlope(aT, aA1, aA2) { return 3 * A(aA1, aA2) * aT * aT + 2 * B(aA1, aA2) * aT + C(aA1) } function binarySubdivide(aX, aA, aB, mX1, mX2) { var currentX, currentT, i = 0; do { currentT = aA + (aB - aA) / 2; currentX = calcBezier(currentT, mX1, mX2) - aX; if (currentX > 0) { aB = currentT } else { aA = currentT } } while (Math.abs(currentX) > SUBDIVISION_PRECISION && ++i < SUBDIVISION_MAX_ITERATIONS); return currentT } function newtonRaphsonIterate(aX, aGuessT, mX1, mX2) { for (var i = 0; i < NEWTON_ITERATIONS; ++i) { var currentSlope = getSlope(aGuessT, mX1, mX2); if (currentSlope === 0) { return aGuessT } var currentX = calcBezier(aGuessT, mX1, mX2) - aX; aGuessT -= currentX / currentSlope } return aGuessT } function LinearEasing(x) { return x } module.exports = function bezier(mX1, mY1, mX2, mY2) { if (!(0 <= mX1 && mX1 <= 1 && 0 <= mX2 && mX2 <= 1)) { throw new Error("bezier x values must be in [0, 1] range") } if (mX1 === mY1 && mX2 === mY2) { return LinearEasing } var sampleValues = float32ArraySupported ? new Float32Array(kSplineTableSize) : new Array(kSplineTableSize); for (var i = 0; i < kSplineTableSize; ++i) { sampleValues[i] = calcBezier(i * kSampleStepSize, mX1, mX2) } function getTForX(aX) { var intervalStart = 0; var currentSample = 1; var lastSample = kSplineTableSize - 1; for (; currentSample !== lastSample && sampleValues[currentSample] <= aX; ++currentSample) { intervalStart += kSampleStepSize } --currentSample; var dist = (aX - sampleValues[currentSample]) / (sampleValues[currentSample + 1] - sampleValues[currentSample]); var guessForT = intervalStart + dist * kSampleStepSize; var initialSlope = getSlope(guessForT, mX1, mX2); if (initialSlope >= NEWTON_MIN_SLOPE) { return newtonRaphsonIterate(aX, guessForT, mX1, mX2) } else if (initialSlope === 0) { return guessForT } else { return binarySubdivide(aX, intervalStart, intervalStart + kSampleStepSize, mX1, mX2) } } return function BezierEasing(x) { if (x === 0) { return 0 } if (x === 1) { return 1 } return calcBezier(getTForX(x), mY1, mY2) } } }, {}], 9: [function (require, module, exports) { module.exports = function (subject) { validateSubject(subject); var eventsStorage = createEventsStorage(subject); subject.on = eventsStorage.on; subject.off = eventsStorage.off; subject.fire = eventsStorage.fire; return subject }; function createEventsStorage(subject) { var registeredEvents = Object.create(null); return { on: function (eventName, callback, ctx) { if (typeof callback !== "function") { throw new Error("callback is expected to be a function") } var handlers = registeredEvents[eventName]; if (!handlers) { handlers = registeredEvents[eventName] = [] } handlers.push({ callback: callback, ctx: ctx }); return subject }, off: function (eventName, callback) { var wantToRemoveAll = typeof eventName === "undefined"; if (wantToRemoveAll) { registeredEvents = Object.create(null); return subject } if (registeredEvents[eventName]) { var deleteAllCallbacksForEvent = typeof callback !== "function"; if (deleteAllCallbacksForEvent) { delete registeredEvents[eventName] } else { var callbacks = registeredEvents[eventName]; for (var i = 0; i < callbacks.length; ++i) { if (callbacks[i].callback === callback) { callbacks.splice(i, 1) } } } } return subject }, fire: function (eventName) { var callbacks = registeredEvents[eventName]; if (!callbacks) { return subject } var fireArguments; if (arguments.length > 1) { fireArguments = Array.prototype.splice.call(arguments, 1) } for (var i = 0; i < callbacks.length; ++i) { var callbackInfo = callbacks[i]; callbackInfo.callback.apply(callbackInfo.ctx, fireArguments) } return subject } } } function validateSubject(subject) { if (!subject) { throw new Error("Eventify cannot use falsy object as events subject") } var reservedWords = ["on", "fire", "off"]; for (var i = 0; i < reservedWords.length; ++i) { if (subject.hasOwnProperty(reservedWords[i])) { throw new Error("Subject cannot be eventified, since it already has property '" + reservedWords[i] + "'") } } } }, {}], 10: [function (require, module, exports) { module.exports = addWheelListener; module.exports.addWheelListener = addWheelListener; module.exports.removeWheelListener = removeWheelListener; var prefix = "", _addEventListener, _removeEventListener, support; detectEventModel(typeof window !== "undefined" && window, typeof document !== "undefined" && document); function addWheelListener(elem, callback, useCapture) { _addWheelListener(elem, support, callback, useCapture); if (support == "DOMMouseScroll") { _addWheelListener(elem, "MozMousePixelScroll", callback, useCapture) } } function removeWheelListener(elem, callback, useCapture) { _removeWheelListener(elem, support, callback, useCapture); if (support == "DOMMouseScroll") { _removeWheelListener(elem, "MozMousePixelScroll", callback, useCapture) } } function _addWheelListener(elem, eventName, callback, useCapture) { elem[_addEventListener](prefix + eventName, support == "wheel" ? callback : function (originalEvent) { !originalEvent && (originalEvent = window.event); var event = { originalEvent: originalEvent, target: originalEvent.target || originalEvent.srcElement, type: "wheel", deltaMode: originalEvent.type == "MozMousePixelScroll" ? 0 : 1, deltaX: 0, deltaY: 0, deltaZ: 0, clientX: originalEvent.clientX, clientY: originalEvent.clientY, preventDefault: function () { originalEvent.preventDefault ? originalEvent.preventDefault() : originalEvent.returnValue = false }, stopPropagation: function () { if (originalEvent.stopPropagation) originalEvent.stopPropagation() }, stopImmediatePropagation: function () { if (originalEvent.stopImmediatePropagation) originalEvent.stopImmediatePropagation() } }; if (support == "mousewheel") { event.deltaY = -1 / 40 * originalEvent.wheelDelta; originalEvent.wheelDeltaX && (event.deltaX = -1 / 40 * originalEvent.wheelDeltaX) } else { event.deltaY = originalEvent.detail } return callback(event) }, useCapture || false) } function _removeWheelListener(elem, eventName, callback, useCapture) { elem[_removeEventListener](prefix + eventName, callback, useCapture || false) } function detectEventModel(window, document) { if (window && window.addEventListener) { _addEventListener = "addEventListener"; _removeEventListener = "removeEventListener" } else { _addEventListener = "attachEvent"; _removeEventListener = "detachEvent"; prefix = "on" } if (document) { support = "onwheel" in document.createElement("div") ? "wheel" : document.onmousewheel !== undefined ? "mousewheel" : "DOMMouseScroll" } else { support = "wheel" } } }, {}] }, {}, [1])(1) }); 2 | -------------------------------------------------------------------------------- /custom_components/zigbee2mqtt_networkmap/www/viz.js/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014-2018 Michael Daines 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /custom_components/zigbee2mqtt_networkmap/www/viz.js/viz.js: -------------------------------------------------------------------------------- 1 | /* 2 | Viz.js 2.1.1 (Graphviz 2.40.1, Expat 2.2.5, Emscripten 1.37.36) 3 | Copyright (c) 2014-2018 Michael Daines 4 | Licensed under MIT license 5 | 6 | This distribution contains other software in object code form: 7 | 8 | Graphviz 9 | Licensed under Eclipse Public License - v 1.0 10 | http://www.graphviz.org 11 | 12 | Expat 13 | Copyright (c) 1998, 1999, 2000 Thai Open Source Software Center Ltd and Clark Cooper 14 | Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006 Expat maintainers. 15 | Licensed under MIT license 16 | http://www.libexpat.org 17 | 18 | zlib 19 | Copyright (C) 1995-2013 Jean-loup Gailly and Mark Adler 20 | http://www.zlib.net/zlib_license.html 21 | */ 22 | (function (global, factory) { 23 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : 24 | typeof define === 'function' && define.amd ? define(factory) : 25 | (global.Viz = factory()); 26 | }(this, (function () { 27 | 'use strict'; 28 | 29 | var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { 30 | return typeof obj; 31 | } : function (obj) { 32 | return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; 33 | }; 34 | 35 | var classCallCheck = function (instance, Constructor) { 36 | if (!(instance instanceof Constructor)) { 37 | throw new TypeError("Cannot call a class as a function"); 38 | } 39 | }; 40 | 41 | var createClass = function () { 42 | function defineProperties(target, props) { 43 | for (var i = 0; i < props.length; i++) { 44 | var descriptor = props[i]; 45 | descriptor.enumerable = descriptor.enumerable || false; 46 | descriptor.configurable = true; 47 | if ("value" in descriptor) descriptor.writable = true; 48 | Object.defineProperty(target, descriptor.key, descriptor); 49 | } 50 | } 51 | 52 | return function (Constructor, protoProps, staticProps) { 53 | if (protoProps) defineProperties(Constructor.prototype, protoProps); 54 | if (staticProps) defineProperties(Constructor, staticProps); 55 | return Constructor; 56 | }; 57 | }(); 58 | 59 | var _extends = Object.assign || function (target) { 60 | for (var i = 1; i < arguments.length; i++) { 61 | var source = arguments[i]; 62 | 63 | for (var key in source) { 64 | if (Object.prototype.hasOwnProperty.call(source, key)) { 65 | target[key] = source[key]; 66 | } 67 | } 68 | } 69 | 70 | return target; 71 | }; 72 | 73 | var WorkerWrapper = function () { 74 | function WorkerWrapper(worker) { 75 | var _this = this; 76 | 77 | classCallCheck(this, WorkerWrapper); 78 | 79 | this.worker = worker; 80 | this.listeners = []; 81 | this.nextId = 0; 82 | 83 | this.worker.addEventListener('message', function (event) { 84 | var id = event.data.id; 85 | var error = event.data.error; 86 | var result = event.data.result; 87 | 88 | _this.listeners[id](error, result); 89 | delete _this.listeners[id]; 90 | }); 91 | } 92 | 93 | createClass(WorkerWrapper, [{ 94 | key: 'render', 95 | value: function render(src, options) { 96 | var _this2 = this; 97 | 98 | return new Promise(function (resolve, reject) { 99 | var id = _this2.nextId++; 100 | 101 | _this2.listeners[id] = function (error, result) { 102 | if (error) { 103 | reject(new Error(error.message, error.fileName, error.lineNumber)); 104 | return; 105 | } 106 | resolve(result); 107 | }; 108 | 109 | _this2.worker.postMessage({ id: id, src: src, options: options }); 110 | }); 111 | } 112 | }]); 113 | return WorkerWrapper; 114 | }(); 115 | 116 | var ModuleWrapper = function ModuleWrapper(module, render) { 117 | classCallCheck(this, ModuleWrapper); 118 | 119 | var instance = module(); 120 | this.render = function (src, options) { 121 | return new Promise(function (resolve, reject) { 122 | try { 123 | resolve(render(instance, src, options)); 124 | } catch (error) { 125 | reject(error); 126 | } 127 | }); 128 | }; 129 | }; 130 | 131 | // https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding 132 | 133 | 134 | function b64EncodeUnicode(str) { 135 | return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function (match, p1) { 136 | return String.fromCharCode('0x' + p1); 137 | })); 138 | } 139 | 140 | function defaultScale() { 141 | if ('devicePixelRatio' in window && window.devicePixelRatio > 1) { 142 | return window.devicePixelRatio; 143 | } else { 144 | return 1; 145 | } 146 | } 147 | 148 | function svgXmlToImageElement(svgXml) { 149 | var _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}, 150 | _ref$scale = _ref.scale, 151 | scale = _ref$scale === undefined ? defaultScale() : _ref$scale, 152 | _ref$mimeType = _ref.mimeType, 153 | mimeType = _ref$mimeType === undefined ? "image/png" : _ref$mimeType, 154 | _ref$quality = _ref.quality, 155 | quality = _ref$quality === undefined ? 1 : _ref$quality; 156 | 157 | return new Promise(function (resolve, reject) { 158 | var svgImage = new Image(); 159 | 160 | svgImage.onload = function () { 161 | var canvas = document.createElement('canvas'); 162 | canvas.width = svgImage.width * scale; 163 | canvas.height = svgImage.height * scale; 164 | 165 | var context = canvas.getContext("2d"); 166 | context.drawImage(svgImage, 0, 0, canvas.width, canvas.height); 167 | 168 | canvas.toBlob(function (blob) { 169 | var image = new Image(); 170 | image.src = URL.createObjectURL(blob); 171 | image.width = svgImage.width; 172 | image.height = svgImage.height; 173 | 174 | resolve(image); 175 | }, mimeType, quality); 176 | }; 177 | 178 | svgImage.onerror = function (e) { 179 | var error; 180 | 181 | if ('error' in e) { 182 | error = e.error; 183 | } else { 184 | error = new Error('Error loading SVG'); 185 | } 186 | 187 | reject(error); 188 | }; 189 | 190 | svgImage.src = 'data:image/svg+xml;base64,' + b64EncodeUnicode(svgXml); 191 | }); 192 | } 193 | 194 | function svgXmlToImageElementFabric(svgXml) { 195 | var _ref2 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}, 196 | _ref2$scale = _ref2.scale, 197 | scale = _ref2$scale === undefined ? defaultScale() : _ref2$scale, 198 | _ref2$mimeType = _ref2.mimeType, 199 | mimeType = _ref2$mimeType === undefined ? 'image/png' : _ref2$mimeType, 200 | _ref2$quality = _ref2.quality, 201 | quality = _ref2$quality === undefined ? 1 : _ref2$quality; 202 | 203 | var multiplier = scale; 204 | 205 | var format = void 0; 206 | if (mimeType == 'image/jpeg') { 207 | format = 'jpeg'; 208 | } else if (mimeType == 'image/png') { 209 | format = 'png'; 210 | } 211 | 212 | return new Promise(function (resolve, reject) { 213 | fabric.loadSVGFromString(svgXml, function (objects, options) { 214 | // If there's something wrong with the SVG, Fabric may return an empty array of objects. Graphviz appears to give us at least one element back even given an empty graph, so we will assume an error in this case. 215 | if (objects.length == 0) { 216 | reject(new Error('Error loading SVG with Fabric')); 217 | } 218 | 219 | var element = document.createElement("canvas"); 220 | element.width = options.width; 221 | element.height = options.height; 222 | 223 | var canvas = new fabric.Canvas(element, { enableRetinaScaling: false }); 224 | var obj = fabric.util.groupSVGElements(objects, options); 225 | canvas.add(obj).renderAll(); 226 | 227 | var image = new Image(); 228 | image.src = canvas.toDataURL({ format: format, multiplier: multiplier, quality: quality }); 229 | image.width = options.width; 230 | image.height = options.height; 231 | 232 | resolve(image); 233 | }); 234 | }); 235 | } 236 | 237 | var Viz = function () { 238 | function Viz() { 239 | var _ref3 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}, 240 | workerURL = _ref3.workerURL, 241 | worker = _ref3.worker, 242 | Module = _ref3.Module, 243 | render = _ref3.render; 244 | 245 | classCallCheck(this, Viz); 246 | 247 | if (typeof workerURL !== 'undefined') { 248 | this.wrapper = new WorkerWrapper(new Worker(workerURL)); 249 | } else if (typeof worker !== 'undefined') { 250 | this.wrapper = new WorkerWrapper(worker); 251 | } else if (typeof Module !== 'undefined' && typeof render !== 'undefined') { 252 | this.wrapper = new ModuleWrapper(Module, render); 253 | } else if (typeof Viz.Module !== 'undefined' && typeof Viz.render !== 'undefined') { 254 | this.wrapper = new ModuleWrapper(Viz.Module, Viz.render); 255 | } else { 256 | throw new Error('Must specify workerURL or worker option, Module and render options, or include one of full.render.js or lite.render.js after viz.js.'); 257 | } 258 | } 259 | 260 | createClass(Viz, [{ 261 | key: 'renderString', 262 | value: function renderString(src) { 263 | var _ref4 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}, 264 | _ref4$format = _ref4.format, 265 | format = _ref4$format === undefined ? 'svg' : _ref4$format, 266 | _ref4$engine = _ref4.engine, 267 | engine = _ref4$engine === undefined ? 'dot' : _ref4$engine, 268 | _ref4$files = _ref4.files, 269 | files = _ref4$files === undefined ? [] : _ref4$files, 270 | _ref4$images = _ref4.images, 271 | images = _ref4$images === undefined ? [] : _ref4$images, 272 | _ref4$yInvert = _ref4.yInvert, 273 | yInvert = _ref4$yInvert === undefined ? false : _ref4$yInvert, 274 | _ref4$nop = _ref4.nop, 275 | nop = _ref4$nop === undefined ? 0 : _ref4$nop; 276 | 277 | for (var i = 0; i < images.length; i++) { 278 | files.push({ 279 | path: images[i].path, 280 | data: '\n\n' 281 | }); 282 | } 283 | 284 | return this.wrapper.render(src, { format: format, engine: engine, files: files, images: images, yInvert: yInvert, nop: nop }); 285 | } 286 | }, { 287 | key: 'renderSVGElement', 288 | value: function renderSVGElement(src) { 289 | var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 290 | 291 | return this.renderString(src, _extends({}, options, { format: 'svg' })).then(function (str) { 292 | var parser = new DOMParser(); 293 | return parser.parseFromString(str, 'image/svg+xml').documentElement; 294 | }); 295 | } 296 | }, { 297 | key: 'renderImageElement', 298 | value: function renderImageElement(src) { 299 | var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 300 | var scale = options.scale, 301 | mimeType = options.mimeType, 302 | quality = options.quality; 303 | 304 | 305 | return this.renderString(src, _extends({}, options, { format: 'svg' })).then(function (str) { 306 | if ((typeof fabric === 'undefined' ? 'undefined' : _typeof(fabric)) === "object" && fabric.loadSVGFromString) { 307 | return svgXmlToImageElementFabric(str, { scale: scale, mimeType: mimeType, quality: quality }); 308 | } else { 309 | return svgXmlToImageElement(str, { scale: scale, mimeType: mimeType, quality: quality }); 310 | } 311 | }); 312 | } 313 | }, { 314 | key: 'renderJSONObject', 315 | value: function renderJSONObject(src) { 316 | var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 317 | var format = options.format; 318 | 319 | 320 | if (format !== 'json' || format !== 'json0') { 321 | format = 'json'; 322 | } 323 | 324 | return this.renderString(src, _extends({}, options, { format: format })).then(function (str) { 325 | return JSON.parse(str); 326 | }); 327 | } 328 | }]); 329 | return Viz; 330 | }(); 331 | 332 | return Viz; 333 | 334 | }))); 335 | -------------------------------------------------------------------------------- /info.md: -------------------------------------------------------------------------------- 1 | # ha_zigbee2mqtt_networkmap 2 | Custom Component for Homeassistant to show the [zigbee2mqtt](https://github.com/Koenkk/zigbee2mqtt) Networkmap with [viz.js](https://github.com/mdaines/viz.js/). 3 | 4 | [Forum link with Screenshot](https://community.home-assistant.io/t/zigbee2mqtt-show-the-networkmap-in-hassio/89116) 5 | 6 | **Important:** you have to clear the browsercache after each update 7 | 8 | 9 | **Instructions** 10 | 1. Add the following to your configuration.yaml. It is possible to update the map directly via button. If you want to use this functionality you also have to activate the webhook component. Otherwise you have to use the service "zigbee2mqtt_networkmap.update" 11 | 12 | webhook: 13 | 14 | zigbee2mqtt_networkmap: 15 | #topic: your topic (optional, default zigbee2mqtt) 16 | panel_iframe: 17 | networkmap: 18 | title: 'Zigbee Map' 19 | url: '/local/community/zigbee2mqtt_networkmap/map.html' 20 | icon: 'mdi:graphql' 21 | You can set the graphviz engine via URL Parameter: 22 | map.html?engine=circo (Default: circo, [Supported Engines](https://github.com/mdaines/viz.js/wiki/Supported-Graphviz-Features)) 23 | 24 | 2. Restart Homeassistant 25 | 3. Test if everything is working 26 | 27 | 28 | -------------------------------------------------------------------------------- /map.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rgruebel/ha_zigbee2mqtt_networkmap/23e20cf83a0c302fa007494d785be1103d2779e3/map.gif --------------------------------------------------------------------------------