Code Documentation¶
General structure¶
fritz_exporter uses some “black magic” (this may be clever, but it makes understanding what’s going on a lot harder…) to manage capabilities of devices. This section will try to make some sense of this code… At least for the author…
Module / File Map¶
A quick orientation before diving in:
fritzexporter/
__main__.py – CLI entry point and main loop (parse_cmdline, main)
fritzdevice.py – FritzDevice, FritzCollector, FritzCredentials
fritzcapabilities.py – FritzCapability (ABC) + all concrete capability classes
+ FritzCapabilities container
fritz_aha.py – XML helper for AHA (smart home) device data
action_blacklists.py – TR-064 service/action pairs that must never be called
data_donation.py – "donate-data" CLI mode: collect & upload device data
exceptions.py – Top-level exceptions
config/
config.py – ExporterConfig, DeviceConfig (attrs @define classes)
+ get_config() factory
exceptions.py – Config-specific exceptions
Startup Flow¶
The entry point is fritzexporter/__main__.py → main().
A
FritzCollectoris created (empty, no devices yet).Configuration is loaded via
get_config(), which reads either a YAML file or environment variables and returns anExporterConfiginstance.For each
DeviceConfigin the config, aFritzDeviceis instantiated. During constructionFritzDeviceconnects to the physical device viafritzconnectionand immediately probes which TR-064 capabilities it supports (see below).Each
FritzDeviceis registered withfritzcollector.register(), which also merges the device’s capabilities into the collector’s global capability set.The
FritzCollectoris registered with the PrometheusREGISTRY.An HTTP server is started (
prometheus_client.start_http_server), and the process enters anasyncioevent loop that runs forever.
When Prometheus scrapes /metrics, FritzCollector.collect() is called. This
method is protected by a re-entrant lock (threading.RLock) so concurrent scrapes
do not interfere with each other.
Configuration System¶
config/config.py provides two attrs @define classes:
DeviceConfigRepresents one Fritz! device. Fields:
hostname,username,password,password_file,name,host_info. Hostname is lowercased automatically via an attrs converter. Validators check password length and that any password file actually exists.ExporterConfigTop-level exporter configuration. Fields:
exporter_port(default 9787),log_level,listen_address,devices(list ofDeviceConfig).
get_config(path) delegates to either _read_config_file() (YAML) or
_read_config_from_env() (environment variables) and then calls
ExporterConfig.from_config(raw_dict).
Both config classes use attrs validators and converters — field values are
validated and coerced at construction time. Config-specific exceptions live in
config/exceptions.py.
The Capability System¶
This is the “black magic” part. Read carefully.
Auto-registration of subclasses
FritzCapability is an abstract base class (ABC) defined in fritzcapabilities.py.
Every concrete capability (e.g. DeviceInfo, WanDSLInterfaceConfig) is a
subclass of it. The __init_subclass__ hook fires automatically when Python loads
each subclass definition and appends the new class to the class-level list
FritzCapability.subclasses. This means no manual registration is needed — just
define the subclass and it is discovered automatically.
FritzCapabilities (plural) — the container
FritzCapabilities is a dict-like wrapper around {class_name: instance} for
every known FritzCapability subclass. It can be constructed in two modes:
Without a device — instantiates all subclasses but leaves every instance’s
presentflag asFalse. Used byFritzCollectorto hold the global, device-agnostic union of capabilities.With a device — same construction, then immediately calls
check_present()which runscheck_capability()on every instance against that specific device.
Capability probing (check_capability)
FritzCapability.check_capability(device) performs a two-stage check:
Static check: each capability declares a
self.requirementslist of(service, action)tuples. The method verifies that each service exists indevice.fc.servicesand that the action is listed in that service’s actions dict.Live call check: even if the static check passes, some Fritz! devices advertise services they do not actually support. The method therefore tries to call each requirement and, if
FritzServiceError,FritzActionError, or similar exceptions are raised, it marks the capabilitypresent = Falseagain.
The present flag of each FritzCapability instance reflects whether that
device supports that capability.
Merging capabilities across devices
When a second (or third…) device is registered via FritzCollector.register(),
FritzCapabilities.merge() is called on the collector’s global capability set.
Merge uses a logical OR: a capability in the global set becomes present = True if
any registered device supports it.
This is what allows the collector to serve a union of all capabilities without needing to know in advance which devices are attached.
The collect loop
When FritzCollector.collect() runs:
It iterates over the global
FritzCapabilities(one entry per capability class,presentmeaning “at least one device supports it”).For each capability
capa:capa.create_metrics()is called — this re-initialises the Prometheus metric family objects, clearing any values from a previous scrape.capa.get_metrics(devices, name)is called. Inside, it iterates over every registered device and callscapa._generate_metric_values(device)only whendevice.capabilities[name].presentisTruefor that device. This is how per-device capability support is respected even though iteration happens at the global level.After all devices have been processed,
capa._get_metric_values()yields the now-populated metric families.
The indirection — global capabilities iterate, but per-device capability flags gate whether values are actually fetched — is the key insight needed to understand the flow.
Additionally, FritzDevice.get_connection_mode() is called per device before the
capability loop to emit a special fritz_connection_mode gauge that detects
DSL/mobile/offline state.
tl;dr¶
FritzCollector has a list of FritzDevices and a global FritzCapabilities
collection representing the union of all capabilities supported by any registered device.
Each FritzDevice has its own FritzCapabilities collection recording which
capabilities that device actually supports.
During collect(), the global set drives iteration; the per-device sets gate whether
values are fetched from each device.
Adding a New Metric / Capability¶
Open
fritzexporter/fritzcapabilities.py.Create a new subclass of
FritzCapability. The name of the class becomes the key used to look up the capability in everyFritzCapabilitiesdict.In
__init__, callsuper().__init__()and then populateself.requirementswith the(service, action)tuples the metric requires. These are TR-064 service/action pairs as exposed byfritzconnection.Implement
create_metrics(self)— create the Prometheus metric family objects and store them inself.metrics[key]. UseGaugeMetricFamilyfor current values andCounterMetricFamilyfor monotonically increasing counters.Implement
_generate_metric_values(self, device)— call the TR-064 actions viadevice.fc.call_action(service, action)and populate the metric families usingself.metrics[key].add_metric(labels, value).Implement
_get_metric_values(self)—yieldeach metric family fromself.metrics.Add tests in
tests/test_fritzcapabilities.pyusing the mock infrastructure intests/fc_services_mock.py. No further registration is needed; the subclass is discovered automatically via__init_subclass__.
Supporting Modules¶
action_blacklists.py
Defines call_blacklist, a list of BlacklistItem(service, action) tuples
representing TR-064 calls that must never be made by the exporter. These include
calls that retrieve persistent configuration data, security keys, WEP keys, phone
book entries, and other sensitive or potentially destructive operations. The blacklist
is used exclusively by the data donation feature.
fritz_aha.py
Provides parse_aha_device_xml(xml_string), a small helper that parses the AHA
(AVM Home Automation) XML format returned for smart home devices. It extracts battery
level and low-battery indicator fields. defusedxml is used instead of the standard
library xml module to prevent XML injection attacks.
data_donation.py
Implements the --donate-data / --upload-data CLI mode. When active, the
exporter connects to the device, iterates over all services and Get* actions
(excluding per-index/per-IP lookups), calls each one, sanitises sensitive fields
using a built-in blacklist plus any user-specified --sanitize arguments, and either
prints or uploads the resulting JSON blob to the project’s collection endpoint. This
data helps the project discover which TR-064 actions are available on different device
models.
exceptions.py
Defines FritzDeviceHasNoCapabilitiesError, raised when a device is connected
successfully but no TR-064 capabilities are detected. Config-specific exceptions are
in config/exceptions.py.