DEV Community

Cover image for Building a Screenless VoIP Phone for a Child
Andre Faria
Andre Faria

Posted on Edited on

Building a Screenless VoIP Phone for a Child

This started, as too many projects do, with me watching Instagram Reels. I came across this Reel about someone who had bought a Tin Can phone for her daughter, and the shape of it immediately appealed to me. A familiar physical interface, a modern backend, no apps, no games, and very little nonsense exposed to the child.

The bit that got me was the quick-dial trick. If a child-friendly phone can call specific numbers, those numbers do not have to be normal phone numbers. They can be internal service codes. If those codes reach a PBX, pressing a physical button can do something other than place a call. Like running code or triggering an automation, with the correct harness.

My first instinct was to build the whole system myself and run it inside Home Assistant as I have done before. Then I found Asterisk through the TECH7Fox Asterisk Home Assistant add-on and companion Asterisk Home Assistant integration. That changed the plan from writing a small phone system to configuring a real PBX.

So the design became almost disappointingly old-fashioned.

big-button analogue phone
  -> VoIP ATA
  -> Asterisk
  -> Home Assistant
  -> Telegram
Enter fullscreen mode Exit fullscreen mode

The phone itself stays dumb. That is the point.

Pink analogue handset connected to a Grandstream HT812 V2 ATA

The phone is not the brain

An RJ11 analogue phone does not understand VoIP, Home Assistant, Telegram, automations, or routing. It expects a telephone line to provide dial tone, line voltage, ringing, and an audio path. That is useful. A simple phone is robust, familiar, and child-friendly. The physical interface is the feature. The intelligence lives behind it in two layers.

  • an ATA, which pretends to be a landline
  • a PBX, which decides what each dialled digit means

ATA means analogue telephone adapter. For this build, the important detail is that it needs an FXS port. FXS is the port that powers an analogue phone. FXO connects to a real landline. I used the Grandstream HT812 V2, bought from Amazon Ireland.

The phone is a big-button analogue handset also from Amazon Ireland. Any RJ11 analogue phone should work. The HT812 has two FXS ports, which is more than I need for one handset but useful for testing or a second one later. The HT818 has eight FXS ports, which is lovely for a small hotel phone system and mildly ridiculous for one child phone.

The working version

This is not a full family PBX. I deliberately built the local, default-deny core before adding a SIP trunk for external calls.

Right now the phone is a one-digit interface.

Backlit keypad on the pink analogue handset

1 = record a message for Dad
2 = record a message for Mum
3 = play a short "Wheels on the Bus" clip
4 = emit a Home Assistant trigger event
5 = emit a Home Assistant trigger event
6 = reserved trigger stub
7 = reserved trigger stub
8 = reserved trigger stub
9 = play a test prompt
Enter fullscreen mode Exit fullscreen mode

The child does not need to know those numbers. The physical buttons get labels or photos, and the backend maps each digit to a fixed action. Pressing a button is not really "dialling a number". It is triggering a controlled workflow.

button press -> ATA -> Asterisk -> local action
Enter fullscreen mode Exit fullscreen mode

That is the architectural trick. Do not make the phone clever. Make the backend clever. There is a decent body of work around tangible interaction for children, which is the academic way of saying that sometimes a real object beats another glowing rectangle.

There is something pleasingly ridiculous about the whole thing. Most of the useful ideas here are proper old. PBXs, extension routing, tone signalling, recorded prompts, call contexts. I am bolting Home Assistant onto settled telephony ideas and letting them do what they were always good at.

Configuring the ATA

The HT812 is configured through its own web UI. It works, but it is enormous. Grandstream exposes SIP profiles, codec order, DTMF behaviour, dial plans, NAT settings, provisioning, certificates, call features, and a swamp of P-values. Useful, but not exactly a child-friendly interface for the adult either.

Only a small slice mattered. Profile 1 points at the Home Assistant Asterisk add-on as the SIP server. FXS port 1 uses child-phone as both the SIP user ID and authenticate ID. The password matches the Asterisk pjsip_custom.conf secret. DTMF uses RFC 4733, and the dial plan is { xS0 }, which sends a single digit immediately to Asterisk.

The full ATA walkthrough lives in the companion repository. That is where the web UI detail belongs.

Configuring Asterisk

Asterisk does the routing

The ATA registers to Asterisk as a SIP endpoint called child-phone. Asterisk runs inside Home Assistant using the TECH7Fox add-on. If you want the grown-up version, GÉANT has a useful guide to implementing an IP telephone exchange using Asterisk.

The add-on creates two useful config directories.

/addon_configs/<asterisk_addon_slug>/asterisk/default
/addon_configs/<asterisk_addon_slug>/asterisk/custom
Enter fullscreen mode Exit fullscreen mode

