Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
24 changes: 16 additions & 8 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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: |
Expand Down Expand Up @@ -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: |
Expand Down Expand Up @@ -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: |
Expand Down Expand Up @@ -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: |
Expand Down Expand Up @@ -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: |
Expand Down Expand Up @@ -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: |
Expand Down Expand Up @@ -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: |
Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/psf/black
rev: 20.8b1
rev: 22.3.0
hooks:
- id: black
args:
Expand Down
2 changes: 1 addition & 1 deletion bellows/__init__.py
Original file line number Diff line number Diff line change
@@ -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}"
2 changes: 1 addition & 1 deletion bellows/cli/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
# flake8: noqa
from . import application, backup, dump, ncp, network
from . import application, backup, dump, ncp, network, stream, tone
2 changes: 1 addition & 1 deletion bellows/cli/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
59 changes: 59 additions & 0 deletions bellows/cli/stream.py
Original file line number Diff line number Diff line change
@@ -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)
59 changes: 59 additions & 0 deletions bellows/cli/tone.py
Original file line number Diff line number Diff line change
@@ -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)
9 changes: 9 additions & 0 deletions bellows/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
15 changes: 10 additions & 5 deletions bellows/ezsp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion bellows/thread.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion bellows/types/named.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)):
Expand Down
11 changes: 7 additions & 4 deletions bellows/zigbee/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
16 changes: 13 additions & 3 deletions tests/test_ezsp.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down