{ lib, pkgs, cfg, models, allPhones, backgroundEntries }:
let
hasTrunk = cfg.sipTrunks != {};
# Page extensions for the intercom button auto-dial
pageExtension =
let pages = lib.attrNames (lib.filterAttrs (_: e: e.mode == "page") cfg.extensions);
in if pages != [] then lib.head pages else null;
hasIntercomButton = cfg.intercomPrefix != null && pageExtension != null;
# Collect all internal extension numbers to generate exact-match patterns.
# This tells the phone "this number is complete" so it dials immediately
# rather than waiting for the timeout or firing on the first digit.
allExtensions = lib.unique (
lib.mapAttrsToList (_: p: p.extension) cfg.sharedPhones
++ lib.mapAttrsToList (_: p: p.extension) cfg.persons
);
# Star extensions: intercom prefix + each extension, plus custom extensions.
allStarExtensions =
(lib.optionals (cfg.intercomPrefix != null)
(map (ext: "${cfg.intercomPrefix}${ext}") allExtensions))
++ lib.attrNames cfg.extensions
++ lib.optional (cfg.sharedMailbox != null) cfg.sharedMailbox.checkExtension;
# Generate the dial template for a phone, given its intercom lineIndex.
# The lineIndex is derived from familyLineEnabled — the same condition used
# in the Cisco template for button layout, so both are always in sync.
#
# Pattern evaluation: the phone tests templates top-to-bottom and dials as
# soon as digits match a pattern with timeout="0", or after the timeout
# elapses for patterns with timeout > 0. Explicit patterns must come before
# the catch-all "." so the phone doesn't fire early on a partial match.
mkDialplanXml = key: intercomLineIndex:
let
# Stable UUID derived from a hash of all inputs that affect this dialplan.
# Changes whenever extensions, trunk config, or intercom config changes,
# causing phones to reload the file. Format: 8-4-4-4-12 hex chars.
h = builtins.hashString "sha256" (builtins.toJSON {
inherit key allExtensions allStarExtensions hasTrunk hasIntercomButton intercomLineIndex;
});
versionStamp = "${builtins.substring 0 8 h}-${builtins.substring 8 4 h}-${builtins.substring 12 4 h}-${builtins.substring 16 4 h}-${builtins.substring 20 12 h}";
# Exact-match a complete extension number — dial immediately.
extTemplate = ext: " ";
in ''
${versionStamp}
${lib.optionalString hasIntercomButton
" "}
${lib.concatMapStrings (ext: extTemplate ext + "\n") allExtensions}
${lib.concatMapStrings (ext: extTemplate ext + "\n") allStarExtensions}
'';
in
pkgs.linkFarm "voip-tftp-root" (
lib.concatLists (lib.mapAttrsToList (key: phone:
let m = models.${phone.model}; in
lib.optionals m.hasProvisioning (
let
upperKey = lib.toUpper key;
familyLineEnabled = hasTrunk && phone.personKey != null;
intercomLineIndex = if familyLineEnabled then 3 else 2;
dialplanFilename = "dialplan-${upperKey}.xml";
xml = m.template ({
mac = key;
inherit (phone) label password displayName;
serverAddress = cfg.serverAddress;
ntpServer = cfg.ntpServer;
sipPort = cfg.sipPort;
directoryPort = cfg.directoryPort;
dialplanFile = dialplanFilename;
} // lib.optionalAttrs (cfg.intercomPrefix != null) {
intercomEnabled = true;
intercomPassword = phone.password;
} // lib.optionalAttrs familyLineEnabled {
inherit familyLineEnabled;
familyLineLabel =
if cfg.sharedMailbox != null
then cfg.sharedMailbox.displayName
else "Familie";
familyLinePassword = phone.password;
});
in [
{ name = "SEP${upperKey}.cnf.xml";
path = pkgs.writeText "SEP${upperKey}.cnf.xml" xml; }
{ name = dialplanFilename;
path = pkgs.writeText dialplanFilename (mkDialplanXml key intercomLineIndex); }
]
)
) allPhones)
++ backgroundEntries
)