The default directory is regenerated by the add-on. Treat it as reference material. Durable edits go in custom.

For this project, these are the important custom files.

pjsip_custom.conf
extensions.conf
manager.conf
indications.conf
modules.conf
Enter fullscreen mode Exit fullscreen mode

One small Asterisk/PJSIP detail cost me time. The working setup uses child-phone as both the SIP username and the AOR name. Renaming the AOR to something tidy like child-phone-aor broke registration with an AOR '' not found for endpoint 'child-phone' error. Sometimes the ugly name is the correct name. Telephony has opinions and we know none of them.

This is the trimmed shape of the PJSIP config.

[child-phone-auth]
type=auth
auth_type=userpass
username=child-phone
password=CHANGEME_CHILD_PHONE_SECRET

[child-phone]
type=aor
max_contacts=1
remove_existing=yes
remove_unavailable=yes
qualify_frequency=30

[child-phone]
type=endpoint
transport=transport-udp
context=child-phone
disallow=all
allow=alaw
allow=ulaw
aors=child-phone
auth=child-phone-auth
callerid="Child Phone" <201>
dtmf_mode=rfc4733
Enter fullscreen mode Exit fullscreen mode

The important bit is that the AOR block and endpoint block are both named child-phone. PJSIP distinguishes them by type, and the endpoint points back to that AOR with aors=child-phone. The full config is in the Asterisk guide in the companion repo.

Default deny, because children press buttons

The safety model matters more than the telephony. The child phone must not inherit a normal outbound dial plan. In Asterisk, the endpoint is assigned to a dedicated context called child-phone, and that context only contains explicit actions. The real dialplan has this shape.

[child-phone]
exten => 1,1,Answer()
 same => n,Gosub(child-phone-trigger,s,1(1))
 same => n,Playback(/media/asterisk/sounds/custom/leave-dad-secret)
 same => n,Playback(beep)
 same => n,Record(/media/asterisk/messages/dad-secret-${STRFTIME(${EPOCH},,%Y%m%d-%H%M%S)}.wav,0,0,xk)
 same => n,Hangup()

exten => 2,1,Answer()
 same => n,Gosub(child-phone-trigger,s,1(2))
 same => n,Playback(/media/asterisk/sounds/custom/leave-mom-secret)
 same => n,Playback(beep)
 same => n,Record(/media/asterisk/messages/mom-secret-${STRFTIME(${EPOCH},,%Y%m%d-%H%M%S)}.wav,0,0,xk)
 same => n,Hangup()

exten => 3,1,Answer()
 same => n,Gosub(child-phone-trigger,s,1(3))
 same => n,Playback(/media/asterisk/sounds/custom/wheels-on-the-bus)
 same => n,Hangup()

exten => _X!,1,NoOp(REJECTED child-phone dial attempt: ${EXTEN})
 same => n,Congestion(3)
 same => n,Hangup()

[child-phone-trigger]
exten => s,1,UserEvent(ChildPhoneButton,Source: child-phone,Button: ${ARG1})
 same => n,Return()
Enter fullscreen mode Exit fullscreen mode

That last _X! rule is important. It rejects everything that has not been explicitly defined. There is no fallback like "if it looks like a number, send it to the trunk".

The ATA digit map only makes the phone feel responsive by sending one digit immediately. The Asterisk context is the real security boundary. The PBX decides what is allowed.

Recording a message is just another extension

The message feature does not need a separate product.

Asterisk writes recordings here.

/media/asterisk/messages
Enter fullscreen mode Exit fullscreen mode

Prompt audio lives here.

/media/asterisk/sounds/custom
Enter fullscreen mode Exit fullscreen mode

When my son presses 1, Asterisk answers, emits a ChildPhoneButton AMI event, plays a custom prompt, plays a beep, and records until hang-up. Button 2 does the same for his mum.

The flow looks like this.

child presses button 1
phone sends digit to HT812
HT812 sends SIP call to Asterisk
Asterisk records dad-secret-YYYYMMDD-HHMMSS.wav
Home Assistant sees the completed file
Home Assistant sends the WAV to Telegram
Enter fullscreen mode Exit fullscreen mode

The nice thing about this design is that Asterisk does not need to know about Telegram. It records audio. Home Assistant handles delivery.

Home Assistant watches for completed files

The recording delivery path uses Home Assistant's Folder Watcher integration.

The watcher uses this configuration.

Folder: /media/asterisk/messages
Pattern: *.wav
Enter fullscreen mode Exit fullscreen mode

The automation listens for the closed event, not the created event.

triggers:
  - trigger: event
    event_type: folder_watcher
    event_data:
      event_type: closed
conditions:
  - condition: template
    value_template: >
      {{ trigger.event.data.path is string
         and trigger.event.data.path.startswith('/media/asterisk/messages/')
         and trigger.event.data.path.endswith('.wav') }}
