Metadata-Version: 2.4
Name: simotel-connect
Version: 1.0.6
Summary: A professional, modern, and reusable Python and Django client library for communicating with the Simotel PBX API (v4).
License: MIT
Keywords: simotel,pbx,voip,django,api,asterisk
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Framework :: Django
Classifier: Topic :: Communications :: Telephony
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: requests>=2.28.0
Provides-Extra: django
Requires-Dist: django>=3.2; extra == "django"
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: pytest-django; extra == "dev"
Requires-Dist: responses>=0.23; extra == "dev"
Requires-Dist: django>=3.2; extra == "dev"
Dynamic: license-file

# 📞 Simotel Connect (Python & Django Client for Simotel PBX API v4)

[![PyPI version](https://img.shields.io/pypi/v/simotel-connect.svg)](https://pypi.org/project/simotel-connect/)
[![Python Versions](https://img.shields.io/badge/python-3.8%20%7C%203.9%20%7C%203.10%20%7C%203.11%20%7C%203.12-blue)](https://pypi.org/project/simotel-connect/)
[![Django](https://img.shields.io/badge/django-%3E%3D3.2-green)](https://www.djangoproject.com/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

A professional, modern, and reusable Python and Django client library for communicating with the **Simotel PBX API (v4)**.

🇮🇷 **[برای مطالعه مستندات به زبان فارسی کلیک کنید (Persian Documentation)](./README_FA.md)**

---

## 🌟 Features

- 🔐 **Full Dual Authentication Support**: Seamlessly manages both `X-APIKEY` and HTTP Basic Auth headers required by Simotel.
- ⚙️ **Flexible 3-Tier Configuration Chain**:
  `Direct arguments` > `Django settings.SIMOTEL` > `Environment variables (.env)`
- 🏗️ **Modular Facade & Manager Pattern**: Clean, categorized, and intuitive API namespaces (`sm.call`, `sm.pbx`, `sm.reports`, `sm.autodialer`, etc.).
- 🔄 **Session Pooling & Auto Retry**: Stable connection management with `urllib3` retry adapters.
- 🛡️ **Comprehensive Exception Hierarchy**: Clear error types for auth failures, network issues, timeouts, and PBX API errors.
- 📊 **Over 80+ Simotel Endpoints Supported**:
  - **Call Origination (Click-to-Call)**: Initiate calls via `POST /call/originate/act`
  - **PBX Users & Extensions**: Add, search, edit, and remove PBX users
  - **Queues & Agents**: Queue management, single and batch agent assignment, pause/resume
  - **Trunks**: Trunk configuration and status
  - **Blacklists & Whitelists**: Block or permit numbers with ease
  - **Announcements & Music on Hold**: Upload and manage audio files
  - **Faxes**: Send, receive, and download incoming/outgoing faxes
  - **Voicemails**: Download voicemail recordings and manage boxes
  - **Reports & CDR**: Search call detail records, queue stats, agent performance, and download single/dual-channel audio recordings
  - **Autodialer**: High-volume automated outbound calling campaigns and contact groups
  - **System Health**: Ping and connectivity verification

---

## 📦 Installation

Install the package via `pip`:

```bash
pip install simotel-connect
```

For development and local testing:

```bash
git clone git@github.com:mraminrzn/simotel-connect.git
cd simotel-connect
pip install -e ".[dev]"
```

---

## ⚙️ Configuration

### Option 1: Django `settings.py` (Recommended for Django projects)

Add `simotel_connect` to your `INSTALLED_APPS` and define the `SIMOTEL` dictionary:

```python
# settings.py

INSTALLED_APPS = [
    ...,
    "simotel_connect",
]

SIMOTEL = {
    "HOST": "192.168.1.10",             # Simotel IP address or hostname
    "API_KEY": "YOUR_SIMOTEL_API_KEY",  # From Maintenance > API Accounts
    "USERNAME": "admin",                # Simotel web panel username
    "PASSWORD": "your_password",        # Simotel password
    "PORT": 80,                         # Optional (default: 80)
    "SCHEME": "http",                   # Optional: "http" or "https" (default: "http")
    "TIMEOUT": 30,                      # Optional: request timeout in seconds (default: 30)
    "VERIFY_SSL": True,                 # Optional (default: True)
}
```

Then use the client anywhere in your project (views, services, Celery tasks):

```python
from simotel_connect import Simotel

# Automatically reads configuration from settings.py
sm = Simotel()
```

---

### Option 2: Environment Variables (`.env`)

```env
SIMOTEL_HOST=192.168.1.10
SIMOTEL_API_KEY=YOUR_SIMOTEL_API_KEY
SIMOTEL_USERNAME=admin
SIMOTEL_PASSWORD=your_password
SIMOTEL_TIMEOUT=30
SIMOTEL_VERIFY_SSL=false
```

```python
from simotel_connect import Simotel

sm = Simotel()
```

---

### Option 3: Direct Initialization (Standalone scripts or multi-tenant PBX)

```python
from simotel_connect import Simotel

sm = Simotel(
    host="192.168.1.10",
    api_key="YOUR_SIMOTEL_API_KEY",
    username="admin",
    password="your_password",
    timeout=20,
    verify_ssl=False,
)
```

---

## 🚀 Usage & Examples

### 1. Connection Health Check (Ping)

```python
response = sm.setting.ping()
if response.success:
    print("Successfully connected to Simotel:", response.message)
```

---

### 2. Call Origination (Click-to-Call / Two-Way Calls)

Simotel's Call Origination initiates a two-way phone call via `POST /call/originate/act`:
1. Simotel first dials the **`caller`** (e.g., an internal extension `1001` or operator softphone).
2. Once the caller picks up, Simotel bridges the channel and dials the **`callee`** (e.g., a customer's external mobile `0912...` or another extension) via the specified dialplan **`context`**.
3. Simotel returns a job confirmation and a tracking identifier `originated_call_id`.

#### A. Agent to External Customer (Two-Way Call)
```python
# Operator 1001 calls an external mobile number
resp = sm.call.originate(
    caller="1001",              # Agent internal extension
    callee="09121234567",       # Customer phone number
    context="default",          # Outgoing dialplan context
    caller_id="1001",           # Caller ID displayed to the customer
    timeout=30,                 # Ringing timeout in seconds
)

if resp.success:
    call_job_id = resp.data.get("originated_call_id")
    print(f"Call initiated successfully! Job ID: {call_job_id}")
```

#### B. Extension to Extension (Internal Two-Way Call)
```python
resp = sm.call.originate(
    caller="1001",
    callee="1002",
    context="default",
)
```

#### C. Automated Robocall / Direct Voice Broadcast (No Human Agent)
To play a pre-recorded message or IVR menu directly to a customer without requiring an agent's handset to ring first:
```python
# Originate directly into a Simotel Announcement or IVR context
resp = sm.call.originate(
    caller="*8001",             # Shortcode or extension mapped to an Announcement/IVR in Simotel Dialplan
    callee="09121234567",       # Customer number
    context="default",
    caller_id="02188888888",    # Company caller ID number
    timeout=35,
)
```


---

### 3. Users & Extensions Management

```python
# Create a new extension
sm.pbx.users.add(
    extension="1001",
    name="John Doe",
    password="StrongPassword123",
    email="john@example.com",
    mobile="09121234567",
)

# Search users
users = sm.pbx.users.search(extension="1001")
print(users.data)

# Update a user
sm.pbx.users.update(extension="1001", name="John Doe (Support Lead)")

# Remove a user
sm.pbx.users.remove(extension="1001")
```

---

### 4. Queues & Call Center Agents

```python
# Create a call queue
sm.pbx.queues.add(name="2000", strategy="leastrecent", timeout=30)

# Add an agent to the queue
sm.pbx.queues.add_agent(queue="2000", agent="1001", penalty=0)

# Pause agent (e.g. lunch break)
sm.pbx.queues.pause_agent(queue="2000", agent="1001", reason="Lunch break")

# Resume agent
sm.pbx.queues.resume_agent(queue="2000", agent="1001")

# Batch add agents to queue
sm.pbx.queues.batch_add_agent(queue="2000", agents=["1001", "1002", "1003"])

# Batch pause agents
sm.pbx.queues.batch_pause_agent(queue="2000", agents=["1001", "1002"])

# Remove agent from queue
sm.pbx.queues.remove_agent(queue="2000", agent="1001")
```

---

### 5. Call Detail Records (CDR), Call Status & Audio Downloads

Query, filter, and track call histories and download recorded voice calls via `sm.reports`:

#### A. Smart CDR Search & Filtering
```python
# Search calls by caller, callee, date range, or call ID (cuid)
response = sm.reports.cdr_search(
    conditions={
        "from": "1001",                 # Caller extension or number
        "to": "09121234567",            # Destination number
        "disposition": "ANSWERED",      # ANSWERED | NO ANSWER | BUSY | FAILED
    },
    date_range={
        "from": "2026-09-01 08:00",
        "to": "2026-09-30 20:00",
    },
    pagination={"start": 0, "count": 25, "sorting": {"starttime": -1}},
    alike="true",                       # Enable fuzzy/partial matching
)

for call in response.data:
    print(f"CUID: {call.get('cuid')}")
    print(f"Start Time: {call.get('starttime')} | Disposition: {call.get('disposition')}")
    print(f"Total Duration: {call.get('duration')}s | Talk Time (billsec): {call.get('billsec')}s")
    print(f"Recorded File: {call.get('record') or call.get('audio_file')}")
    print("-" * 40)
```

#### B. Quick Search by Unique Call ID (`cuid`)
```python
# Check status of a specific call by its Simotel unique ID
res = sm.reports.cdr_search(conditions={"cuid": "cuid-1726478923.412"})
if res.data:
    call_info = res.data[0]
    is_answered = call_info.get("disposition") == "ANSWERED"
    talk_seconds = call_info.get("billsec", 0)
    print(f"Answered: {is_answered}, Talk time: {talk_seconds}s")
```

#### C. Download Call Audio Recordings (.mp3 / .wav)
```python
# 1. Download single-channel recording
audio_bytes = sm.reports.download_audio(file="20260914_v2.1789362850.25437.mp3")
with open("recorded_call.mp3", "wb") as f:
    f.write(audio_bytes)

# 2. Download dual-channel recording (agent and customer separated left/right)
dual_bytes = sm.reports.download_audio_dual_channel(file="20260914_v2.1789362850.25437.mp3")
with open("dual_channel.mp3", "wb") as f:
    f.write(dual_bytes)
```

---

### 6. Blacklists & Whitelists

```python
# Block unwanted incoming number
sm.pbx.blacklists.add(number="09999999999", description="Spam caller")

# Remove from blacklist
sm.pbx.blacklists.remove(number="09999999999")

# Add VIP contact to whitelist
sm.pbx.whitelists.add(number="09120000000", description="Executive VIP")
```

---

### 7. Autodialer & Automated Robocall Campaigns (Simotel v4)

Simotel Autodialer enables mass outbound campaigns, playing pre-recorded audio messages without human intervention. The client supports the complete official Simotel v4 API workflow:

#### Step 1: Discover Trunks & Announcements
```python
# 1. Query Autodialer & PBX Trunks to get trunk_manager_id
trunks = sm.autodialer.trunks.search().data
# [{"_id": "6033876dc92de036d1390923", "name": "SipTrunk_Main", "channels": 30, ...}]
trunk_id = trunks[0]["_id"]

# 2. Query available Audio Announcements
announcements = sm.autodialer.announcements.search().data
# [{"_id": "653e0c4f95e63077f8379be7", "name": "Welcome_Offer", ...}]
announcement_id = announcements[0]["_id"]
```

#### Step 2: Create a Contact Group (Optional)
Groups allow organizing target contact lists in Simotel:
```python
# Create group with contact numbers in a single call
group_res = sm.autodialer.groups.add(
    name="VIP_Customers",
    numbers=["09121111111", "09122222222", "09123333333"],
    description="VIP autumn outreach list",
)
group_id = group_res.data.get("_id")
```

#### Step 3: Launch Campaign

##### Option A: Launch using Contact Group
```python
camp_res = sm.autodialer.campaigns.add(
    name="Autumn_Promotion",
    groups=[group_id],                  # Simotel Group ObjectId
    trunk_manager_id=trunk_id,          # Trunk ObjectId
    announcement=announcement_id,        # Audio Announcement ObjectId
    start="2026-09-16 09:00",           # Start datetime (YYYY-MM-DD HH:MM)
    end="2026-09-23 20:00",             # End datetime
    count=3,                            # Number of recipients
    try="2",                            # Max retry attempts for unanswered calls
    try_interval="600",                 # Interval between retries in seconds
    interface_context="auto",
    interface_text="text1",
    description="Automated promo voice call",
)
campaign_id = camp_res.data.get("_id")
print(f"Campaign launched successfully! ID: {campaign_id}")
```

##### Option B: Direct Campaign without Creating a Group
Simotel v4 allows passing phone numbers directly to the campaign:
```python
camp_res = sm.autodialer.campaigns.add(
    name="Instant_Alert_Campaign",
    numbers=["09121111111", "09122222222"],
    groups=[],                          # Empty groups list
    trunk_manager_id=trunk_id,
    announcement=announcement_id,
    start="2026-09-16 09:00",
    end="2026-09-17 18:00",
    count=2,
    try="1",
)
```

#### Step 4: Campaign Reports & Asterisk Cause Inspection
Track real-time call dispositions and troubleshoot why calls might fail:
```python
# Fetch reports for the campaign
rep_res = sm.autodialer.reports.search(
    conditions={"campaign_id": campaign_id},
    pagination={"start": 0, "count": 50, "sorting": {"date": -1}}
)

for report in rep_res.data:
    number = report.get("number")
    disposition = report.get("disposition")   # ANSWERED, NO ANSWER, BUSY, FAILED
    billsec = report.get("billsec", 0)        # Talk duration in seconds
    print(f"Number: {number} | Status: {disposition} | Talk: {billsec}s")

    # If call failed, inspect Asterisk cause details:
    tries = report.get("tries", [])
    if tries:
        last_try = tries[-1]
        cause_txt = last_try.get("cause-txt") # e.g. "Circuit/channel congestion", "Call Rejected"
        cause_code = last_try.get("cause")
        if cause_txt:
            print(f"  --> Cause: {cause_txt} (Asterisk Code: {cause_code})")
```

#### Step 5: Cleanup & Removing Campaigns & Groups
```python
# Remove Campaign by ObjectId
sm.autodialer.campaigns.remove(_id=campaign_id)

# Remove Group by ObjectId
sm.autodialer.groups.remove(_id=group_id)
```


---

## ⚠️ Exception Handling

All custom exceptions inherit from `SimotelError`:

```python
from simotel_connect import (
    Simotel,
    SimotelError,
    SimotelAuthError,
    SimotelAPIError,
    SimotelConnectionError,
    SimotelTimeoutError,
)

sm = Simotel()

try:
    sm.call.originate(caller="1001", callee="1002")
except SimotelAuthError:
    print("Authentication failed: Check API Key, username, or password.")
except SimotelAPIError as e:
    print(f"Simotel returned an API error: {e.args[0]}")
    print(f"Details: {e.data}")
except SimotelConnectionError:
    print("Connection error: Unable to reach Simotel server.")
except SimotelTimeoutError:
    print("Request timed out.")
except SimotelError as e:
    print(f"Simotel error: {e}")
```

---

## 🖥️ Interactive Web Demo

An interactive Django test panel is provided under `demo_project/`. It allows you to visually test audio downloading and call origination:

```bash
cd demo_project
python manage.py runserver
```

Open `http://127.0.0.1:8000/` in your browser.

---

## 🧪 Running Tests

```bash
pip install -e ".[dev]"
pytest
```

---

## 📄 License

This project is licensed under the **MIT License**.
