Files
ha-sip-notifier/sip-notifier/sip_service.py

244 lines
6.9 KiB
Python

#!/usr/bin/env python3
"""SIP Voice Notifier using Linphone CLI."""
import json
import logging
import os
import subprocess
import tempfile
import time
from urllib.parse import urlparse
import requests
from flask import Flask, request, jsonify
from pydub import AudioSegment
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
_LOGGER = logging.getLogger(__name__)
app = Flask(__name__)
CONFIG = {}
DEFAULT_SAMPLE_RATE = 8000
def setup_linphone_config():
"""Create Linphone configuration."""
sip_user = CONFIG['sip_user']
sip_server = CONFIG['sip_server']
sip_password = CONFIG['sip_password']
config_dir = '/data/linphone'
os.makedirs(config_dir, exist_ok=True)
config_file = f'{config_dir}/linphonerc'
config = f"""[sip]
sip_port=5060
default_proxy=0
[proxy_0]
reg_proxy=sip:{sip_server}
reg_identity=sip:{sip_user}@{sip_server}
reg_expires=3600
reg_sendregister=1
[auth_info_0]
username={sip_user}
userid={sip_user}
passwd={sip_password}
realm={sip_server}
"""
with open(config_file, 'w') as f:
f.write(config)
return config_file
def download_and_convert_audio(url: str) -> str:
"""Download and convert audio."""
parsed = urlparse(url)
ext = os.path.splitext(parsed.path)[1] or ".mp3"
with tempfile.NamedTemporaryFile(delete=False, suffix=ext) as tmp:
download_path = tmp.name
response = requests.get(url, stream=True, timeout=30)
response.raise_for_status()
with open(download_path, 'wb') as f:
for chunk in response.iter_content(8192):
f.write(chunk)
try:
audio = AudioSegment.from_file(download_path)
audio = audio.set_frame_rate(DEFAULT_SAMPLE_RATE).set_channels(1).set_sample_width(2)
with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as tmp:
wav_path = tmp.name
audio.export(wav_path, format="wav")
os.remove(download_path)
return wav_path
except Exception as e:
if os.path.exists(download_path):
os.remove(download_path)
raise Exception(f"Audio conversion failed: {e}")
def generate_tts_audio(message: str) -> str:
"""Generate TTS audio."""
from gtts import gTTS
with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as tmp:
mp3_path = tmp.name
tts = gTTS(text=message, lang='en')
tts.save(mp3_path)
audio = AudioSegment.from_mp3(mp3_path)
audio = audio.set_frame_rate(DEFAULT_SAMPLE_RATE).set_channels(1).set_sample_width(2)
with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as tmp:
wav_path = tmp.name
audio.export(wav_path, format="wav")
os.remove(mp3_path)
return wav_path
def place_sip_call(destination: str, audio_file: str, duration: int):
"""Place SIP call using Linphone CLI."""
config_file = setup_linphone_config()
sip_server = CONFIG['sip_server']
if not destination.startswith('sip:'):
dest_uri = f"sip:{destination}@{sip_server}"
else:
dest_uri = destination
_LOGGER.info(f"Calling {dest_uri} using Linphone...")
try:
cmd = ['linphonec', '-c', config_file, '-C']
process = subprocess.Popen(
cmd,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
# Wait for registration
_LOGGER.info("Waiting for SIP registration...")
time.sleep(5)
# Make call
_LOGGER.info(f"Calling {dest_uri}...")
process.stdin.write(f"call {dest_uri}\n")
process.stdin.flush()
# Wait for answer
_LOGGER.info("Waiting for answer...")
time.sleep(5)
# Play audio
_LOGGER.info(f"Playing {audio_file}...")
process.stdin.write(f"play {audio_file}\n")
process.stdin.flush()
# Keep call active
_LOGGER.info(f"Call active for {duration}s...")
time.sleep(duration)
# Hangup
_LOGGER.info("Ending call...")
process.stdin.write("terminate\n")
process.stdin.flush()
time.sleep(2)
process.stdin.write("quit\n")
process.stdin.flush()
try:
process.wait(timeout=5)
except subprocess.TimeoutExpired:
process.kill()
_LOGGER.info("Call completed")
except Exception as e:
_LOGGER.error(f"Call error: {e}", exc_info=True)
raise
@app.route('/health', methods=['GET'])
def health():
return jsonify({'status': 'ok'}), 200
@app.route('/send_notification', methods=['POST'])
def handle_send_notification():
try:
data = request.json
destination = data.get('destination')
audio_url = data.get('audio_url')
message = data.get('message')
duration = data.get('duration', CONFIG.get('default_duration', 30))
if not destination:
return jsonify({'error': 'destination required'}), 400
if not audio_url and not message:
return jsonify({'error': 'audio_url or message required'}), 400
temp_files = []
try:
if audio_url:
_LOGGER.info(f"Downloading: {audio_url}")
audio_file = download_and_convert_audio(audio_url)
temp_files.append(audio_file)
else:
_LOGGER.info(f"Generating TTS: {message}")
audio_file = generate_tts_audio(message)
temp_files.append(audio_file)
place_sip_call(destination, audio_file, duration)
return jsonify({'status': 'success'}), 200
finally:
for f in temp_files:
try:
if os.path.exists(f):
os.remove(f)
except Exception as e:
_LOGGER.warning(f"Cleanup failed: {e}")
except Exception as e:
_LOGGER.error(f"Request error: {e}", exc_info=True)
return jsonify({'error': str(e)}), 500
if __name__ == '__main__':
options_file = '/data/options.json'
if os.path.exists(options_file):
with open(options_file, 'r') as f:
CONFIG = json.load(f)
else:
CONFIG = {
'sip_server': os.getenv('SIP_SERVER', ''),
'sip_user': os.getenv('SIP_USER', ''),
'sip_password': os.getenv('SIP_PASSWORD', ''),
'default_duration': int(os.getenv('DEFAULT_DURATION', 30))
}
_LOGGER.info("=" * 60)
_LOGGER.info("SIP Voice Notifier Ready (Linphone CLI on Debian)")
_LOGGER.info(f"SIP Server: {CONFIG.get('sip_server')}")
_LOGGER.info(f"SIP User: {CONFIG.get('sip_user')}")
_LOGGER.info("=" * 60)
app.run(host='0.0.0.0', port=8099, debug=False)