Home Assistant addon for phone number resolution with Fritz!Box Callmonitor integration.
Unknown phone numbers are resolved through a configurable multi-stage adapter chain (Chain-of-Responsibility pattern). Web lookup results are cached in SQLite. All calls are recorded in a persistent call history.
The system uses a symmetric adapter concept across three layers:
┌─────────────────────────────────────────────────────────┐
│ INPUT ADAPTERS │
│ Fritz Callmonitor (TCP) │ REST API │ MQTT (opt.) │
│ │
│ ┌─────▼────────────▼────┐ │
│ │ Event Normalizer │ │
│ └─────────┬─────────────┘ │
│ │ │
│ ┌────────────────▼─────────────────┐ │
│ │ RESOLVER (Chain-of-Resp.) │ │
│ │ JSON → SQLite → Tellows → │ │
│ │ DasTelefonbuch → Klartelbuch │ │
│ └────────────────┬─────────────────┘ │
│ │ │
│ ┌─────────▼───────────┐ │
│ │ OUTPUT ADAPTERS │ │
│ Call Log (SQLite) │ HA Webhook │ MQTT Publisher │
└─────────────────────────────────────────────────────────┘
| Layer | Adapter | Description |
|---|---|---|
| Input | fritz |
TCP connection to Fritz!Box Callmonitor (port 1012) with auto-reconnect |
rest |
Manual trigger via REST API | |
mqtt |
MQTT subscriber (optional) | |
| Resolver | json_file |
Static contact list from a JSON file |
sqlite |
SQLite contact database (manageable via GUI) | |
tellows |
Spam score via tellows.de (web scraping + cache) | |
dastelefon |
Reverse lookup via dasTelefonbuch.de (web scraping + cache) | |
klartelbuch |
Reverse lookup via klartelefonbuch.de (web scraping + cache) | |
| Output | call_log |
All events stored in SQLite history |
ha_webhook |
POST to Home Assistant webhook | |
mqtt |
Publish to MQTT topics (optional) |
All adapters can be individually enabled or disabled. The resolver order determines priority — the first match wins.
- Add this repository as a custom repository in Home Assistant
- Install the "Phone Logger" addon
- Configure the addon settings
- Enable Fritz!Box Callmonitor: dial
#96*5*on any connected phone
# Install dependencies
uv sync
# Start server (Fritz disabled, REST input only)
PHONE_LOGGER_CONFIG=config.dev.yaml uv run python -m src.main
# Run tests
uv run pytest -vThe application will be available at http://localhost:8080.
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/resolve/{number} |
Resolve a phone number through the chain (404 if unknown) |
POST |
/api/trigger/{number} |
Manually trigger a call event through the pipeline |
GET |
/api/contacts |
List all SQLite contacts |
POST |
/api/contacts |
Create a contact |
PUT |
/api/contacts/{number} |
Update a contact |
DELETE |
/api/contacts/{number} |
Delete a contact |
GET |
/api/calls |
Call history (paginated) |
GET |
/api/cache |
List cache entries |
DELETE |
/api/cache/{number} |
Delete a cache entry |
POST |
/api/cache/cleanup |
Remove expired cache entries |
GET |
/api/config |
Current adapter configuration |
The GUI is accessible via HA Ingress and provides four tabs:
- Contacts — CRUD management of SQLite contacts with search
- Calls — Call history with direction, resolution status, and pagination
- Cache — View, delete, and clean up web service cache entries
- Configuration — View active adapter configuration and test number resolution
Adapter order and settings are configured via HA addon options or a YAML file:
fritz:
host: "192.168.178.1"
port: 1012
resolver_adapters:
- name: "json_file"
enabled: true
config:
path: "contacts.json"
- name: "sqlite"
enabled: true
- name: "tellows"
enabled: true
config:
ttl_days: 7
- name: "dastelefon"
enabled: true
config:
ttl_days: 30
output_adapters:
- name: "call_log"
enabled: true
- name: "ha_webhook"
enabled: trueEach web scraping adapter has a configurable TTL (time-to-live) in days. Expired entries are automatically refreshed on the next lookup or can be manually cleaned up via the GUI or API.
number (PK), name, tags[], notes, spam_score (1-10), source, last_seen, created_at, updated_at
number + adapter (PK), result_json, cached_at, ttl_days
id (PK), number, direction (inbound/outbound), event_type (ring/call/connect/disconnect),
resolved_name, source, timestamp
When a call is resolved, the ha_webhook adapter sends a POST request:
{
"number": "+491234567890",
"direction": "inbound",
"event_type": "ring",
"timestamp": "2026-03-18T10:15:00",
"resolved": true,
"name": "Max Mustermann",
"tags": ["Familie"],
"spam_score": null,
"is_spam": false,
"resolver_source": "sqlite"
}phone-logger/
├── config.yaml # HA addon manifest
├── build.yaml # Multi-arch build config
├── Dockerfile
├── pyproject.toml # Dependencies (uv)
├── uv.lock
├── config.dev.yaml # Local development configuration
├── src/
│ ├── main.py # Entry point, FastAPI lifecycle
│ ├── config.py # Load YAML/JSON configuration
│ ├── core/
│ │ ├── event.py # CallEvent, ResolveResult models
│ │ └── pipeline.py # Input → Resolve → Output orchestration
│ ├── adapters/
│ │ ├── base.py # Abstract base classes
│ │ ├── input/ # fritz, rest, mqtt_sub
│ │ ├── resolver/ # chain, json_file, sqlite_db, tellows, dastelefon, klartelbuch
│ │ └── output/ # call_log, ha_webhook, mqtt_pub
│ ├── api/
│ │ ├── app.py # FastAPI app factory
│ │ ├── models.py # Pydantic request/response models
│ │ └── routes/ # resolve, contacts, calls, cache, config
│ ├── db/
│ │ ├── database.py # Async SQLite wrapper
│ │ └── schema.sql # Database schema
│ └── gui/
│ ├── routes.py # Jinja2 template routes
│ ├── static/css/ # Stylesheet
│ └── templates/ # base, contacts, calls, cache, config
└── tests/
├── test_resolver_chain.py
├── test_fritz_parser.py
└── test_api.py
MIT