diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9a7c571..0efe7a4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,7 +7,7 @@ on: env: CACHE_VERSION: 1 - DEFAULT_PYTHON: 3.8 + DEFAULT_PYTHON: 3.8.16 PRE_COMMIT_HOME: ~/.cache/pre-commit jobs: @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.8.14', '3.9.15', '3.10.8', '3.11.0'] + python-version: ['3.8.16', '3.9.15', '3.10.8', '3.11.0'] steps: - name: Check out code from GitHub uses: actions/checkout@v2 @@ -271,7 +271,7 @@ jobs: needs: prepare-base strategy: matrix: - python-version: ['3.8.14', '3.9.15', '3.10.8', '3.11.0'] + python-version: ['3.8.16', '3.9.15', '3.10.8', '3.11.0'] name: >- Run tests Python ${{ matrix.python-version }} steps: @@ -311,7 +311,7 @@ jobs: . venv/bin/activate pytest \ -qq \ - --timeout=9 \ + --timeout=15 \ --durations=10 \ --cov zigpy_deconz \ --cov-report=term-missing \ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5b16775..f5bdfe2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/psf/black - rev: 22.3.0 + rev: 23.1.0 hooks: - id: black args: @@ -8,7 +8,7 @@ repos: - --quiet - repo: https://github.com/pycqa/flake8 - rev: 3.8.4 + rev: 6.0.0 hooks: - id: flake8 additional_dependencies: @@ -16,12 +16,12 @@ repos: - pydocstyle==5.1.1 - repo: https://github.com/PyCQA/isort - rev: 5.10.1 + rev: 5.12.0 hooks: - id: isort - repo: https://github.com/codespell-project/codespell - rev: v1.17.1 + rev: v2.2.4 hooks: - id: codespell args: diff --git a/setup.py b/setup.py index 3856070..5363f17 100644 --- a/setup.py +++ b/setup.py @@ -21,6 +21,6 @@ author_email="schmidt.d@aon.at", license="GPL-3.0", packages=find_packages(exclude=["tests"]), - install_requires=["zigpy>=0.52.1"], + install_requires=["zigpy>=0.54.0"], tests_require=["pytest", "asynctest"], ) diff --git a/tests/test_application.py b/tests/test_application.py index e3c6267..c00a7ab 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -2,11 +2,13 @@ import asyncio import logging +from unittest import mock import pytest +import zigpy.application import zigpy.config import zigpy.device -from zigpy.types import EUI64 +from zigpy.types import EUI64, Channels import zigpy.zdo.types as zdo_t from zigpy_deconz import types as t @@ -110,28 +112,32 @@ def addr_nwk_and_ieee(nwk, ieee): return addr +@patch("zigpy_deconz.zigbee.application.CHANGE_NETWORK_WAIT", 0.001) @pytest.mark.parametrize( - "proto_ver, nwk_state, error", + "proto_ver, target_state, returned_state", [ - (0x0107, deconz_api.NetworkState.CONNECTED, None), - (0x0106, deconz_api.NetworkState.CONNECTED, None), - (0x0107, deconz_api.NetworkState.OFFLINE, None), - (0x0107, deconz_api.NetworkState.OFFLINE, asyncio.TimeoutError()), + (0x0107, deconz_api.NetworkState.CONNECTED, deconz_api.NetworkState.CONNECTED), + (0x0106, deconz_api.NetworkState.CONNECTED, deconz_api.NetworkState.CONNECTED), + (0x0107, deconz_api.NetworkState.OFFLINE, deconz_api.NetworkState.CONNECTED), + (0x0107, deconz_api.NetworkState.CONNECTED, deconz_api.NetworkState.OFFLINE), ], ) -async def test_start_network(app, proto_ver, nwk_state, error): +async def test_start_network(app, proto_ver, target_state, returned_state): app.load_network_info = AsyncMock() app.restore_neighbours = AsyncMock() app.add_endpoint = AsyncMock() - app._change_network_state = AsyncMock(side_effect=error) app._api.device_state = AsyncMock( - return_value=(deconz_api.DeviceState(nwk_state), 0, 0) + return_value=(deconz_api.DeviceState(returned_state), 0, 0) ) + app._api._proto_ver = proto_ver app._api.protocol_version = proto_ver - if nwk_state != deconz_api.NetworkState.CONNECTED and error is not None: + if ( + target_state == deconz_api.NetworkState.CONNECTED + and returned_state != deconz_api.NetworkState.CONNECTED + ): with pytest.raises(zigpy.exceptions.FormationFailure): await app.start_network() @@ -569,3 +575,16 @@ async def test_reset_network_info(app): await app.reset_network_info() app.form_network.assert_called_once() + + +async def test_energy_scan(app): + with mock.patch.object( + zigpy.application.ControllerApplication, + "energy_scan", + return_value={c: c for c in Channels.ALL_CHANNELS}, + ): + results = await app.energy_scan( + channels=Channels.ALL_CHANNELS, duration_exp=0, count=1 + ) + + assert results == {c: c * 3 for c in Channels.ALL_CHANNELS} diff --git a/zigpy_deconz/__init__.py b/zigpy_deconz/__init__.py index ad39134..cea639a 100644 --- a/zigpy_deconz/__init__.py +++ b/zigpy_deconz/__init__.py @@ -2,7 +2,7 @@ # coding: utf-8 MAJOR_VERSION = 0 -MINOR_VERSION = 19 -PATCH_VERSION = "2" +MINOR_VERSION = 20 +PATCH_VERSION = "0" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" diff --git a/zigpy_deconz/zigbee/application.py b/zigpy_deconz/zigbee/application.py index 9379b35..e23d3a8 100644 --- a/zigpy_deconz/zigbee/application.py +++ b/zigpy_deconz/zigbee/application.py @@ -111,11 +111,7 @@ async def permit_with_key(self, node: t.EUI64, code: bytes, time_s=60): async def start_network(self): await self.register_endpoints() await self.load_network_info(load_devices=False) - - try: - await self._change_network_state(NetworkState.CONNECTED) - except asyncio.TimeoutError as e: - raise FormationFailure() from e + await self._change_network_state(NetworkState.CONNECTED) coordinator = await DeconzDevice.new( self, @@ -141,7 +137,19 @@ async def change_loop(): await asyncio.sleep(CHANGE_NETWORK_WAIT) await self._api.change_network_state(target_state) - await asyncio.wait_for(change_loop(), timeout=timeout) + + try: + await asyncio.wait_for(change_loop(), timeout=timeout) + except asyncio.TimeoutError: + if target_state != NetworkState.CONNECTED: + raise + + raise FormationFailure( + "Network formation refused: there is likely too much RF interference." + " Make sure your coordinator is on a USB 2.0 extension cable and" + " away from any sources of interference, like USB 3.0 ports, SSDs," + " 2.4GHz routers, motherboards, etc." + ) if self._api.protocol_version < PROTO_VER_WATCHDOG: return @@ -332,6 +340,16 @@ async def force_remove(self, dev): """Forcibly remove device from NCP.""" pass + async def energy_scan( + self, channels: t.Channels.ALL_CHANNELS, duration_exp: int, count: int + ) -> dict[int, float]: + results = await super().energy_scan( + channels=channels, duration_exp=duration_exp, count=count + ) + + # The Conbee seems to max out at an LQI of 85, which is exactly 255/3 + return {c: v * 3 for c, v in results.items()} + async def add_endpoint(self, descriptor: zdo_t.SimpleDescriptor) -> None: """Register an endpoint on the device, replacing any with conflicting IDs."""