Skip to content
This repository has been archived by the owner on Mar 11, 2021. It is now read-only.

Add hacs.json #14

Merged
merged 31 commits into from
Feb 29, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
dea0364
Initial commit.
Adminiuga Aug 1, 2019
ecdbc9f
Scan all discovered neighbours.
Adminiuga Aug 1, 2019
85a3e0f
Start initial scan after a delay.
Adminiuga Aug 1, 2019
ef23385
isort imports.
Adminiuga Aug 1, 2019
9ec3be2
Handle unknown neigbours.
Adminiuga Aug 1, 2019
2d18b09
Handle failures gracefuly.
Adminiuga Aug 1, 2019
700037d
scan_now service.
Adminiuga Aug 1, 2019
66d03c8
Black format.
Adminiuga Aug 1, 2019
b94c2a0
Expose "current" topology through a property.
Adminiuga Aug 4, 2019
e99180a
Fix handling of devices not in db.
Adminiuga Aug 7, 2019
579eefe
Add WS command.
Adminiuga Aug 7, 2019
3cc5203
"offline" property.
Adminiuga Aug 7, 2019
2747100
Add "neighbours" missing in neighobour table.
Adminiuga Aug 14, 2019
ae74eb7
Make sure device_typs always has a value.
Adminiuga Aug 20, 2019
4aea068
Skip devices not supporting ZDO lqi_mgmt_req.
Adminiuga Sep 1, 2019
ccbaf82
return timestamp in WS call (#2)
abmantis Oct 10, 2019
22752ea
ignore invalid neighbours reported by some devices (#1)
abmantis Oct 10, 2019
0a798d3
Ignore 00:00:00:00:00:00:00:00 neighbours.
Adminiuga Oct 10, 2019
c979627
Fix IEEE conversions.
Adminiuga Nov 11, 2019
c352b18
Use node descriptor only if available.
Adminiuga Nov 11, 2019
23efc79
Doh.
Adminiuga Nov 11, 2019
ae3661c
Use hex representation for NWK addresses.
Adminiuga Nov 11, 2019
c246984
Black format code.
Adminiuga Nov 11, 2019
7e341b3
Update neighbour.py (#4)
jkkwon83 Jan 20, 2020
49fb8c7
fix unhadled exception on scan timeout (#5)
abmantis Jan 22, 2020
9ef57df
Update README.md with usage instruction (#7)
Gamester17 Feb 2, 2020
0a4b775
Revert "fix unhadled exception on scan timeout (#5)" (#10)
Adminiuga Feb 16, 2020
f0ffb0e
Create info.md
Adminiuga Feb 29, 2020
4680580
Create LICENSE (#12)
Adminiuga Feb 29, 2020
97088ad
Update info.md
Adminiuga Feb 29, 2020
d77cabb
Create hacs.json
Adminiuga Feb 29, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2019 Alexei Chetroi

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:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

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.
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,24 @@
# zha-map
Build ZHA network topology map.

[zha-map](https://github.com/zha-ng/zha-map) integration commponent for [Home Assistant](https://www.home-assistant.io) allow you to make a ZHA (Zigbee Home Automation) network topology map.

Requires that you are already using the [ZHA](https://www.home-assistant.io/integrations/zha/) integration commponent in Home Assistant.

Zigbee network mapping with zha-map can help you identify weak points like bad links between your devices.

# Installation Instructions

1. Install the zha_map custom component
- https://github.com/zha-ng/zha-map
2. Add zha_map: to your configuration.yaml and restart Home Assistant
3. Wait for a scan to complete (logs to DEBUG, or use the new zha_map service to scan on demand)
4. Install the zha-network-visualization-card lovelace card
- https://github.com/dmulcahey/zha-network-visualization-card
5. Add to your lovelace global config as type: module
6. Add custom card (works best in panel mode): - type: 'custom:zha-network-visualization-card'

# Usage
The Zigbee coordinator will be represented by a rectangle at the top. Any device that serves as Zigbee router (usually all devices running on Mains electricity / grid power) will represented as ovals, and Zigbee end-device (usually battery powered sensors) will be represented by as circles.

The lines between those representions show all the possible paths through Zigbee mesh. Any path with a LQI over 192 is shown as green, LQI 129-192 is shown as yellow, and anything 128 and lower is shown as red.
245 changes: 245 additions & 0 deletions custom_components/zha_map/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
import asyncio
import logging
import os
import time
from datetime import timedelta

import voluptuous as vol
import zigpy.exceptions as zigpy_exc

from homeassistant.components import websocket_api
from homeassistant.helpers.event import async_call_later, async_track_time_interval
from homeassistant.util.json import save_json

from .helpers import LogMixin
from .neighbour import Neighbour, NeighbourType

ATTR_TOPO = "topology"
ATTR_TYPE = "type"
ATTR_OUTPUT_DIR = "output_dir"
AWAKE_INTERVAL = timedelta(hours=4, minutes=15)
DOMAIN = "zha_map"
CONFIG_OUTPUT_DIR_NAME = "neighbours"
CONFIG_INITIAL_SCAN_DELAY = 20 * 60
SERVICE_SCAN_NOW = "scan_now"
SERVICE_SCHEMAS = {SERVICE_SCAN_NOW: vol.Schema({})}

LOGGER = logging.getLogger(__name__)


async def async_setup(hass, config):
"""Set up ZHA from config."""

if DOMAIN not in config:
return True

try:
zha_gateway = hass.data["zha"]["zha_gateway"]
except KeyError:
return False

builder = TopologyBuilder(hass, zha_gateway)
hass.data[DOMAIN] = {ATTR_TOPO: builder}
output_dir = os.path.join(hass.config.config_dir, CONFIG_OUTPUT_DIR_NAME)
hass.data[DOMAIN][ATTR_OUTPUT_DIR] = output_dir

def mkdir(dir):
try:
os.mkdir(dir)
return True
except OSError as exc:
LOGGER.error("Couldn't create '%s' dir: %s", dir, exc)
return False

if not os.path.isdir(output_dir):
if not await hass.async_add_executor_job(mkdir, output_dir):
return False

async def setup_scanner(_now):
async_track_time_interval(hass, builder.time_tracker, AWAKE_INTERVAL)
await builder.time_tracker()

async_call_later(hass, CONFIG_INITIAL_SCAN_DELAY, setup_scanner)

async def scan_now_handler(service):
"""Scan topology right now."""
await builder.preempt_build()

hass.services.async_register(
DOMAIN,
SERVICE_SCAN_NOW,
scan_now_handler,
schema=SERVICE_SCHEMAS[SERVICE_SCAN_NOW],
)

@websocket_api.require_admin
@websocket_api.async_response
@websocket_api.websocket_command({vol.Required(ATTR_TYPE): f"{DOMAIN}/devices"})
async def websocket_get_devices(hass, connection, msg):
"""Get ZHA Map devices."""

response = {
"time": builder.timestamp,
"devices": [nei.json() for nei in builder.current.values()],
}
connection.send_result(msg["id"], response)

websocket_api.async_register_command(hass, websocket_get_devices)

return True


class TopologyBuilder(LogMixin):
"""Keeps track of topology."""

log = LOGGER.log

def __init__(self, hass, zha_gw):
"""Init instance."""
self._hass = hass
self._app = zha_gw
self._in_process = None
self._seen = {}
self._current = {}
self._failed = {}
self._timestamp = 0

@property
def current(self):
"""Return a dict with all Router/Coordinator devices."""
return self._current

@property
def timestamp(self):
"""Return the timestamp of the last scan."""
return self._timestamp

async def time_tracker(self, time=None):
"""Awake periodically."""
if self._in_process and not self._in_process.done():
return
self._in_process = self._hass.async_create_task(self.build())

async def preempt_build(self):
"""Start a new scan, preempting the current one in progress."""
if self._in_process and not self._in_process.done():
self.debug("Cancelling a neighbour scan in progress")
self._in_process.cancel()
self._in_process = self._hass.async_create_task(self.build())

async def build(self):
self._seen.clear()
self._failed.clear()

seed = self._app.application_controller.get_device(nwk=0x0000)
self.debug("Building topology starting from coordinator")
try:
await self.scan_device(seed)
except zigpy_exc.ZigbeeException as exc:
self.error("failed to scan %s device: %s", seed.ieee, exc)
return

pending = self._pending()
while pending:
for nei in pending:
try:
await nei.scan()
except (zigpy_exc.ZigbeeException, asyncio.TimeoutError):
self.warning("Couldn't scan %s neighbours", nei.ieee)
self._failed[nei.ieee] = nei
nei.offline = True
continue
await self.process_neighbour_table(nei)
pending = self._pending()

await self.sanity_check()
self._current = {**self._seen}
self._timestamp = time.time()

def _pending(self):
"""Return neighbours still pending a scan."""
pending = [
n
for n in self._seen.values()
if not n.neighbours
and n.supported
and n.device is not None
and n.device_type
in (NeighbourType.Coordinator.name, NeighbourType.Router.name)
and n.ieee not in self._failed
]

if pending:
self.debug(
"continuing neighbour scan. Neighbours discovered: %s",
[n.ieee for n in pending],
)
else:
self.debug(
"Finished neighbour scan pass. Failed: %s",
[k for k in self._failed.keys()],
)
return pending

async def sanity_check(self):
"""Check discovered neighbours vs Zigpy database."""
# do we have extra neighbours
for nei in self._seen.values():
if nei.ieee not in self._app.application_controller.devices:
self.debug(
"Neighbour not in 'zigbee.db': %s - %s", nei.ieee, nei.device_type
)

# are we missing neighbours
for dev in self._app.application_controller.devices.values():
if dev.ieee in self._seen:
continue

if dev.ieee in self._failed:
self.debug(
(
"%s (%s %s) was discovered in the neighbours "
"tables, but did not respond"
),
dev.ieee,
dev.manufacturer,
dev.model,
)
else:
self.debug(
"%s (%s %s) was not found in the neighbours tables",
dev.ieee,
dev.manufacturer,
dev.model,
)
nei = Neighbour(dev.ieee, f"0x{dev.nwk:04x}", "unk")
nei.device = dev
nei.model = dev.model
nei.manufacturer = dev.manufacturer
nei.offline = True
if dev.node_desc.logical_type is not None:
nei.device_type = dev.node_desc.logical_type.name
self._seen[dev.ieee] = nei

async def scan_device(self, device):
"""Scan device neigbours."""
nei = await Neighbour.scan_device(device)
await self.process_neighbour_table(nei)

async def process_neighbour_table(self, nei):
for entry in nei.neighbours:
if entry.ieee in self._seen:
continue
self.debug("Adding %s to all neighbours", entry.ieee)
self._seen[entry.ieee] = entry
await self.save_neighbours(nei)

async def save_neighbours(self, nei):
suffix = str(nei.ieee).replace(":", "")
suffix = f"_{suffix}.txt"

file_name = os.path.join(
self._hass.data[DOMAIN][ATTR_OUTPUT_DIR], "neighbours" + suffix
)
self.debug("Saving %s", file_name)
await self._hass.async_add_executor_job(save_json, file_name, nei.json())
25 changes: 25 additions & 0 deletions custom_components/zha_map/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import logging


class LogMixin:
"""Log helper."""

def log(self, level, msg, *args):
"""Log with level."""
raise NotImplementedError

def debug(self, msg, *args):
"""Debug level log."""
return self.log(logging.DEBUG, msg, *args)

def info(self, msg, *args):
"""Info level log."""
return self.log(logging.INFO, msg, *args)

def warning(self, msg, *args):
"""Warning method log."""
return self.log(logging.WARNING, msg, *args)

def error(self, msg, *args):
"""Error level log."""
return self.log(logging.ERROR, msg, *args)
10 changes: 10 additions & 0 deletions custom_components/zha_map/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"domain": "zha_map",
"name": "ZHA Network Map",
"documentation": "https://github.com/zha-ng/zha-map",
"requirements": [],
"dependencies": ["zha"],
"codeowners": [
"@adminiuga"
]
}
Loading