fritz
Read the Fritz!Box's own view of the network — cleaner and more complete than an ARP scan (real
hostnames, connection type, active/inactive), and the only place the radio-level truth lives.
Two transports in one tool:
- TR-064 (
fritzconnection) — the authoritative host table and guest-WLAN state.
- AVM JSON API (
login_sid.lua + data.lua, stdlib only) — channel, width, DFS exposure,
mesh topology, event log, subsystem load. A slow-WLAN diagnosis needs this one.
uv run handles the dep; the host needs uv (one binary). Read-only by contract — every
request is a GET or an xhr=1 fetch, and no subcommand may be added that writes configuration.
Setup (once)
- On the Fritz!Box: enable TR-064 (Heimnetz → Netzwerk → Netzwerkeinstellungen → "Zugriff für Apps
erlauben"), and create a dedicated user (System → FRITZ!Box-Benutzer) with the "FRITZ!Box Einstellungen"
right. Use that user, not the box password.
- In Bitwarden: an item named by
FRITZ_BW_ITEM (default home-assistant/fritz) carrying the
password — either as a custom field FRITZBOX_PASSWORD or in the item's own password slot.
Provision via Axon setup-secret.sh.
- In the overlay config (
<overlay>/config/home-assistant.vars): FRITZBOX_HOST,
FRITZBOX_USERNAME, and optionally FRITZ_BW_ITEM. An env var of the same name wins per key.
- Point
bw at the vault once: bw config server <vault-url>.
Run
Unlock Bitwarden interactively, then call (the tool reads $BW_SESSION, never the master password):
export BW_SESSION=$(bw unlock --raw)
# TR-064 — the authoritative host table
scripts/fritz devices # active hosts: ip / mac / interface / active / name
scripts/fritz devices --all -j # include inactive, JSON
scripts/fritz guest # guest-WLAN enabled?
# JSON API — what TR-064 cannot tell you
scripts/fritz overview # model, uptime, device count, unmeshed flag, subsystem load
scripts/fritz chan # active channel + width per band, and whether that sits in DFS
scripts/fritz mesh # which AP serves how many clients, and how it backhauls
scripts/fritz log -f wlan # timestamped radar / DFS / channel-change events
scripts/fritz wlan --detail # every WLAN client and its serving AP
scripts/fritz api <page> # raw data.lua page — the escape hatch
Diagnosing "the WiFi is slow" — four commands, in this order: overview (is a subsystem
saturated?) → chan (is 5 GHz sitting in DFS, and how wide?) → log -f wlan (did something
happen, and when?) → mesh (how much traffic crosses the air twice?).
Gotchas
page=overview returns HTML, not JSON, on FRITZ!OS 8.25. The overview payload lives at
page=wStat. Page names drift between firmware versions — that is what fritz api <page> is
for, and why the opinionated subcommands name their source page in the output.
- Ping is not a liveness test for a repeater. A FRITZ!Repeater can serve a dozen clients
happily while returning 100 % packet loss to ICMP. Use
fritz mesh, not ping.
- A connected repeater is not a meshed repeater, and the difference is the whole ballgame.
An unmeshed repeater broadcasts the house SSIDs on the house channels while the box has no
control over it. Clients then ping-pong between it and the box, dropping on every jump.
Check
mesh --raw | grep -c mesh_repeater_no_trusted (must be 0) and whether every AP
appears in mesh with the box as parent. The tell is an AP that is powered and serving
zero clients: the box steers nobody to an AP it does not trust, so the repeater burns
airtime and returns nothing. Fix it before investigating channels, width or DFS — those
investigations are expensive and, in the one case measured here, were all wrong.
- The client-side signature of an unmeshed second AP is RSSI stepping 15–20 dB back and
forth on an unchanged channel and SSID, roughly once a minute, each step with a loss window.
That is two radios, not fading.
netmon/wlanwatch catches it; a spot check never will.
- TR-064 must be enabled on the box and the user needs the "FRITZ!Box Einstellungen" right — without it
get_hosts_info() raises an auth/permission error, not an empty list.
- The box lists its own valid usernames, unauthenticated:
curl -s "http://<host>/login_sid.lua?version=2"
returns a <Users> block. Check there before assuming a login name — it costs nothing and
avoids burning failed attempts against the box's rate limiter.
- Failed logins are rate-limited.
<BlockTime> in the same response counts the lockout down;
the tool refuses to attempt a login while it is non-zero rather than deepening the block.
$BW_SESSION must be set and unlocked. An unlocked Bitwarden desktop app does not unlock
the CLI — they hold separate vault state, and bw status will keep saying locked. The tool
refuses to run without a session rather than hanging on a master-password prompt.
bw serves a stale cache until you bw sync. An item edited in the web vault or another
client reads as empty here until then; check lastSync in bw status before believing a
missing field.
bw unlock --raw on bitwarden-cli v2026.2.0 can emit an empty string in non-interactive
shells (upstream issue #19649) — pin a known-good bw if you get an empty session. Note it
also cannot be driven from a non-TTY: run it in a real terminal and pass the token onward.
- Vaultwarden with a self-signed cert:
export NODE_EXTRA_CA_CERTS=/path/to/ca.pem before bw, or bw
itself hangs before this tool is even reached.
interface_type is the LAN/WLAN discriminator in get_hosts_info() (not a separate field); status
(bool) is active/inactive. This is the discovery signal ARP can't give you (raw MACs, no names).
- Per-client RSSI/MCS/rate is not exposed by
data.lua on 8.25. The association and its
serving AP are; the radio numbers are not. Read those client-side
(system_profiler SPAirPortDataType on macOS) or from the box's support file.
- Values in the vars file may carry inline
# comments. The parser ends a quoted value at
its closing quote and an unquoted one at #; naive quote-stripping swallows the comment into
the value and surfaces much later as an unexplained auth failure.
- Credentials never print. A missing field is reported by name, never by value.
Examples
"What's on my network / any unknown devices?"
→ fritz devices --all → full host table (name, IP, MAC, WLAN vs LAN, active) → scan for unnamed/unexpected MACs.
"Why is the WiFi slow?" — in this order, cheapest and most-often-right first.
→ fritz mesh + mesh --raw | grep -c mesh_repeater_no_trusted → start here. Every AP a
mesh member with the box as parent? An unmeshed one is both a ping-pong source and an airtime
parasite, and re-pairing it is two button presses.
→ fritz mesh again for topology → clients behind a wirelessly-backhauled repeater spend their
traffic twice on the air, and worse if the repeater relays on the box's own channel.
→ fritz log -f wlan → dates the event. Chronic misconfiguration cannot explain "it worked yesterday".
→ fritz wlan --detail → the slowest clients hold the medium longest per byte. Hunt those next.
→ fritz chan → last. 160 MHz on a DFS channel means the network can be evicted by radar;
the non-DFS block is only 80 MHz wide, so 160 MHz and DFS-free cannot both hold. True, and
still the least likely thing to be causing today's complaint.
fritz overview reports a per-subsystem energy share, not utilisation. "WLAN 100 %" means
the radios are powered, not that they are saturated. It cost a session to learn that; do not
cite it as congestion.
"Is the guest WiFi on?"
→ fritz guest → Guest WLAN: enabled|disabled.
1---2name: fritz3description: Queries an AVM Fritz!Box (7590/FRITZ!OS) read-only over two transports — TR-064 via fritzconnection for the authoritative host table, and the AVM JSON API (login_sid.lua + data.lua) for what TR-064 cannot see: active channel and width per band, DFS exposure, mesh topology with per-AP client counts, the timestamped WLAN event log, and subsystem load. Credentials come from a Bitwarden item at runtime, never hardcoded. Use when listing network devices, checking what is on the network, Fritz!Box device discovery, WLAN clients, guest-WiFi status, spotting an unknown device, or diagnosing a slow or unstable WLAN (channel width, DFS/radar eviction, repeater backhaul, airtime saturation). Do not use for Home Assistant entity control (use ha-cli), DNS or ad-blocking (use pihole), local subnet scanning and drift (use netmon), or non-Fritz routers.4---56# fritz78Read the Fritz!Box's own view of the network — cleaner and more complete than an ARP scan (real9hostnames, connection type, active/inactive), and the only place the radio-level truth lives.10Two transports in one tool:1112- **TR-064** (`fritzconnection`) — the authoritative host table and guest-WLAN state.13- **AVM JSON API** (`login_sid.lua` + `data.lua`, stdlib only) — channel, width, DFS exposure,14 mesh topology, event log, subsystem load. **A slow-WLAN diagnosis needs this one.**1516`uv run` handles the dep; the host needs `uv` (one binary). **Read-only by contract** — every17request is a GET or an `xhr=1` fetch, and no subcommand may be added that writes configuration.1819## Setup (once)20211. **On the Fritz!Box:** enable TR-064 (Heimnetz → Netzwerk → Netzwerkeinstellungen → *"Zugriff für Apps22 erlauben"*), and create a dedicated user (System → FRITZ!Box-Benutzer) with the *"FRITZ!Box Einstellungen"*23 right. Use that user, not the box password.242. **In Bitwarden:** an item named by `FRITZ_BW_ITEM` (default `home-assistant/fritz`) carrying the25 password — either as a custom field `FRITZBOX_PASSWORD` or in the item's own password slot.26 Provision via Axon `setup-secret.sh`.273. **In the overlay config** (`<overlay>/config/home-assistant.vars`): `FRITZBOX_HOST`,28 `FRITZBOX_USERNAME`, and optionally `FRITZ_BW_ITEM`. An env var of the same name wins per key.294. **Point `bw` at the vault once:** `bw config server <vault-url>`.3031## Run3233Unlock Bitwarden interactively, then call (the tool reads `$BW_SESSION`, never the master password):3435```bash36export BW_SESSION=$(bw unlock --raw)3738# TR-064 — the authoritative host table39scripts/fritz devices # active hosts: ip / mac / interface / active / name40scripts/fritz devices --all -j # include inactive, JSON41scripts/fritz guest # guest-WLAN enabled?4243# JSON API — what TR-064 cannot tell you44scripts/fritz overview # model, uptime, device count, unmeshed flag, subsystem load45scripts/fritz chan # active channel + width per band, and whether that sits in DFS46scripts/fritz mesh # which AP serves how many clients, and how it backhauls47scripts/fritz log -f wlan # timestamped radar / DFS / channel-change events48scripts/fritz wlan --detail # every WLAN client and its serving AP49scripts/fritz api <page> # raw data.lua page — the escape hatch50```5152**Diagnosing "the WiFi is slow" — four commands, in this order:** `overview` (is a subsystem53saturated?) → `chan` (is 5 GHz sitting in DFS, and how wide?) → `log -f wlan` (did something54happen, and when?) → `mesh` (how much traffic crosses the air twice?).5556## Gotchas5758- **`page=overview` returns HTML, not JSON, on FRITZ!OS 8.25.** The overview payload lives at59 `page=wStat`. Page names drift between firmware versions — that is what `fritz api <page>` is60 for, and why the opinionated subcommands name their source page in the output.61- **Ping is not a liveness test for a repeater.** A FRITZ!Repeater can serve a dozen clients62 happily while returning 100 % packet loss to ICMP. Use `fritz mesh`, not `ping`.63- **A connected repeater is not a meshed repeater, and the difference is the whole ballgame.**64 An unmeshed repeater broadcasts the house SSIDs on the house channels while the box has no65 control over it. Clients then ping-pong between it and the box, dropping on every jump.66 Check `mesh --raw | grep -c mesh_repeater_no_trusted` (must be 0) and whether every AP67 appears in `mesh` with the box as parent. **The tell is an AP that is powered and serving68 zero clients:** the box steers nobody to an AP it does not trust, so the repeater burns69 airtime and returns nothing. Fix it before investigating channels, width or DFS — those70 investigations are expensive and, in the one case measured here, were all wrong.71- **The client-side signature of an unmeshed second AP** is RSSI stepping 15–20 dB back and72 forth on an unchanged channel and SSID, roughly once a minute, each step with a loss window.73 That is two radios, not fading. `netmon/wlanwatch` catches it; a spot check never will.74- **TR-064 must be enabled on the box** and the user needs the *"FRITZ!Box Einstellungen"* right — without it75 `get_hosts_info()` raises an auth/permission error, not an empty list.76- **The box lists its own valid usernames, unauthenticated:** `curl -s "http://<host>/login_sid.lua?version=2"`77 returns a `<Users>` block. Check there before assuming a login name — it costs nothing and78 avoids burning failed attempts against the box's rate limiter.79- **Failed logins are rate-limited.** `<BlockTime>` in the same response counts the lockout down;80 the tool refuses to attempt a login while it is non-zero rather than deepening the block.81- **`$BW_SESSION` must be set and unlocked.** An unlocked Bitwarden **desktop app does not unlock82 the CLI** — they hold separate vault state, and `bw status` will keep saying `locked`. The tool83 refuses to run without a session rather than hanging on a master-password prompt.84- **`bw` serves a stale cache until you `bw sync`.** An item edited in the web vault or another85 client reads as empty here until then; check `lastSync` in `bw status` before believing a86 missing field.87- **`bw unlock --raw` on bitwarden-cli v2026.2.0** can emit an empty string in non-interactive88 shells (upstream issue #19649) — pin a known-good `bw` if you get an empty session. Note it89 also cannot be driven from a non-TTY: run it in a real terminal and pass the token onward.90- **Vaultwarden with a self-signed cert:** `export NODE_EXTRA_CA_CERTS=/path/to/ca.pem` before `bw`, or `bw`91 itself hangs before this tool is even reached.92- **`interface_type` is the LAN/WLAN discriminator** in `get_hosts_info()` (not a separate field); `status`93 (bool) is active/inactive. This is the discovery signal ARP can't give you (raw MACs, no names).94- **Per-client RSSI/MCS/rate is not exposed by `data.lua` on 8.25.** The association and its95 serving AP are; the radio numbers are not. Read those client-side96 (`system_profiler SPAirPortDataType` on macOS) or from the box's support file.97- **Values in the vars file may carry inline `#` comments.** The parser ends a quoted value at98 its closing quote and an unquoted one at ` #`; naive quote-stripping swallows the comment into99 the value and surfaces much later as an unexplained auth failure.100- **Credentials never print.** A missing field is reported by name, never by value.101102## Examples103104**"What's on my network / any unknown devices?"**105→ `fritz devices --all` → full host table (name, IP, MAC, WLAN vs LAN, active) → scan for unnamed/unexpected MACs.106107**"Why is the WiFi slow?"** — in this order, cheapest and most-often-right first.108→ `fritz mesh` + `mesh --raw | grep -c mesh_repeater_no_trusted` → **start here.** Every AP a109mesh member with the box as parent? An unmeshed one is both a ping-pong source and an airtime110parasite, and re-pairing it is two button presses.111→ `fritz mesh` again for topology → clients behind a wirelessly-backhauled repeater spend their112traffic twice on the air, and worse if the repeater relays on the box's own channel.113→ `fritz log -f wlan` → dates the event. Chronic misconfiguration cannot explain "it worked yesterday".114→ `fritz wlan --detail` → the slowest clients hold the medium longest per byte. Hunt those next.115→ `fritz chan` → **last.** 160 MHz on a DFS channel means the network can be evicted by radar;116the non-DFS block is only 80 MHz wide, so 160 MHz and DFS-free cannot both hold. True, and117still the least likely thing to be causing today's complaint.118119> `fritz overview` reports a per-subsystem **energy** share, not utilisation. "WLAN 100 %" means120> the radios are powered, not that they are saturated. It cost a session to learn that; do not121> cite it as congestion.122123**"Is the guest WiFi on?"**124→ `fritz guest` → `Guest WLAN: enabled|disabled`.