python-afsapi
Asynchronous Python implementation of the Frontier Silicon API
- This project was started in order to embed Frontier Silicon devices in Home Assistant (https://home-assistant.io/)
- Inspired by:
- https://github.com/flammy/fsapi/
- https://github.com/tiwilliam/fsapi
- https://github.com/p2baron/fsapi
Runtime dependency:
- aiohttp
- defusedxml
Development workflow (uv + ruff):
uv sync --group dev
uv run ruff check .
uv run pytest -q
Device recording tests (pytest-recording)
The repository includes integration tests that can record and replay real responses from the development radio.
- Tests are in
tests/test_device_recordings.py(read calls) andtests/test_device_recordings_write.py(write calls with state restore). - Cassettes are stored under
tests/cassettes/. - Device tests replay cassettes by default (
pytestruns them with no live device access). - Live/recording runs require both
AFSAPI_DEVICE_IPandAFSAPI_DEVICE_PIN.
Set device IP + PIN and record fresh cassettes:
AFSAPI_DEVICE_IP="192.168.1.183" AFSAPI_DEVICE_PIN="1234" uv run pytest tests/test_device_recordings.py tests/test_device_recordings_write.py --run-device-tests --record-mode=once -q
Re-record all cassettes:
AFSAPI_DEVICE_IP="192.168.1.183" AFSAPI_DEVICE_PIN="1234" uv run pytest tests/test_device_recordings.py tests/test_device_recordings_write.py --run-device-tests --record-mode=all -q
Run default tests without touching the device:
uv run pytest -q
Usage
Navigation list readiness
nav_list() and get(Nodes.nav_list) collect the complete directory before
returning items. Some radios need additional time after enabling navigation or
changing folders: list reads can raise FSNodeBlockedError or return an
FS_OK loading placeholder with item type 255 (sometimes without a name).
These two conditions trigger up to 8 complete read attempts, separated by
1 second: at most 7 seconds of added retry sleeps, not an overall
wall-clock timeout. HTTP request timeouts and time spent reading all pages are
additional. A successful first read incurs no extra delay.
Each retry starts reading from the first page and discards the incomplete
attempt; partial results are never yielded or duplicated. If the last read is
still blocked, its FSNodeBlockedError is propagated. A persistent loading
placeholder raises FSNodeBlockedError with a loading-specific message.
Other errors and cancellation propagate without readiness retries. Navigation
SET commands, other endpoints, and the low-level handle_list() iterator keep
their existing behavior; no navigation state changes are replayed by this retry
policy. A genuinely empty list remains an empty list.
This handles delayed readiness, not changes made by other clients to the radio's global navigation state, nor pagination/cadence limits on very large station directories.
import asyncio
from afsapi import AFSAPI
URL = 'http://192.168.1.XYZ:80/device'
PIN = 1234
TIMEOUT = 1 # in seconds
async def test():
afsapi = await AFSAPI.create(URL, PIN, TIMEOUT)
print(f'Set power succeeded? - {await afsapi.set_power(True)}' )
print(f'Power on: {await afsapi.get_power()}')
print(f'Friendly name: {await afsapi.get_friendly_name()}')
for mode in await afsapi.get_modes():
print(f'Available Mode: {mode}')
print(f'Current Mode: {await afsapi.get_mode()}')
for equaliser in await afsapi.get_equalisers():
print(f'Equaliser: {equaliser}')
print(f'EQ Preset: {await afsapi.get_eq_preset()}' )
for preset in await afsapi.get_presets():
print(f"Preset: {preset}")
print(f'Set power succeeded? - {await afsapi.set_power(False)}')
print(f'Set sleep succeeded? - {await afsapi.set_sleep(10)}')
print(f'Sleep: {await afsapi.get_sleep()}')
print(f'Get power {await afsapi.get_power()}' )
loop = asyncio.new_event_loop()
loop.run_until_complete(test())
Metadata
Release files for afsapi 1.0.3
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| afsapi-1.0.3.tar.gz | 145.6 kB | Without provenance | Details for afsapi-1.0.3.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| afsapi-1.0.3-py3-none-any.whl | Python 3 | none | any | Details for afsapi-1.0.3-py3-none-any.whl |
Total release size: 169.9 kB
Release files / afsapi-1.0.3.tar.gz
| Download URL | afsapi-1.0.3.tar.gz |
|---|---|
| Size | 145.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
83049a396799b6340c358383704008f5735d08010d36cb59f6922c58157b1e91
|
|
BLAKE2b-256 checksum How to use checksums |
c684adcdd85fa574704a521aa6c1613cd91013274c3fa9491833300d44835dd1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Release files / afsapi-1.0.3-py3-none-any.whl
| Download URL | afsapi-1.0.3-py3-none-any.whl |
|---|---|
| Size | 24.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e3f94abac18a0fe51041fae7036f6e2c6b96c55444de0aec7673261831b0fb3d
|
|
BLAKE2b-256 checksum How to use checksums |
798595d9397c90e1aebcf604702070c4196da917c87efded981546f278ba5567
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Security for afsapi 1.0.3
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.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.