Interact with AMI
Listen to live Asterisk events and send AMI commands from a module worker.
The Asterisk Manager Interface (AMI) is a TCP protocol exposed by Asterisk on 127.0.0.1:5038. It lets you do two things from a module:
Listen to a live stream of events (
Newchannel,DialBegin,Hangup,UserEvent, peer/extension status, …).Send actions (
Originate,Hangup,SetVar,Command, …) and read the response.
MikoPBX never makes you open a raw socket. The Core gives you a fully wired MikoPBX\Core\Asterisk\AsteriskManager instance through MikoPBX\Core\System\Util::getAstManager(), already connected and logged in with the system AMI credentials.
This recipe threads the running example module ModuleBlackList (config class BlackListConf, main class BlackListMain, model BlackListNumbers) and anchors every pattern on two real, working modules:
Extensions/EXAMPLES/AMI/ModuleExampleAmi/— the canonical AMI example.Extensions/ModuleCallTracking/— a production module that listens for a singleUserEventand forwards it over HTTP.
Two connection modes: listener vs commander
Util::getAstManager() resolves one of two shared DI services depending on the $events argument:
public static function getAstManager(string $events = 'on'): AsteriskManager
{
if ($events === 'on') {
$nameService = AmiConnectionListener::SERVICE_NAME; // 'amiListener'
} else {
$nameService = AmiConnectionCommand::SERVICE_NAME; // 'amiCommander'
}
// returns the shared, already-connected manager from the DI container
}Util::getAstManager() / Util::getAstManager('on')
amiListener
A worker that subscribes to the event stream.
Util::getAstManager('off')
amiCommander
One-shot actions (Originate, Hangup, Command) where you do not want the event firehose.
The amiCommander provider connects to 127.0.0.1:<AMI port> (the port comes from PbxSettings::AMI_PORT, default 5038) with events turned off:
Step 1 — Write the AMI listener worker
An AMI listener is a WorkerBase subclass whose start() method:
obtains the listener connection via
Util::getAstManager();installs event filters (
setFilter()) — without them AMI sends almost nothing;registers a callback with
addEventHandler();blocks in a
while (true)loop onwaitUserEvent(true), reconnecting when the loop returns an empty array.
Here is the ModuleBlackList worker, modelled directly on the real example.
See the working originals: Extensions/EXAMPLES/AMI/ModuleExampleAmi/Lib/WorkerExampleAmiAMI.php (broadcasts events to the web UI over the EventBus) and Extensions/ModuleCallTracking/Lib/WorkerCallTrackingAMI.php (forwards a single UserEvent over HTTP).
Why each piece is there
require_once 'Globals.php';— bootstraps the module autoloader/DI so the worker can run as a standalone CLI process.setFilter()is not optional. A freshly logged-in AMI connection with an event filter installed receives only what you explicitly add. The Core's own example puts it bluntly: "Without explicit filters, AMI only sends login/ping responses." AddUserEvent: <name>filters for user events andEvent: <Name>filters for native Asterisk events.makePingTubeName()+replyOnPingRequest()come fromWorkerBase. The monitor (WorkerSafeScriptsCore) pings the worker by sending aUserEventwhose name equalsmakePingTubeName(static::class);replyOnPingRequest()detects it and answers with a…PongUserEvent. If you skip the ping filter, the monitor concludes the worker is dead and restarts it in a loop.waitUserEvent(true)blocks reading the socket, runs each event through the registered handler, and returns[]on timeout — your cue to reconnect. It loops internally (do { … } while (!$timeout)) and returns only when a ping fails, so it does not hand control back once per event. The body of yourwhile (true)loop therefore executes only on disconnect. Put per-event work in the handler and periodic work insetOnIdleCallback(); anything you place in the outer loop expecting it to run regularly will never run at all on a healthy PBX — and will look fine in testing for exactly that reason.
Three constraints on addEventHandler(string $event, array|string $callback) (Core/src/Core/Asterisk/AsteriskManager.php:1751).
It registers a handler only once per event name — the name is lowercased, and if one is already registered the method returns
falseand does nothing. Register handlers instart()before the loop, not inside it.The signature is
array|string— a closure is not accepted. Pass an array callable[$this, 'callback'](the form used above) or a plain function-name string. Anything else is silently stored and then fails at dispatch time.The two forms are not invoked the same way. An array callable receives a single argument — the parsed event parameters array. A string callable receives four arguments (event name, parameters, server, port). Use the array form unless you have a reason not to; then your handler signature is
function callback(array $parameters): void.
Use 'userevent' for user events or '*' to catch every event type (as ModuleExampleAmi does).
Subscribing to native Asterisk events
To watch call progress instead of (or in addition to) custom user events, add Event: filters and handle '*':
Extensions/EXAMPLES/AMI/ModuleExampleAmi/Lib/WorkerExampleAmiAMI.php filters exactly this set (Newchannel, Newstate, DialBegin, DialEnd, Hangup, PeerStatus, ExtensionStatus, Hold, Unhold, Bridge) and registers its handler against '*' so every one of them reaches the callback.
Step 2 — Register the worker for monitoring
Workers do not start themselves. Declare them from your config class via getModuleWorkers() so the Core supervisor (WorkerSafeScriptsCore) launches and watches them. For an AMI listener use the CHECK_BY_AMI strategy — that is the ping mechanism the worker already answers in callback().
WorkerSafeScriptsCore::CHECK_BY_AMI is the literal string 'checkWorkerAMI' (defined in Core/src/Core/Workers/Cron/WorkerSafeScriptsCore.php). The other strategy, CHECK_BY_BEANSTALK ('checkWorkerBeanstalk'), is for queue workers, not AMI listeners.
Confirm against Extensions/EXAMPLES/AMI/ModuleExampleAmi/Lib/ExampleAmiConf.php, which registers WorkerExampleAmiAMI with exactly this shape.
Step 3 — Send AMI commands
To act on the PBX, grab a manager connection and call the typed methods on AsteriskManager. All of these are real methods on Core/src/Core/Asterisk/AsteriskManager.php.
Originate a call
Originate() accepts named parameters ($channel, $exten, $context, $priority, $application, $data, $timeout, $callerid, $variable, $account, $async, $actionid). Empty parameters are stripped before the request is sent, and Async is normalized to 'true'/'false' for you.
Hang up a channel
Run a CLI command or a raw action
AsteriskManager also exposes Command() (wraps the AMI Command action) and sendRequestTimeout($action, $params) for any action that has no typed wrapper:
In Asterisk 20 the AMI Command action does not reliably return CLI output. ExampleAmiConf::executeCliCommand() works around this by shelling out with Processes::mwExec("/usr/sbin/asterisk -rx " . escapeshellarg($command), …) instead. Use a typed action (CoreStatus, Originate, …) when you need the response array; fall back to asterisk -rx only for CLI text output.
A complete request router — accepting either a CLI string or a multi-line Action: … block over REST — lives in Extensions/EXAMPLES/AMI/ModuleExampleAmi/Lib/ExampleAmiConf.php (sendAmiCommandAction(), sendNativeAmiAction()).
Step 4 — Provision a dedicated AMI user (optional)
The shared Util::getAstManager() connection uses the system AMI account, which is enough for most modules. If you need your own AMI user (for example, a third-party app that connects directly), override generateManagerConf() on your config class. The Core appends its return value to manager.conf.
generateManagerConf() is a real hook declared in Core/src/Core/Asterisk/Configs/AsteriskConfigInterface.php and overridden here. ManagerConf::reload() (Core/src/Core/Asterisk/Configs/ManagerConf.php:258-270) regenerates manager.conf — which is what invokes your generateManagerConf() hook again — plus http.conf, then issues module reload manager and module reload http. ExampleAmiConf::generateManagerConf() (Extensions/EXAMPLES/AMI/ModuleExampleAmi/Lib/ExampleAmiConf.php:269) is the working version of the hook itself.
Older module code — including ExampleAmiConf::onAfterModuleEnable() at line 292 of that same file — reloads the manager with System::invokeActions(['manager' => 0]). Do not copy that part. The method is marked @deprecated in Core/src/Core/System/System.php:61-69 (the docblock points at WorkerModelsEvents::invokeAction()). Call the specific config class's reload() instead — ManagerConf::reload() here, ExtensionsConf::reload() for the dialplan. The PBX class is deprecated for the same reason.
read=all / write=all grants full AMI privileges. Keep deny=0.0.0.0/0.0.0.0 plus an explicit permit=127.0.0.1/... so the account is reachable only from localhost, and narrow the read/write classes to what your module actually needs.
Reference: methods used in this recipe
All confirmed against the pinned Core sources.
Util::getAstManager(string $events = 'on')
Core/src/Core/System/Util.php
Get the shared listener ('on') or commander ('off') connection.
AsteriskManager::sendRequestTimeout(string $action, array $params = [])
AsteriskManager.php
Send any AMI action and read the response array.
AsteriskManager::waitUserEvent(bool $allow_timeout = false)
AsteriskManager.php
Block on the event stream; returns [] on timeout.
AsteriskManager::addEventHandler(string $event, array|string $callback)
AsteriskManager.php
Register a per-event handler (once per name).
AsteriskManager::Originate(...)
AsteriskManager.php
Originate a call (named args).
AsteriskManager::Hangup(string $channel)
AsteriskManager.php
Hang up a channel.
AsteriskManager::SetVar(string $channel, string $variable, string $value)
AsteriskManager.php
Set a channel variable.
AsteriskManager::Command(string $command, ?string $actionid = null)
AsteriskManager.php
Run a CLI command via the Command action.
WorkerBase::makePingTubeName(string $class)
Core/src/Core/Workers/WorkerBase.php
Compute this worker's liveness ping name.
WorkerBase::replyOnPingRequest(array $parameters)
WorkerBase.php
Answer a monitor ping; returns true if handled.
WorkerSafeScriptsCore::CHECK_BY_AMI
WorkerSafeScriptsCore.php
Monitoring strategy string 'checkWorkerAMI'.
ManagerConf::reload()
Core/src/Core/Asterisk/Configs/ManagerConf.php:258
Regenerate manager.conf + http.conf and reload both Asterisk modules.
PbxSettings::AMI_PORT
Core/src/Common/Models/PbxSettings.php
AMI TCP port setting (default 5038).
See also
Last updated
Was this helpful?