nix/modules/voip/phones.nix

107 lines
3.9 KiB
Nix

{ lib }:
let
# Per-model config. Adding a new hardware model:
# 1. Add an entry here (PBX-relevant fields only)
# 2. Add a provisioning template in ./provisioning/templates/<model>.nix
# exporting: desktopSize, thumbnailSize, mkConfig, mkDialplan
models = {
"cisco-8961" = {
endpointTemplate = "endpoint-cisco-8961";
maxContacts = 1;
hasProvisioning = true;
hasIntercom = true; # auto-answer speakerphone line
hasMultiLine = true; # separate L2 line for family/shared DID
codecs = [ "g722" "alaw" "ulaw" "ilbc" ];
};
"cisco-9971" = {
endpointTemplate = "endpoint-cisco-8961";
maxContacts = 1;
hasProvisioning = true;
hasIntercom = true;
hasMultiLine = true;
codecs = [ "g722" "alaw" "ulaw" "ilbc" ];
};
"sip-client" = {
endpointTemplate = "endpoint-generic";
maxContacts = 1;
hasProvisioning = false;
hasIntercom = false;
hasMultiLine = false;
codecs = [ "opus" "g722" "alaw" "ulaw" ];
};
};
# Shared option set for a physical phone device.
# isPersonPhone = true → no extension/displayName fields (inherited from person)
# isPersonPhone = false → includes extension and displayName
phoneDeviceOptions = isPersonPhone: {
model = lib.mkOption {
type = lib.types.enum (lib.attrNames models);
description = "Phone model. Use \"sip-client\" for software SIP clients (no provisioning file).";
};
label = lib.mkOption {
type = lib.types.str;
default = "";
description = "Label shown on the phone screen (max ~12 chars for Cisco). Required for provisioned hardware phones.";
};
password = lib.mkOption {
type = lib.types.str;
description = "SIP registration password.";
};
} // lib.optionalAttrs (!isPersonPhone) {
extension = lib.mkOption {
type = lib.types.str;
description = "Internal extension number for this shared phone.";
};
displayName = lib.mkOption {
type = lib.types.str;
default = "";
description = "Name shown in the phone directory.";
};
};
# Unified view of all physical devices, keyed by SIP identity (MAC or username).
# Each entry carries the fields needed by sub-modules without them having to
# know about sharedPhones vs persons.
mkAllPhones = cfg:
lib.mapAttrs (key: p: {
inherit (p) model label password;
extension = p.extension;
displayName = p.displayName;
personKey = null;
sharedLine = true; # shared phones always participate in shared line
mailboxExt = null; # shared phones have no personal mailbox
}) cfg.sharedPhones
//
lib.foldlAttrs (acc: personKey: person:
acc // lib.mapAttrs (_key: ph: {
inherit (ph) model label password;
extension = person.extension;
displayName = person.displayName;
personKey = personKey;
sharedLine = person.sharedLine;
mailboxExt = if person.mailbox then person.extension else null;
}) person.phones
) {} cfg.persons;
# Auto-generate intercom extensions for provisioned phones.
mkIntercomEntries = cfg: allPhones:
if cfg.intercomPrefix == null then []
else lib.concatLists (lib.mapAttrsToList (key: phone:
let m = models.${phone.model}; in
lib.optional m.hasIntercom {
extension = "${cfg.intercomPrefix}${phone.extension}";
endpoint = "${key}-intercom";
phoneKey = key;
target = phone.extension;
displayName = "Intercom ${phone.displayName}";
password = phone.password;
endpointTemplate = m.endpointTemplate;
maxContacts = m.maxContacts;
}
) allPhones);
in {
inherit models phoneDeviceOptions mkAllPhones mkIntercomEntries;
}