From c5d35db252d8689e1d8fbe83392be995bb001488 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 21 Sep 2020 19:40:28 +0200 Subject: [PATCH 1/5] Add GitHub Action to validate integration with hassfest (#18) --- .github/workflows/hassfest.yaml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .github/workflows/hassfest.yaml diff --git a/.github/workflows/hassfest.yaml b/.github/workflows/hassfest.yaml new file mode 100644 index 0000000..440e454 --- /dev/null +++ b/.github/workflows/hassfest.yaml @@ -0,0 +1,14 @@ +name: Validate with hassfest + +on: + push: + pull_request: + schedule: + - cron: '0 0 * * *' + +jobs: + validate: + runs-on: "ubuntu-latest" + steps: + - uses: "actions/checkout@v2" + - uses: home-assistant/actions/hassfest@master From 6b88442732f48951045939bd675bdb956bedc854 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Mon, 21 Sep 2020 22:40:39 -0400 Subject: [PATCH 2/5] Supress active scan (#27) --- custom_components/zha_map/neighbour.py | 55 ++++++-------------------- 1 file changed, 13 insertions(+), 42 deletions(-) diff --git a/custom_components/zha_map/neighbour.py b/custom_components/zha_map/neighbour.py index 58f5e94..0cb45f5 100644 --- a/custom_components/zha_map/neighbour.py +++ b/custom_components/zha_map/neighbour.py @@ -1,12 +1,8 @@ -import asyncio import enum import logging -import random import attr import zigpy.zdo.types as zdo_t -from zigpy.exceptions import DeliveryError -from zigpy.util import retryable from .helpers import LogMixin @@ -49,7 +45,10 @@ def new_from_record(cls, record): r.device_type = record.struct.device_type.name r.rx_on_when_idle = record.struct.rx_on_when_idle.name - r.relation = record.struct.relationship.name + if record.struct.relationship == zdo_t.Neighbor.RelationShip.NoneOfTheAbove: + r.relation = "None_of_the_above" + else: + r.relation = record.struct.relationship.name r.new_joins_accepted = record.permit_joining.name r.depth = record.depth r.lqi = record.lqi @@ -77,43 +76,15 @@ async def scan_device(cls, device): async def scan(self): """Scan for neighbours.""" - idx = 0 - while True: - status, val = await self.device.zdo.Mgmt_Lqi_req(idx, tries=3, delay=1) - self.debug("neighbor request Status: %s. Response: %r", status, val) - if zdo_t.Status.SUCCESS != status: - self.supported = False - self.debug("device does not support 'Mgmt_Lqi_req'") - return - - neighbors = val.neighbor_table_list - for neighbor in neighbors: - new = self.new_from_record(neighbor) - - if repr(new.ieee) in ( - "00:00:00:00:00:00:00:00", - "ff:ff:ff:ff:ff:ff:ff:ff", - ): - self.debug("Ignoring invalid neighbour: %s", new.ieee) - idx += 1 - continue - - try: - new.device = self.device.application.get_device(new.ieee) - new._update_info() - except KeyError: - self.warning("neighbour %s is not in 'zigbee.db'", new.ieee) - self.neighbours.append(new) - idx += 1 - if idx >= val.entries: - break - if len(neighbors) <= 0: - idx += 1 - self.debug("Neighbor count is 0 (idx : %d)", idx) - - await asyncio.sleep(random.uniform(1.0, 1.5)) - self.debug("Querying next starting at %s", idx) - + for neighbor in self.device.neighbors: + new = self.new_from_record(neighbor.neighbor) + try: + new.device = self.device.application.get_device(new.ieee) + new._update_info() + except KeyError: + self.warning("neighbour %s is not in 'zigbee.db'", new.ieee) + + self.neighbours.append(new) self.debug("Done scanning. Total %s neighbours", len(self.neighbours)) def log(self, level, msg, *args): From 336427fbe140f80e872b13221cb6d9a6688e0d3d Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Tue, 22 Sep 2020 20:52:11 -0400 Subject: [PATCH 3/5] Refactor scanner to use zigpy topology scan_now() service rebuilds topology based on zigpy information and kicks off zigpy scanning. --- custom_components/zha_map/__init__.py | 103 +++++++------------------- 1 file changed, 26 insertions(+), 77 deletions(-) diff --git a/custom_components/zha_map/__init__.py b/custom_components/zha_map/__init__.py index b7ee9de..5a980ba 100644 --- a/custom_components/zha_map/__init__.py +++ b/custom_components/zha_map/__init__.py @@ -57,7 +57,7 @@ def mkdir(dir): async def setup_scanner(_now): async_track_time_interval(hass, builder.time_tracker, AWAKE_INTERVAL) - await builder.time_tracker() + await builder.preempt_build() async_call_later(hass, CONFIG_INITIAL_SCAN_DELAY, setup_scanner) @@ -101,7 +101,6 @@ def __init__(self, hass, zha_gw): self._in_process = None self._seen = {} self._current = {} - self._failed = {} self._timestamp = 0 @property @@ -116,71 +115,32 @@ def timestamp(self): 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()) + await 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()) + await self.build() + self._in_process = self._hass.async_create_task(self.zigpy_scan_wrapper()) + + async def zigpy_scan_wrapper(self) -> None: + """Scan zigpy then build.""" + await self._app.application_controller.topology.scan() + await 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() + self.debug("Building topology") + for device in self._app.application_controller.devices.values(): + await self.scan_device(device) 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 @@ -195,31 +155,20 @@ async def sanity_check(self): 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 + 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.""" From c5931d0da1097a418ac49364580dbfcfb8cd7684 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Tue, 22 Sep 2020 21:20:27 -0400 Subject: [PATCH 4/5] Update device type --- custom_components/zha_map/neighbour.py | 1 + 1 file changed, 1 insertion(+) diff --git a/custom_components/zha_map/neighbour.py b/custom_components/zha_map/neighbour.py index 0cb45f5..e2af782 100644 --- a/custom_components/zha_map/neighbour.py +++ b/custom_components/zha_map/neighbour.py @@ -61,6 +61,7 @@ def _update_info(self): self.nwk = "0x{:04x}".format(self.device.nwk) self.model = self.device.model self.manufacturer = self.device.manufacturer + self.device_type = self.device.node_desc.logical_type.name @classmethod async def scan_device(cls, device): From 7d2ebd46ddbbbacce0ef977b8f6385f7964b55e0 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Tue, 22 Sep 2020 21:35:06 -0400 Subject: [PATCH 5/5] Save only routers and coordinator. Don't overwrite entries --- custom_components/zha_map/__init__.py | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/custom_components/zha_map/__init__.py b/custom_components/zha_map/__init__.py index 5a980ba..0e09975 100644 --- a/custom_components/zha_map/__init__.py +++ b/custom_components/zha_map/__init__.py @@ -135,7 +135,10 @@ async def build(self): self.debug("Building topology") for device in self._app.application_controller.devices.values(): - await self.scan_device(device) + nei = await Neighbour.scan_device(device) + self._seen[nei.ieee] = nei + if device.node_desc.logical_type in (0, 1): + await self.save_neighbours(nei) await self.sanity_check() self._current = {**self._seen} @@ -170,19 +173,6 @@ async def sanity_check(self): 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"