Enter fullscreen mode Exit fullscreen mode

That detail matters. Asterisk creates the WAV file before the recording is finished. If Home Assistant sends the file on created, it can race the recorder and upload a partial recording. The closed event fires after Asterisk has finished writing.

The action is just telegram_bot.send_document.

action: telegram_bot.send_document
data:
  file: "{{ trigger.event.data.path }}"
  caption: "New phone message: {{ trigger.event.data.file }}"
Enter fullscreen mode Exit fullscreen mode

The bot token stays in Home Assistant. The repository contains config templates and examples, not live secrets.

Telegram delivery

The Telegram side is intentionally small. I created a bot through BotFather, then created a private broadcast channel for the phone messages and added the bot there. Home Assistant only needs the bot token and destination chat. The real test was a completed WAV recording landing in the channel.

Telegram broadcast channel receiving a Home Assistant test message and a child phone WAV recording

Home Assistant and Asterisk

The TECH7Fox Asterisk add-on runs Asterisk inside Home Assistant and exposes the real config files under the add-on config directory. The HACS Asterisk integration connects over AMI and can expose device state, registration state, connected line, DTMF events, and a generic send_action service.

There was one catch. Quick-dialling a Home Assistant action directly is not quite first-class yet. Asterisk can emit AMI UserEvents, but the integration did not forward those into Home Assistant as normal events. I opened TECH7Fox/asterisk-hass-integration PR #126 for that, and TECH7Fox/asterisk-hass-addons PR #453 so Home Assistant can connect without loosening AMI access more than necessary.

For this build, I split responsibilities this way.

  • Asterisk add-on - the actual PBX and routing engine
  • Asterisk integration - monitoring and Home Assistant event/control bridge

Home Assistant should observe and react. Asterisk should decide what a dialled digit means. That path is lower-level, more deterministic, and less likely to break because a custom integration changed an entity model.

What I Made

The core build is not a full telephone system. It is a local, default-deny phone interface with a handful of deliberately boring behaviours.

Dialled input Behaviour
1 Play Dad prompt, beep, record WAV
2 Play Mum prompt, beep, record WAV
3 Play "Wheels on the Bus" clip
4 EmitChildPhoneButton event and hang up
5 EmitChildPhoneButton event and hang up
6, 7, 8 Emit event, play three beeps, hang up
9 Play test prompt
random multi-digit input Reject with congestion
999 / 112 Reject unless emergency support is intentionally implemented

The HT812 registers successfully as child-phone. Asterisk reports the endpoint as reachable. Recording works. Home Assistant sees completed WAV files and can deliver them to Telegram. That makes the current build useful without pretending it is a normal telephone.

The emergency-call problem

There is one uncomfortable detail. This thing looks like a landline. That means everyone in the house may assume it can call emergency services. If it cannot call 999 or 112, that needs to be explicit. If it can, then emergency support becomes a proper requirement.

  • SIP provider support for Irish emergency calls
  • correct caller ID
  • registered location/address handling
  • UPS for ATA, PBX, switch, router, and internet handoff
  • tested routing
  • fallback if internet or power is down

I am not pretending it is an emergency phone. It is a family communication device and should be labelled accordingly. Half-supporting emergency calls is worse than not supporting them. It creates confidence where there should be caution.

Privacy is part of the build

Recorded child voice messages are private data. Research on internet-connected toys that listen is a useful reminder that child audio should be treated as sensitive by default. For this setup, I use these rules.

  • recordings go only to a private parent-controlled Telegram destination
  • bot tokens and chat IDs stay in Home Assistant secrets/config, not in the public repo
  • live SIP, AMI, Home Assistant, and Telegram credentials are never committed
  • recordings should have a retention policy rather than piling up forever
  • failure should be visible because a missed upload should not silently disappear

The next hardening step is operational rather than architectural. Retention, monitoring, and backup/restore of the ATA and Asterisk config matter more now than more features.

The build, in one page

The current version looks like this.

big-button RJ11 phone
  -> Grandstream HT812 V2 FXS port
  -> Asterisk Home Assistant add-on
  -> child-phone context
  -> local recordings and AMI UserEvents
  -> Home Assistant Folder Watcher / automations
  -> Telegram delivery for completed WAV files
Enter fullscreen mode Exit fullscreen mode

The source config lives at github.com/andremmfaria/child-phone.

This can still become an actual phone later. The missing commercial piece is a SIP trunk from a provider such as VoIPLine Ireland, with numbers purchased separately. That would let Asterisk place and receive calls through the ordinary phone network.

It keeps the child-facing device physical and boring, which is exactly what I want. All the complexity stays in software, where it can be inspected, backed up, changed, and locked down.

Top comments (0)