diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8744c795..40badf3b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,7 +36,8 @@ jobs: ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('setup.py') }}-${{ - hashFiles('requirements_test.txt') }} + hashFiles('requirements_test.txt') }}-${{ + hashFiles('.pre-commit-config.yaml') }} restore-keys: | ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}- - name: Create Python virtual environment @@ -69,7 +70,8 @@ jobs: ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('setup.py') }}-${{ - hashFiles('requirements_test.txt') }} + hashFiles('requirements_test.txt') }}-${{ + hashFiles('.pre-commit-config.yaml') }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -111,7 +113,8 @@ jobs: ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('setup.py') }}-${{ - hashFiles('requirements_test.txt') }} + hashFiles('requirements_test.txt') }}-${{ + hashFiles('.pre-commit-config.yaml') }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -155,7 +158,8 @@ jobs: ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('setup.py') }}-${{ - hashFiles('requirements_test.txt') }} + hashFiles('requirements_test.txt') }}-${{ + hashFiles('.pre-commit-config.yaml') }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -202,7 +206,8 @@ jobs: ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('setup.py') }}-${{ - hashFiles('requirements_test.txt') }} + hashFiles('requirements_test.txt') }}-${{ + hashFiles('.pre-commit-config.yaml') }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -246,7 +251,8 @@ jobs: ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('setup.py') }}-${{ - hashFiles('requirements_test.txt') }} + hashFiles('requirements_test.txt') }}-${{ + hashFiles('.pre-commit-config.yaml') }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -297,7 +303,8 @@ jobs: ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('setup.py') }}-${{ - hashFiles('requirements_test.txt') }} + hashFiles('requirements_test.txt') }}-${{ + hashFiles('.pre-commit-config.yaml') }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | @@ -360,7 +367,8 @@ jobs: ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('setup.py') }}-${{ - hashFiles('requirements_test.txt') }} + hashFiles('requirements_test.txt') }}-${{ + hashFiles('.pre-commit-config.yaml') }} - name: Fail job if Python cache restore failed if: steps.cache-venv.outputs.cache-hit != 'true' run: | diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9909b758..57e8be76 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/psf/black - rev: 20.8b1 + rev: 22.3.0 hooks: - id: black args: diff --git a/bellows/__init__.py b/bellows/__init__.py index 6520a1a0..97994f1a 100644 --- a/bellows/__init__.py +++ b/bellows/__init__.py @@ -1,5 +1,5 @@ MAJOR_VERSION = 0 -MINOR_VERSION = 29 +MINOR_VERSION = 30 PATCH_VERSION = "0" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" diff --git a/bellows/cli/__init__.py b/bellows/cli/__init__.py index b44d8087..1c8f3836 100644 --- a/bellows/cli/__init__.py +++ b/bellows/cli/__init__.py @@ -1,2 +1,2 @@ # flake8: noqa -from . import application, backup, dump, ncp, network +from . import application, backup, dump, ncp, network, stream, tone diff --git a/bellows/cli/application.py b/bellows/cli/application.py index aab619b3..36d85f74 100644 --- a/bellows/cli/application.py +++ b/bellows/cli/application.py @@ -200,7 +200,7 @@ async def bind(ctx, endpoint, cluster): return try: - v = await dev.zdo.bind(endpoint, cluster) + v = await dev.zdo.bind(clust) click.echo(v) except zigpy.exceptions.ZigbeeException as e: click.echo(e) diff --git a/bellows/cli/stream.py b/bellows/cli/stream.py new file mode 100644 index 00000000..fea70959 --- /dev/null +++ b/bellows/cli/stream.py @@ -0,0 +1,59 @@ +import asyncio +import logging +import time + +import click + +from . import util +from .main import main + +LOGGER = logging.getLogger(__name__) + + +@main.command() +@click.option( + "-c", "--channel", type=click.IntRange(11, 26), metavar="CHANNEL", required=True +) +@click.option( + "-p", "--power", type=click.IntRange(-100, 20), metavar="POWER", required=True +) +@click.pass_context +def stream(ctx, channel, power): + """Transmit random stream of characters on CHANNEL with POWER (in dBm).""" + loop = asyncio.get_event_loop() + try: + loop.run_until_complete(_stream(ctx, channel, power)) + except KeyboardInterrupt: + start_time = ctx.obj.get("start_time", None) + if start_time: + duration = time.time() - start_time + click.echo( + "\nStreamed on channel %d for %0.2fs" % (channel, duration), err=True + ) + finally: + if "ezsp" in ctx.obj: + s = ctx.obj["ezsp"] + loop.run_until_complete(s.mfglibStopStream()) + loop.run_until_complete(s.mfglibEnd()) + s.close() + + +async def _stream(ctx, channel, power): + s = await util.setup(ctx.obj["device"], ctx.obj["baudrate"]) + ctx.obj["ezsp"] = s + + v = await s.mfglibStart(False) + util.check(v[0], "Unable to start mfglib") + + v = await s.mfglibSetChannel(channel) + util.check(v[0], "Unable to set channel") + + v = await s.mfglibSetPower(0, power) + util.check(v[0], "Unable to set power") + + v = await s.mfglibStartStream() + util.check(v[0], "Unable to start stream") + click.echo("Started transmitting random stream of characters", err=True) + ctx.obj["start_time"] = time.time() + while True: + await asyncio.sleep(3600) diff --git a/bellows/cli/tone.py b/bellows/cli/tone.py new file mode 100644 index 00000000..e31e3a0c --- /dev/null +++ b/bellows/cli/tone.py @@ -0,0 +1,59 @@ +import asyncio +import logging +import time + +import click + +from . import util +from .main import main + +LOGGER = logging.getLogger(__name__) + + +@main.command() +@click.option( + "-c", "--channel", type=click.IntRange(11, 26), metavar="CHANNEL", required=True +) +@click.option( + "-p", "--power", type=click.IntRange(-100, 20), metavar="POWER", required=True +) +@click.pass_context +def tone(ctx, channel, power): + """Transmit continuous unmodulated tone on CHANNEL with POWER (in dBm).""" + loop = asyncio.get_event_loop() + try: + loop.run_until_complete(_tone(ctx, channel, power)) + except KeyboardInterrupt: + start_time = ctx.obj.get("start_time", None) + if start_time: + duration = time.time() - start_time + click.echo( + "\nStreamed on channel %d for %0.2fs" % (channel, duration), err=True + ) + finally: + if "ezsp" in ctx.obj: + s = ctx.obj["ezsp"] + loop.run_until_complete(s.mfglibStopTone()) + loop.run_until_complete(s.mfglibEnd()) + s.close() + + +async def _tone(ctx, channel, power): + s = await util.setup(ctx.obj["device"], ctx.obj["baudrate"]) + ctx.obj["ezsp"] = s + + v = await s.mfglibStart(False) + util.check(v[0], "Unable to start mfglib") + + v = await s.mfglibSetChannel(channel) + util.check(v[0], "Unable to set channel") + + v = await s.mfglibSetPower(0, power) + util.check(v[0], "Unable to set power") + + v = await s.mfglibStartTone() + util.check(v[0], "Unable to start tone") + click.echo("Started transmitting unmodulated tone", err=True) + ctx.obj["start_time"] = time.time() + while True: + await asyncio.sleep(3600) diff --git a/bellows/config/__init__.py b/bellows/config/__init__.py index 2a64a063..b6dfd059 100644 --- a/bellows/config/__init__.py +++ b/bellows/config/__init__.py @@ -2,6 +2,15 @@ from zigpy.config import ( # noqa: F401 pylint: disable=unused-import CONF_DEVICE, CONF_DEVICE_PATH, + CONF_NWK, + CONF_NWK_CHANNEL, + CONF_NWK_CHANNELS, + CONF_NWK_EXTENDED_PAN_ID, + CONF_NWK_KEY, + CONF_NWK_PAN_ID, + CONF_NWK_TC_ADDRESS, + CONF_NWK_TC_LINK_KEY, + CONF_NWK_UPDATE_ID, CONFIG_SCHEMA, SCHEMA_DEVICE, cv_boolean, diff --git a/bellows/ezsp/__init__.py b/bellows/ezsp/__init__.py index 5c19f8f5..c07cf397 100644 --- a/bellows/ezsp/__init__.py +++ b/bellows/ezsp/__init__.py @@ -24,7 +24,7 @@ from . import v4, v5, v6, v7, v8 EZSP_LATEST = v8.EZSP_VERSION -PROBE_TIMEOUT = 2 +PROBE_TIMEOUT = 3 NETWORK_OPS_TIMEOUT = 10 LOGGER = logging.getLogger(__name__) MTOR_MIN_INTERVAL = 10 @@ -77,7 +77,7 @@ async def _probe(self) -> None: @classmethod async def initialize(cls, zigpy_config: Dict) -> "EZSP": - """Return initialized EZSP instance. """ + """Return initialized EZSP instance.""" ezsp = cls(zigpy_config[CONF_DEVICE]) await ezsp.connect() await ezsp.reset() @@ -219,9 +219,14 @@ def connection_lost(self, exc): def enter_failed_state(self, error): """UART received error frame.""" - LOGGER.error("NCP entered failed state. Requesting APP controller restart") - self.close() - self.handle_callback("_reset_controller_application", (error,)) + if self._callbacks: + LOGGER.error("NCP entered failed state. Requesting APP controller restart") + self.close() + self.handle_callback("_reset_controller_application", (error,)) + else: + LOGGER.info( + "NCP entered failed state. No application handler registered, ignoring..." + ) def __getattr__(self, name: str) -> Callable: if name not in self._protocol.COMMANDS: diff --git a/bellows/thread.py b/bellows/thread.py index d9e1c939..9c25f2f4 100644 --- a/bellows/thread.py +++ b/bellows/thread.py @@ -8,7 +8,7 @@ class EventLoopThread: - """ Run a parallel event loop in a separate thread """ + """Run a parallel event loop in a separate thread.""" def __init__(self): self.loop = None diff --git a/bellows/types/named.py b/bellows/types/named.py index 6e3c11be..30ea9a9e 100644 --- a/bellows/types/named.py +++ b/bellows/types/named.py @@ -1141,7 +1141,7 @@ class EzspSourceRouteOverheadInformation(basic.enum8): class EmberKeyData(basic.fixed_list(16, basic.uint8_t)): - """A 128-bit key. """ + """A 128-bit key.""" class EmberCertificateData(basic.fixed_list(48, basic.uint8_t)): diff --git a/bellows/zigbee/application.py b/bellows/zigbee/application.py index c7ee6634..3e5f119d 100644 --- a/bellows/zigbee/application.py +++ b/bellows/zigbee/application.py @@ -145,10 +145,13 @@ async def startup(self, auto_form=False): output_clusters=[zigpy.zcl.clusters.security.IasZone.cluster_id] ) - brd_manuf, brd_name, version = await self._ezsp.get_board_info() - LOGGER.info("EZSP Radio manufacturer: %s", brd_manuf) - LOGGER.info("EZSP Radio board name: %s", brd_name) - LOGGER.info("EmberZNet version: %s", version) + try: + brd_manuf, brd_name, version = await self._ezsp.get_board_info() + LOGGER.info("EZSP Radio manufacturer: %s", brd_manuf) + LOGGER.info("EZSP Radio board name: %s", brd_name) + LOGGER.info("EmberZNet version: %s", version) + except EzspError as exc: + LOGGER.info("EZSP Radio does not support getMfgToken command: %s", str(exc)) v = await ezsp.networkInit() if v[0] != t.EmberStatus.SUCCESS: diff --git a/tests/test_ezsp.py b/tests/test_ezsp.py index 23ac5b36..77b2d252 100644 --- a/tests/test_ezsp.py +++ b/tests/test_ezsp.py @@ -241,12 +241,22 @@ def test_connection_lost(ezsp_f): async def test_enter_failed_state(ezsp_f): ezsp_f.stop_ezsp = MagicMock(spec_set=ezsp_f.stop_ezsp) - ezsp_f.handle_callback = MagicMock(spec_set=ezsp_f.handle_callback) + cb = MagicMock(spec_set=ezsp_f.handle_callback) + ezsp_f.add_callback(cb) ezsp_f.enter_failed_state(sentinel.error) await asyncio.sleep(0) assert ezsp_f.stop_ezsp.call_count == 1 - assert ezsp_f.handle_callback.call_count == 1 - assert ezsp_f.handle_callback.call_args[0][1][0] == sentinel.error + assert cb.call_count == 1 + assert cb.call_args[0][1][0] == sentinel.error + + +async def test_no_close_without_callback(ezsp_f): + ezsp_f.stop_ezsp = MagicMock(spec_set=ezsp_f.stop_ezsp) + ezsp_f.close = MagicMock(spec_set=ezsp_f.close) + ezsp_f.enter_failed_state(sentinel.error) + await asyncio.sleep(0) + assert ezsp_f.stop_ezsp.call_count == 0 + assert ezsp_f.close.call_count == 0 @patch.object(ezsp.EZSP, "reset", new_callable=AsyncMock)