devicectl-core
Shared building blocks for programs that drive one device at a time: a subcommand table, a progress protocol, a serialising worker that owns the one connection a device allows, and a build-free browser UI -- its HTTP server, its event stream and the design system it is drawn with.
It exists because two such programs -- alfenctl for an Alfen
wallbox on the network and jkctl for a JK BMS on an RS485 bus --
turned out to be the same program with different protocols underneath, and
the second one was produced by copying the first. Line for line, after
normalising the two names, their frontend linters agreed on 528 lines out
of 529 and their event broadcasters on 161 out of 167. The drift had begun
anyway: fixes were being made on one side only. This package is the part
that was the same, with one name each.
What is here
| Module | What it holds |
|---|---|
devicectl.errors |
DeviceError, the root of every expected failure |
devicectl.report |
Reporter, Wait, SILENT |
devicectl.progress |
the stderr progress bar and the live line |
devicectl.doctor |
Finding, Report, finding_json, and the three weights |
devicectl.fields |
FieldSpec, one description of a setting for all its audiences |
devicectl.paths |
config_dir, the per-user config directory by platform |
devicectl.cli.command |
Command, Need, Handler |
devicectl.cli.exits |
the exit codes that mean the same thing everywhere |
devicectl.cli.target |
first_set, the one-line precedence rule |
devicectl.web.worker |
Worker, the one thread that owns the one connection |
devicectl.web.events |
Broadcaster, Subscription, Event |
devicectl.web.http |
Request, Response, ApiError, Route, the upload spool |
devicectl.web.server |
serve, Branding, parse_listen, and the four guards |
devicectl.devtools |
frontlint, htmcheck and rendercheck, run as python -m |
web/static/ |
the design system (core.css) and the shared widget library, over the vendored Preact + htm |
What is deliberately not here
Any runtime dependency. Everything is standard library, so a program that depends on this pays for its own protocol layer and nothing else -- and the package stays installable on a Raspberry Pi, where a dependency with no armv7 wheel is a compiler run rather than a download.
Anything that knows what a device is. No transport, no retry policy, no register catalog. Retry in particular does not generalise: one program retries at the authentication layer (a 401 means log in again), the other at the protocol layer (a framing fault is not a Modbus exception), and a shared abstraction over the two would describe neither.
Using it
from devicectl.cli.command import Command, Need
from devicectl.errors import DeviceError
from devicectl.web import http
class WidgetError(DeviceError):
"""Anything this program refuses to do, in one line."""
def get_state(ctx: "Context", req: http.Request) -> http.Response:
return http.ok({"ready": ctx.worker.ready})
ROUTES: dict[tuple[str, str], http.Route["Context"]] = {
("GET", "/api/state"): http.Route(get_state),
}
The frontend checks take the static root to check:
python -m devicectl.devtools.frontlint src/widgetctl/web/static
python -m devicectl.devtools.htmcheck src/widgetctl/web/static
htmcheck runs each html template through the real vendored parser
inside V8, so it needs an engine. That engine is ~80 MB and publishes no
armv7 wheel, so it lives in a dependency group of its own and the check
skips itself, saying so, without it:
uv sync --group browser
Development
uv sync
uv run ruff check . && uv run ruff format --check . && uv run ty check && uv run pytest
Licence
EUPL-1.2, except the vendored Preact + htm bundle, which is MIT -- see
NOTICE.
Release files for devicectl-core 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| devicectl_core-0.1.0.tar.gz | 232.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| devicectl_core-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 431.4 kB
Release files / devicectl_core-0.1.0.tar.gz
| Download URL | devicectl_core-0.1.0.tar.gz |
|---|---|
| Size | 232.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
8f02dafe305f9662c854b50a71db310fd5cd1e59ea10fcc3a6234a239460bad3
|
|
BLAKE2b-256 checksum How to use checksums |
662e2becc045eaa817820e114977e02433b523c9e88b2b8a85cf4f59d8872127
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 26, 2026.
Transparency logRelease files / devicectl_core-0.1.0-py3-none-any.whl
| Download URL | devicectl_core-0.1.0-py3-none-any.whl |
|---|---|
| Size | 198.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
b67bb1f8ea3c5692a62f278dbb20faf3e2ad16e2c17e100cebe976e6957321c3
|
|
BLAKE2b-256 checksum How to use checksums |
5dc4bbfb98aca86a7e775006b17ae179d52c0aaf47fb14e9532ede40cd9ea044
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 26, 2026.
Transparency log