{ lib, ... }: let phones = import ./phones.nix { inherit lib; }; in { options.services.voip = { enable = lib.mkEnableOption "VoIP provisioning (Asterisk + TFTP)"; serverAddress = lib.mkOption { type = lib.types.str; description = "IP address or hostname of this server (used in phone configs and SIP)."; }; ntpServer = lib.mkOption { type = lib.types.str; description = "NTP server for phones. Defaults to serverAddress."; default = ""; }; sipPort = lib.mkOption { type = lib.types.port; default = 5060; }; rtpStart = lib.mkOption { type = lib.types.port; default = 10000; }; rtpEnd = lib.mkOption { type = lib.types.port; default = 20000; }; directoryName = lib.mkOption { type = lib.types.str; default = "tel.baubs.net"; description = "Name shown in the phone directory title."; }; directoryPort = lib.mkOption { type = lib.types.port; default = 8080; description = "HTTP port for the phone directory and services."; }; backgroundImages = lib.mkOption { default = {}; description = '' Attrset of background images keyed by display name. Value is a path to any image file — it will be resized automatically to the correct dimensions for each phone model during build. For best results, use a 4:3 aspect ratio source image. ''; type = lib.types.attrsOf lib.types.path; }; sharedPhones = lib.mkOption { default = {}; description = '' Shared/location phones not assigned to a specific person (e.g. hallway, kitchen). These have their own extension but no personal voicemail mailbox. For cisco-8961, the key must be the lowercase MAC address (no colons). For sip-client, the key is a free-form username. ''; type = lib.types.attrsOf (lib.types.submodule { options = phones.phoneDeviceOptions false; }); }; persons = lib.mkOption { default = {}; description = "People with personal extensions, optional voicemail mailboxes, and their own phones."; type = lib.types.attrsOf (lib.types.submodule { options = { extension = lib.mkOption { type = lib.types.str; description = "Personal extension number."; }; displayName = lib.mkOption { type = lib.types.str; default = ""; description = "Name shown in the directory and on caller ID."; }; mailbox = lib.mkOption { type = lib.types.bool; default = true; description = "Whether this person gets a personal voicemail mailbox."; }; ringTimeout = lib.mkOption { type = lib.types.ints.positive; default = 30; description = "Seconds to ring before going to voicemail (or hanging up if no mailbox)."; }; mailboxGreeting = lib.mkOption { type = lib.types.nullOr lib.types.path; default = null; description = "Audio file played to callers before the voicemail beep. Transcoded to multiple formats at build time. null = Asterisk's default announcement."; }; phones = lib.mkOption { default = {}; description = '' Phones belonging to this person, keyed by SIP identity. For cisco-8961, the key must be the lowercase MAC address (no colons). For sip-client, the key is a free-form username. ''; type = lib.types.attrsOf (lib.types.submodule { options = phones.phoneDeviceOptions true; }); }; }; }); }; intercomPrefix = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; description = "Dial prefix for auto-generated intercom extensions. e.g. \"*80\" generates *80100 for ext 100. Only intercom-capable (provisioned) phones get entries."; }; codecs = lib.mkOption { description = "Codec preference lists for each endpoint class, ordered highest priority first."; default = {}; type = lib.types.submodule { options = { hardwarePhones = lib.mkOption { type = lib.types.listOf lib.types.str; default = [ "g722" "alaw" "ulaw" "ilbc" ]; description = "Codecs for provisioned hardware phones (e.g. Cisco 8961). Supports G.722, G.711, iLBC. Opus and G.726 not supported. G.729 supported but not useful on LAN."; }; softClients = lib.mkOption { type = lib.types.listOf lib.types.str; default = [ "opus" "g722" "alaw" "ulaw" ]; description = "Codecs for software SIP clients. Opus first for best quality on modern softphones."; }; trunk = lib.mkOption { type = lib.types.listOf lib.types.str; default = [ "alaw" "ulaw" ]; description = "Codecs offered to SIP trunks. Most providers only support G.711."; }; }; }; }; mohClasses = lib.mkOption { default = {}; description = "Music on hold classes. Files are transcoded to ulaw at build time."; type = lib.types.attrsOf (lib.types.submodule { options = { files = lib.mkOption { type = lib.types.listOf lib.types.path; description = "Audio files to play (MP3, WAV, etc). Transcoded to 8kHz ulaw automatically."; }; sort = lib.mkOption { type = lib.types.enum [ "random" "alphabetical" ]; default = "random"; }; }; }); }; sipTrunks = lib.mkOption { default = {}; description = "External SIP provider trunks, keyed by a short name (e.g. \"provider-a\")."; type = lib.types.attrsOf (lib.types.submodule { options = { host = lib.mkOption { type = lib.types.str; default = ""; description = "SIP provider hostname or IP address. Use hostFile to read from a file."; }; hostFile = lib.mkOption { type = lib.types.nullOr lib.types.path; default = null; description = "File containing the SIP provider hostname. Takes precedence over host."; }; username = lib.mkOption { type = lib.types.str; default = ""; description = "SIP account username. Use usernameFile to read from a file."; }; usernameFile = lib.mkOption { type = lib.types.nullOr lib.types.path; default = null; description = "File containing the SIP account username. Takes precedence over username."; }; password = lib.mkOption { type = lib.types.str; default = ""; description = "SIP account password. Use passwordFile to read from a file."; }; passwordFile = lib.mkOption { type = lib.types.nullOr lib.types.path; default = null; description = "File containing the SIP account password. Takes precedence over password."; }; transport = lib.mkOption { type = lib.types.enum [ "udp" "tcp" ]; default = "udp"; }; callerId = lib.mkOption { type = lib.types.str; default = ""; description = "Outbound caller ID number presented to the provider. Leave empty to use provider default. Use callerIdFile to read from a file."; }; callerIdFile = lib.mkOption { type = lib.types.nullOr lib.types.path; default = null; description = "File containing the outbound caller ID. Takes precedence over callerId."; }; }; }); }; sharedMailbox = lib.mkOption { default = null; description = "Shared voicemail mailbox accessible by all phones (family answering machine)."; type = lib.types.nullOr (lib.types.submodule { options = { mailboxId = lib.mkOption { type = lib.types.str; default = "200"; description = "Numeric mailbox ID used internally in voicemail.conf and VoiceMail(). Must be numeric."; }; checkExtension = lib.mkOption { type = lib.types.str; default = "*98"; description = "Extension dialled to check this shared mailbox directly (via VoiceMailMain)."; }; displayName = lib.mkOption { type = lib.types.str; default = "Shared"; description = "Name shown in voicemail configuration."; }; greeting = lib.mkOption { type = lib.types.nullOr lib.types.path; default = null; description = "Audio file played to callers before the voicemail beep. Transcoded to multiple formats at build time. null = Asterisk's default announcement."; }; }; }); }; dids = lib.mkOption { default = {}; description = "Inbound DID routing. Each DID must reference a key from sipTrunks."; type = lib.types.attrsOf (lib.types.submodule { options = { number = lib.mkOption { type = lib.types.str; default = ""; description = "DID number in E.164 format (e.g. \"+4912345678\"). Use numberFile to read from a file."; }; numberFile = lib.mkOption { type = lib.types.nullOr lib.types.path; default = null; description = "File containing the DID number. Takes precedence over number."; }; trunk = lib.mkOption { type = lib.types.str; description = "Key of the sipTrunks entry this DID arrives on."; }; displayName = lib.mkOption { type = lib.types.str; default = ""; description = "Human-readable label for this DID (informational only)."; }; routing = lib.mkOption { description = "How inbound calls on this DID are distributed to phones."; type = lib.types.submodule { options = { type = lib.mkOption { type = lib.types.enum [ "all" "person" "persons" ]; description = '' all — ring all phones (sharedPhones + all persons) on their L2 line person — ring a single person on their L1 line persons — ring a list of persons on their L2 line ''; }; person = lib.mkOption { type = lib.types.str; default = ""; description = "Person key for routing.type = \"person\"."; }; persons = lib.mkOption { type = lib.types.listOf lib.types.str; default = []; description = "Person keys for routing.type = \"persons\"."; }; timeout = lib.mkOption { type = lib.types.ints.positive; default = 30; description = "Seconds to ring before going to voicemail (or hanging up)."; }; }; }; }; mailbox = lib.mkOption { type = lib.types.enum [ "shared" "person" "none" ]; default = "shared"; description = '' shared — go to sharedMailbox on no answer (requires sharedMailbox to be set) person — go to the routed person's mailbox on no answer (only valid with routing.type = "person") none — hang up on no answer ''; }; musicOnHold = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; description = '' MOH class name to play to the caller while phones ring, instead of ringback. Must match a key in mohClasses. null = standard ringback. ''; }; }; }); }; extensions = lib.mkOption { default = {}; description = '' Extra extensions: page groups and custom app entries. Line extensions are auto-generated from sharedPhones and persons — do not declare them here. ''; type = lib.types.attrsOf (lib.types.submodule { options = { mode = lib.mkOption { type = lib.types.enum [ "page" "app" ]; default = "page"; description = '' Extension mode: - "page": one-way announcement to all phones - "app": custom Asterisk dialplan application ''; }; displayName = lib.mkOption { type = lib.types.str; default = ""; }; app = lib.mkOption { type = lib.types.nullOr lib.types.str; default = null; description = "Verbatim Asterisk dialplan app. Required for mode = \"app\"."; }; }; }); }; }; }