Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
509fed5
Packaging (#26)
puddly Feb 9, 2023
366644b
Add Python matcher
puddly Feb 9, 2023
b7580fd
Add a simple unit test
puddly Feb 9, 2023
a716253
Use correct `dependencies` in `pyproject.toml`
puddly Feb 9, 2023
c521ecb
Disable pytest `fail-under`, for now
puddly Feb 9, 2023
7358ca7
Set version only during release
puddly Feb 9, 2023
fff2064
Only publish to PyPI on release
puddly Feb 9, 2023
4659040
Properly include `readme` in `pyproject.toml`
puddly Feb 9, 2023
87d0264
Use `PYPI_API_TOKEN` secret
puddly Feb 9, 2023
142c39c
Update package installation command in README
puddly Feb 9, 2023
de9b462
Parse Hue SBL images properly (#23)
puddly Feb 9, 2023
125b537
OTA index file generation (#28)
puddly Feb 14, 2023
d380e1f
Reconstruct OTA images from PCAP files (#29)
puddly Mar 31, 2023
54ec10f
Utilize zigpy energy scanning API (#31)
puddly Mar 31, 2023
1243cf9
Use the zigpy channel migration API (#36)
puddly Apr 26, 2023
3f6a0ee
Store partial images when the OTA image's size is unknown (#37)
puddly Apr 26, 2023
65fe568
Switch to `setuptools-git-versioning` (#38)
puddly Apr 26, 2023
83199a9
Add zboss support (#40)
DamKast Aug 17, 2023
ab58a1b
Add zigpy_zboss to the RADIO_TO_PACKAGE map (#44)
natexornate Jan 29, 2024
e13497f
Do not double convert `SCHEMA` (#50)
puddly Aug 8, 2024
2f7eec1
Zigpy packet capture interface (#54)
puddly Jan 23, 2025
22d2ccb
Network scanning interface (#55)
puddly Jan 23, 2025
bd6a9df
Update README for network scanning and packet capture (#56)
puddly Jan 23, 2025
0f03551
Drop old `PYTHON_VERSION_DEFAULT` from CI (#60)
TheJulianJES Dec 4, 2025
1c6d98f
Add `file_size` to OTA index (#59)
TheJulianJES Dec 4, 2025
d05db0f
Fix index generation not wrapping list in `firmwares` (#61)
TheJulianJES Dec 8, 2025
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
16 changes: 16 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
name: CI

on:
push:
pull_request: ~

jobs:
shared-ci:
uses: zigpy/workflows/.github/workflows/ci.yml@main
with:
CODE_FOLDER: zigpy_cli
CACHE_VERSION: 3
PRE_COMMIT_CACHE_PATH: ~/.cache/pre-commit
MINIMUM_COVERAGE_PERCENTAGE: 1
secrets:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
18 changes: 18 additions & 0 deletions .github/workflows/matchers/python.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"problemMatcher": [
{
"owner": "python",
"pattern": [
{
"regexp": "^\\s*File\\s\\\"(.*)\\\",\\sline\\s(\\d+),\\sin\\s(.*)$",
"file": 1,
"line": 2
},
{
"regexp": "^\\s*raise\\s(.*)\\(\\'(.*)\\'\\)$",
"message": 2
}
]
}
]
}
12 changes: 12 additions & 0 deletions .github/workflows/publish-to-pypi.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
name: Publish distributions to PyPI

on:
release:
types:
- published

jobs:
shared-build-and-publish:
uses: zigpy/workflows/.github/workflows/publish-to-pypi.yml@main
secrets:
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
17 changes: 6 additions & 11 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
repos:
- repo: https://github.com/psf/black
rev: 22.3.0
rev: 23.1.0
hooks:
- id: black
args:
- --safe
- --quiet
- repo: https://gitlab.com/pycqa/flake8
rev: 4.0.1
args: ["--safe", "--quiet"]
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: 'v0.0.246'
hooks:
- id: flake8
- repo: https://github.com/PyCQA/isort
rev: 5.10.1
hooks:
- id: isort
- id: ruff
args: ["--fix"]
70 changes: 69 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Zigbee tools into a single binary.
## Installation

```console
$ pip install git+https://github.com/zigpy/zigpy-cli.git
$ pip install zigpy-cli
```

## Usage
Expand Down Expand Up @@ -137,6 +137,42 @@ Some devices (like older Aqara sensors) may not migrate.
$ zigpy radio znp /dev/ttyUSB0 change-channel --channel 25
```

## Network scan

On supported radios, you can perform an active beacon scan for nearby 802.15.4 networks:

```console
$ zigpy radio ezsp /dev/ttyUSB0 network-scan --channels 11,15,20,25 --duration-exponent 3
channel: 11, network: 0x1D13 (00:07:81:00:0e:e9:d8:9f), permitting joins: 1, nwk update id: 0, lqi: 180, rssi: -66
channel: 11, network: 0x2857 (00:07:81:00:fc:9e:ef:95), permitting joins: 0, nwk update id: 0, lqi: 224, rssi: -55
channel: 11, network: 0x08C7 (00:07:81:00:50:d2:be:2e), permitting joins: 0, nwk update id: 0, lqi: 216, rssi: -57
channel: 15, network: 0x2ABB (00:07:81:00:c5:10:10:4b), permitting joins: 0, nwk update id: 0, lqi: 212, rssi: -58
Scanning channel 15
```

## Packet capture

On supported radios, you can capture packets and pipe the PCAP output to Wireshark:

```console
$ zigpy radio ezsp /dev/cu.SLAB_USBtoUART14 packet-capture -c 12,13,14,26 --interleave --channel-hop-period 1 -o - | wireshark -k -S -i -
```

If you have multiple adapters, you can capture with multiple interfaces concurrently:

```console
(
zigpy radio ezsp /dev/cu.SLAB_USBtoUART packet-capture -c 11 --interleave -o - &
zigpy radio ezsp /dev/cu.SLAB_USBtoUART8 packet-capture -c 15 --interleave -o - &
zigpy radio ezsp /dev/cu.SLAB_USBtoUART10 packet-capture -c 20 --interleave -o - &
zigpy radio ezsp /dev/cu.SLAB_USBtoUART13 packet-capture -c 25 --interleave -o - &
zigpy radio ezsp /dev/cu.SLAB_USBtoUART14 packet-capture -c 12,13,14,26 --interleave --channel-hop-period 1 -o - &
zigpy radio ezsp /dev/cu.SLAB_USBtoUART17 packet-capture -c 16,17,18,19 --interleave --channel-hop-period 1 -o - &
zigpy radio ezsp /dev/cu.SLAB_USBtoUART18 packet-capture -c 21,22,23,24 --interleave --channel-hop-period 1 -o - &
wait
) | zigpy pcap interleave-combine -o - | wireshark -k -S -i -
```

# OTA
## Display basic information about OTA files
```console
Expand All @@ -155,6 +191,38 @@ $ zigpy ota dump-firmware 10047227-1.2-TRADFRI-cv-cct-unified-2.3.050.ota.ota.si
Ember Version: 6.3.1.1
```

## Generate OTA index files

Create a JSON index for a given directory of firmwares:

```console
$ zigpy ota generate-index --ota-url-root="https://example.org/fw" path/to/firmwares/**/*.ota
2023-02-14 12:02:03.532 ubuntu zigpy_cli.ota INFO Parsing path/to/firmwares/fw/test.ota
2023-02-14 12:02:03.533 ubuntu zigpy_cli.ota INFO Writing path/to/firmwares/fw/test.ota
[
{
"binary_url": "https://example.org/fw/test.ota",
"file_version": 1762356,
"image_type": 1234,
"manufacturer_id": 5678,
"changelog": "",
"checksum": "sha3-256:1ddaa649eb920dea9e5f002fe0d1443cc903ac0c1b26e7ad2c97b928edec2786"
},
...
```

## Reconstruct an OTA image from a series of packet captures

Requires the `tshark` binary to be available.

```console
$ zigpy ota reconstruct-from-pcaps --add-network-key aa:bb:cc:dd:ee:ff:00:11:22:33:44:55:66:77:88:99 --output-root ./extracted/ *.pcap
Constructing image type=0x298b, version=0x00000005, manuf_code=0x115f: 157424 bytes
2023-02-22 03:39:51.406 ubuntu zigpy_cli.ota ERROR Missing 48 bytes starting at offset 0x0000ADA0: filling with 0xAB
2023-02-22 03:39:51.406 ubuntu zigpy_cli.ota ERROR Missing 48 bytes starting at offset 0x000106B0: filling with 0xAB
Constructing image type=0x298b, version=0x00000009, manuf_code=0x115f: 163136 bytes
```


# PCAP
## Re-calculate the FCS on a packet capture
Expand Down
61 changes: 61 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
[build-system]
requires = ["setuptools>=61.0.0", "wheel", "setuptools-git-versioning<2"]
build-backend = "setuptools.build_meta"

[project]
name = "zigpy-cli"
dynamic = ["version"]
description = "Unified command line interface for zigpy radios"
urls = {repository = "https://github.com/zigpy/zigpy-cli"}
authors = [
{name = "puddly", email = "[email protected]"}
]
readme = "README.md"
license = {text = "GPL-3.0"}
requires-python = ">=3.8"
dependencies = [
"click",
"coloredlogs",
"scapy",
"zigpy>=0.75.0",
"bellows>=0.43.0",
"zigpy-deconz>=0.21.0",
"zigpy-xbee>=0.18.0",
"zigpy-zboss>=1.1.0",
"zigpy-zigate>=0.11.0",
"zigpy-znp>=0.11.1"
]

[tool.setuptools.packages.find]
exclude = ["tests", "tests.*"]

[project.optional-dependencies]
testing = [
"pytest>=7.1.2",
"pytest-asyncio>=0.19.0",
"pytest-timeout>=2.1.0",
"pytest-mock>=3.8.2",
"pytest-cov>=3.0.0",
]

[tool.setuptools-git-versioning]
enabled = true

[project.scripts]
zigpy = "zigpy_cli.__main__:cli"


[tool.ruff]
select = [
# Pyflakes
"F",
# Pycodestyle
"E",
"W",
# isort
"I001"
]
src = ["zigpy_cli", "tests"]

[tool.ruff.isort]
known-first-party = ["zigpy_cli", "tests"]
5 changes: 5 additions & 0 deletions requirements_test.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
coverage[toml]
pytest
pytest-asyncio
pytest-cov
pytest-timeout
34 changes: 0 additions & 34 deletions setup.cfg

This file was deleted.

42 changes: 3 additions & 39 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,40 +1,4 @@
import pathlib
import setuptools

from setuptools import setup, find_packages

import zigpy_cli

setup(
name="zigpy-cli",
version=zigpy_cli.__version__,
description="Unified command line interface for zigpy radios",
long_description=(pathlib.Path(__file__).parent / "README.md").read_text(),
long_description_content_type="text/markdown",
url="https://github.com/zigpy/zigpy-cli",
author="puddly",
author_email="[email protected]",
license="GPL-3.0",
entry_points={"console_scripts": ["zigpy=zigpy_cli.__main__:cli"]},
packages=find_packages(exclude=["tests", "tests.*"]),
install_requires=[
"click",
"coloredlogs",
"scapy",
"zigpy>=0.48.1",
"bellows>=0.34.3",
"zigpy-deconz>=0.18.0",
"zigpy-znp>=0.8.0",
],
extras_require={
# [all] pulls in all radio libraries
"testing": [
"pytest>=5.4.5",
"pytest-asyncio>=0.12.0",
"pytest-timeout",
"pytest-mock",
"pytest-cov",
"coveralls",
'asynctest; python_version < "3.8.0"',
],
},
)
if __name__ == "__main__":
setuptools.setup()
16 changes: 16 additions & 0 deletions tests/test_common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import pytest

from zigpy_cli.common import HEX_OR_DEC_INT


@pytest.mark.parametrize(
"unparsed,parsed",
[
("0x1234", 0x1234),
("1234", 1234),
("0xA", 0xA),
],
)
def test_hex_or_dec_int(unparsed, parsed):
assert HEX_OR_DEC_INT.convert(unparsed, None, None) == parsed
assert HEX_OR_DEC_INT.convert(parsed, None, None) == parsed
27 changes: 0 additions & 27 deletions tox.ini

This file was deleted.

1 change: 0 additions & 1 deletion zigpy_cli/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +0,0 @@
__version__ = "0.0.1"
2 changes: 1 addition & 1 deletion zigpy_cli/__main__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import zigpy_cli.database # noqa: F401
import zigpy_cli.ota # noqa: F401
import zigpy_cli.pcap # noqa: F401
import zigpy_cli.radio # noqa: F401
import zigpy_cli.database # noqa: F401
from zigpy_cli.cli import cli # noqa: F401
7 changes: 4 additions & 3 deletions zigpy_cli/cli.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
from __future__ import annotations

import asyncio
import logging
import functools
import logging

import click
import coloredlogs

from zigpy_cli.const import LOG_LEVELS

LOGGER = logging.getLogger(__name__)
ROOT_LOGGER = logging.getLogger()


def click_coroutine(cmd):
Expand All @@ -31,7 +32,7 @@ def cli(verbose):
level_styles["trace"] = level_styles["spam"]

LOGGER.setLevel(log_level)
logging.getLogger().setLevel(log_level)
ROOT_LOGGER.setLevel(log_level)

coloredlogs.install(
fmt=(
Expand All @@ -42,5 +43,5 @@ def cli(verbose):
),
level=log_level,
level_styles=level_styles,
logger=logging.getLogger(),
logger=ROOT_LOGGER,
)
Loading