A self-hosted phone system for the analog phones in your home — antique Western Electric sets, rotary desk phones, the wall phone in the kitchen — built on Asterisk 20 + PJSIP and packaged as a native Home Assistant add-on with an Ingress dashboard and an AppArmor profile.
Analog handsets plug into a Grandstream GXW4216 V2 FXS gateway; each port becomes a room extension. From any phone you can call any other phone, reach a voice operator that actually understands you, set a wake-up call by speaking the time, page the whole house, control your lights, hear the weather — and, when you want it, dial the outside world over a SIP trunk like it's a cell phone.
One add-on, running inside Home Assistant next to your others. No separate PBX box, no FreePBX, no LAMP stack. Speech recognition runs on-box — nothing leaves your network. G.711 µ-law end to end, so a call never transcodes.
The dashboard in the Home Assistant sidebar — every phone's registration and call state, the trunk registration and speech-engine health, per-phone latency, and one click to ring, patch two rooms together, hang up, transfer, page, or set a wake-up:
The operator console — the same board, full-screen, driven by keystroke. It opens from the Switchboard panel in the Home Assistant sidebar, and also answers over telnet on the Home Assistant machine itself:
- Room-to-room calling between every analog phone in the house.
- A voice operator (dial
0) that hands you off to anything. Say a room name ("Kitchen"), a spoken extension ("one four"), or just what you want — "time", "weather", "wake me up", "lights", "directory", "page the house", "announce". Recognition is whisper.cpp on-box; a confident room name always wins over a feature word, and it re-asks rather than guess wrong. - Directory assistance (dial
411) — say a room to be connected, or "list" to hear every room and extension read out. - Talking clock (dial
41) — an old-style "at the sound of the tone" speaking clock in your local time. - Wake-up calls (dial
42) — say a time ("seven thirty", "quarter past six"); the phone rings you and speaks it back, and can raise a Home Assistant scene, read the local weather, and announce your next calendar event. - Home-automation voice menu (dial
43) — turn your Home Assistant lights on and off by voice, room by room. - Intercom paging (dial
44) — talk out of every handset at once. - Dial-a-status (dial
45) — hear live Home Assistant readings spoken aloud: power/battery, the weather, the state of the house. - Announcements (dial
46) — record a message and play it out your Home Assistant speakers, bracketed by a station chime. A companion HTTP endpoint lets Home Assistant (or another add-on) speak an alert onto a handset — turning any phone, including a WiFi cordless, into an announce target. - An outside line, dialed like a cell phone. Enable a SIP trunk for real
inbound and outbound calls. With direct dial you dial
1+ the 10-digit number with no prefix. Requiring that leading1is what keeps the operator, rooms11and20, and the feature codes dialling instantly — a bare-10 pattern would make each of them look like the start of a phone number and stall on the gateway's inter-digit timer. Rooms12–19are the exception: they are both a complete extension and the first two digits of an 11-digit number, so those wait out the timer. All behind layered toll-fraud protection. Off by default; room-to-room needs no trunk. - A live dashboard in the Home Assistant sidebar (Ingress): every phone's registration and call state, the trunk's registration and the speech engine's health, per-phone latency, one-click test-ring, patch-two-rooms, hang-up, transfer, page, message-waiting, and wake-up controls, and a Lights panel with per-area on/off toggles for every Home Assistant light.
- A full-screen operator console — a switchboard board you drive by keystroke, in the Home Assistant sidebar, over telnet, or through a standalone browser terminal, mirroring the same live signals.
- Proactive health monitoring — the add-on watches every phone's registration and round-trip latency, scores each call's audio quality, tracks the WiFi cordless's battery and signal, and watches the trunk registration — publishing it all as Home Assistant sensors and raising a notification only when something is actually wrong (a whole-gateway outage, a dying battery, a genuinely rough call).
Switchboard is G.711 µ-law on every leg, and it isn't configurable — on purpose. An antique analog handset is narrowband by physics: the carbon/electret element and the two-wire loop top out around 300–3400 Hz, so wrapping it in G.722 or Opus buys nothing the transducer can reproduce. Pinning one codec everywhere means no call ever transcodes — lowest latency, and dial tone, ringback, and fax/modem tones pass through cleanly. See DOCS §13.
- Home Assistant OS / Supervised (this is a Supervisor add-on). Architectures:
amd64,aarch64. - A Grandstream GXW4216 V2 (or compatible) FXS gateway on the same LAN — one FXS port per analog phone. The add-on uses host networking, so the gateway and Home Assistant speak SIP directly with no NAT.
- Optional: a Grandstream WP826 WiFi cordless as a room extension and Home Assistant announce endpoint.
- Optional: a SIP-trunk provider (host, username, password, a DID) for an outside line.
- Optional: Home Assistant media players (for announcements) and lights (for the automation menu and console light control).
- Add the repository to Home Assistant → Settings → Add-ons → Add-on Store →
⋮ → Repositories →
https://github.com/tesseractAZ/Switchboard, then install Switchboard. - Open Configuration and define one entry under
roomsper phone — a uniqueext(2–6 digits), a friendlyname, and a strongsecret(the SIP password that port will use). Change thechange-me-…placeholder secrets. - Start the add-on. The Log tab shows
switchboard-configrendering the configuration, then Asterisk starting. - Provision the gateway
(DOCS §7) so each FXS
port registers — SIP server = your Home Assistant IP, transport UDP, and per port
the User ID / Auth ID = the extension and the password = that room's
secret. - Open the Switchboard panel in the sidebar; each room flips to Registered as its port comes online. Pick up a phone and dial another room. Done.
Changing options and restarting the add-on regenerates the entire Asterisk
configuration — the add-on options are the single source of truth; hand edits to
/etc/asterisk/*.conf are overwritten on every start.
| Dial | Feature | Option |
|---|---|---|
0 |
Voice operator — say a room, an extension, or any feature by name | operator.enabled |
41 |
Talking clock | clock_ext |
42 |
Set / cancel a wake-up call | wakeup_ext |
43 |
Home-automation voice menu (lights) | automation_ext |
44 |
Page all — house-wide intercom | page_ext |
45 |
Dial-a-status (live Home Assistant readings) | status_ext |
46 |
Announce out your Home Assistant speakers | announce_ext |
47 |
Local voice assistant — ask Home Assistant anything (off by default) | assistant_ext |
411 |
Directory assistance | directory_ext |
911 |
Answered with a spoken notice that these phones cannot reach emergency services | always on |
933 |
The FCC's number for testing an emergency line — answered with the same spoken notice as 911, never dialled out. Nothing is read back: there is no carrier registration to read. |
always on |
Every feature code 41–47 and 411 is configurable (*_ext) and can be
disabled (*_enabled); dial 0 (the operator) can be turned off but not
re-assigned. 911 and 933 are always emitted and cannot be moved. On a live call, analog phones
blind-transfer with ## and attended-transfer with *2 (internal destinations
only). The table shows the defaults.
- Asterisk 20 + PJSIP is the only telephony engine (
chan_sipis not used). On every start,switchboard-configregenerates the entire Asterisk configuration from your add-on options, so the running PBX always matches what you declared. - Offline voice: speech-to-text is whisper.cpp (English
base.enmodel, kept resident in RAM with a per-call fallback), and text-to-speech is espeak-ng — both on-box, no cloud, no API key. - The dashboard is a small FastAPI app talking to Asterisk over a loopback-only Manager (AMI) socket, served behind Home Assistant Ingress.
- The health monitors, wake-up scheduler, resident recognizer, and operator console run as separate supervised services under s6-overlay, each idling when its feature is off.
- Built on the Home Assistant Alpine base image (a two-stage build that compiles whisper.cpp from source), under an AppArmor profile and host networking (required for SIP + RTP on your LAN).
Antique analog phones ──FXS──▶ Grandstream GXW4216 V2 ─┐
WP826 WiFi cordless ──────WiFi─────────────────────────┤ each phone = 1 SIP extension
▼
Home Assistant host (LAN, host network)
┌─────────────────────────────────────────┐
│ Add-on: Switchboard │
│ • Asterisk 20 + PJSIP (SIP / RTP) │
│ • Ingress dashboard (FastAPI ⇄ AMI) │
│ • Voice: whisper.cpp STT + espeak TTS │
│ • Health monitors → Home Assistant │
│ • Config generated from add-on options │
└─────────────────────────────────────────┘
│ (optional) SIP trunk
▼
outside line (PSTN)
- Complete system reference — installation, every configuration option, the voice features, gateway and cordless provisioning, the SIP trunk and direct dial, the operator console, health monitoring and sensors, codecs, troubleshooting, and reproducing the system on new hardware.
- Security — the security model, toll-fraud defenses, the accepted LAN-local risks, what you must configure, and how to report an issue.
- Changelog — the full release history.
- Measured behaviour — what the system is observed doing in service, from its own ledgers: call quality, link health, which alarms have ever actually fired.
- Printable manual — a GitHub Release
carries the README + Security + reference assembled into a Word (
.docx) and PDF you can read offline. (Releases v0.69.0 through v0.80.0 shipped without one — the build waited on an event a bot-created release does not emit. Repaired in v0.81.0; every release since carries both files.)
Every version from v0.43.0 on is tagged and released, so git checkout vX.Y.Z
reproduces the exact source that built it. One older version, v0.42.1, was
never tagged: tagging was manual until v0.46.1 and that one was missed. See
DOCS §16, Reproducing on new hardware.
The Ingress dashboard is reachable only from the Home Assistant Supervisor; the
Asterisk Manager socket is loopback-only with a fresh random secret each boot and no
shell-command privilege; the SIP trunk blocks international/premium prefixes and
confines every transfer to internal destinations. The operator console runs in
the Home Assistant sidebar, where your Home Assistant login is the only login and
the port it is served on refuses every connection whose peer is not the Supervisor
— WebSocket upgrades included. Host networking means that port does listen on
your LAN; the guard, not the bind, is what closes it. The two standalone consoles — telnet on
2300 and the browser terminal on 8100 — both default to 127.0.0.1, so neither
is reachable from your LAN as shipped. Neither has authentication of its own that
is on by default, so if you move either bind, put it on a trusted network or
configure console_users for the browser one. Change the default room secrets before
your phones register. Full details in SECURITY.md.
MIT © Eric Paschal.

