2026-08-14 18:02:25 +02:00
|
|
|
"""
|
|
|
|
|
Yeastar Routes - GMAO Collège
|
|
|
|
|
Surveillance téléphone P520
|
|
|
|
|
"""
|
|
|
|
|
from flask import Blueprint, render_template, jsonify, request, redirect, url_for, flash
|
|
|
|
|
from flask_login import login_required
|
|
|
|
|
from app_new.extensions import db
|
|
|
|
|
from app_new.core.system_security import system_admin_required
|
|
|
|
|
from app_new.yeastar.models import YeastarConfig, YeastarDNDConfig
|
|
|
|
|
from app_new.lib_ext.api_client import YeastarClient
|
2026-08-14 19:33:05 +02:00
|
|
|
from cryptography.fernet import InvalidToken
|
2026-08-14 18:02:25 +02:00
|
|
|
import subprocess
|
|
|
|
|
from datetime import datetime, timezone
|
|
|
|
|
|
|
|
|
|
yeastar_bp = Blueprint('yeastar', __name__, template_folder='templates')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_client():
|
|
|
|
|
"""Crée et retourne un client P520 connecté en utilisant la config DB."""
|
|
|
|
|
config = YeastarConfig.get_config()
|
|
|
|
|
|
|
|
|
|
if not config:
|
|
|
|
|
return None, "Configuration Yeastar non trouvée. Veuillez configurer les identifiants."
|
|
|
|
|
|
2026-08-14 19:33:05 +02:00
|
|
|
try:
|
|
|
|
|
username = config.get_username()
|
|
|
|
|
password = config.get_password()
|
|
|
|
|
except (InvalidToken, ValueError, RuntimeError):
|
|
|
|
|
return None, "Les identifiants Yeastar doivent etre saisis de nouveau."
|
2026-08-14 18:02:25 +02:00
|
|
|
|
|
|
|
|
if not all([config.base_url, username, password]):
|
|
|
|
|
return None, "Identifiants incomplets dans la configuration."
|
|
|
|
|
|
|
|
|
|
client = YeastarClient(config.base_url, username, password)
|
|
|
|
|
result = client.login()
|
|
|
|
|
|
|
|
|
|
if result['success']:
|
|
|
|
|
return client, None
|
|
|
|
|
return None, result.get('error')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@yeastar_bp.route('/')
|
|
|
|
|
@login_required
|
|
|
|
|
def index():
|
|
|
|
|
"""Dashboard surveillance P520."""
|
|
|
|
|
client, error = get_client()
|
|
|
|
|
|
|
|
|
|
extensions = []
|
|
|
|
|
pbx_info = {}
|
|
|
|
|
|
|
|
|
|
if client:
|
|
|
|
|
try:
|
|
|
|
|
extensions = client.get_extensions()
|
|
|
|
|
pbx_info = client.get_pbx_info()
|
|
|
|
|
client.logout()
|
|
|
|
|
except Exception as e:
|
|
|
|
|
error = str(e)
|
|
|
|
|
|
|
|
|
|
status_map = {
|
|
|
|
|
'available': 'Disponible',
|
|
|
|
|
'away': 'Absent',
|
|
|
|
|
'business_trip': 'En déplacement',
|
|
|
|
|
'do_not_disturb': 'Ne pas déranger',
|
|
|
|
|
'launch': 'Pause déjeuner',
|
|
|
|
|
'off_work': 'Hors service'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Status de connexion réseau (status field)
|
|
|
|
|
# 0 = Enregistré/Connecté, 4 = Non enregistré/Non connecté
|
|
|
|
|
conn_map = {
|
|
|
|
|
0: ('Connecté', 'success'),
|
|
|
|
|
4: ('Non connecté', 'warning'),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ext_display = []
|
|
|
|
|
for ext in sorted(extensions, key=lambda x: int(x.get('ext_num', 0))):
|
|
|
|
|
status_code = ext.get('status', -1)
|
|
|
|
|
conn_info = conn_map.get(status_code, ('Inconnu', 'secondary'))
|
|
|
|
|
|
|
|
|
|
ext_display.append({
|
|
|
|
|
'num': ext['ext_num'],
|
|
|
|
|
'name': ext.get('caller_id_name', 'N/A'),
|
|
|
|
|
'status': ext.get('presence_status', 'unknown'),
|
|
|
|
|
'status_fr': status_map.get(ext.get('presence_status', ''), ext.get('presence_status', '')),
|
|
|
|
|
'status_code': status_code,
|
|
|
|
|
'conn_label': conn_info[0],
|
|
|
|
|
'conn_class': conn_info[1],
|
|
|
|
|
'ip': ext.get('ip_address', 'N/A'),
|
|
|
|
|
'device': ext.get('device', 'N/A')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
# Configuration DND
|
|
|
|
|
dnd_config = YeastarDNDConfig.query.first()
|
|
|
|
|
if not dnd_config:
|
|
|
|
|
dnd_config = YeastarDNDConfig()
|
|
|
|
|
db.session.add(dnd_config)
|
|
|
|
|
db.session.commit()
|
|
|
|
|
|
|
|
|
|
# Configuration Yeastar
|
|
|
|
|
yeastar_config = YeastarConfig.get_config()
|
|
|
|
|
|
|
|
|
|
return render_template('yeastar/index.html',
|
|
|
|
|
extensions=ext_display,
|
|
|
|
|
pbx_info=pbx_info,
|
|
|
|
|
error=error,
|
|
|
|
|
dnd_config=dnd_config,
|
|
|
|
|
yeastar_config=yeastar_config)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@yeastar_bp.route('/config', methods=['GET', 'POST'])
|
2026-08-14 19:33:05 +02:00
|
|
|
@system_admin_required
|
2026-08-14 18:02:25 +02:00
|
|
|
def config_page():
|
|
|
|
|
"""Page de configuration des identifiants Yeastar."""
|
|
|
|
|
config = YeastarConfig.get_config()
|
|
|
|
|
|
|
|
|
|
if request.method == 'POST':
|
|
|
|
|
base_url = request.form.get('base_url', '').strip()
|
|
|
|
|
username = request.form.get('username', '').strip()
|
|
|
|
|
password = request.form.get('password', '').strip()
|
|
|
|
|
|
2026-08-14 19:33:05 +02:00
|
|
|
if not base_url.startswith(('https://', 'http://')):
|
|
|
|
|
flash("L'URL Yeastar doit commencer par https:// ou http://.", "danger")
|
|
|
|
|
return redirect(url_for('yeastar.config_page'))
|
|
|
|
|
|
2026-08-14 18:02:25 +02:00
|
|
|
if not all([base_url, username]):
|
|
|
|
|
flash("L'URL et le nom d'utilisateur sont obligatoires.", "danger")
|
|
|
|
|
return redirect(url_for('yeastar.config_page'))
|
|
|
|
|
|
|
|
|
|
# Créer ou mettre à jour la configuration
|
|
|
|
|
if not config:
|
|
|
|
|
config = YeastarConfig()
|
|
|
|
|
db.session.add(config)
|
|
|
|
|
|
|
|
|
|
config.base_url = base_url
|
|
|
|
|
|
|
|
|
|
# Chiffrer les identifiants
|
|
|
|
|
config.set_username(username)
|
|
|
|
|
if password:
|
|
|
|
|
config.set_password(password)
|
2026-08-14 19:33:05 +02:00
|
|
|
else:
|
|
|
|
|
try:
|
|
|
|
|
existing_password = config.get_password()
|
|
|
|
|
except (InvalidToken, ValueError, RuntimeError):
|
|
|
|
|
existing_password = None
|
|
|
|
|
if not existing_password:
|
|
|
|
|
flash(
|
|
|
|
|
"Le mot de passe est obligatoire pour cette configuration.",
|
|
|
|
|
"danger",
|
|
|
|
|
)
|
|
|
|
|
return redirect(url_for('yeastar.config_page'))
|
2026-08-14 18:02:25 +02:00
|
|
|
|
|
|
|
|
config.updated_at = datetime.now(timezone.utc)
|
|
|
|
|
db.session.commit()
|
|
|
|
|
|
|
|
|
|
flash("Configuration Yeastar sauvegardée avec succès.", "success")
|
|
|
|
|
return redirect(url_for('yeastar.config_page'))
|
|
|
|
|
|
|
|
|
|
# Préparer les données pour l'affichage (ne pas afficher le mot de passe)
|
|
|
|
|
display_config = None
|
2026-08-14 19:33:05 +02:00
|
|
|
password_is_valid = False
|
2026-08-14 18:02:25 +02:00
|
|
|
if config:
|
2026-08-14 19:33:05 +02:00
|
|
|
try:
|
|
|
|
|
username = config.get_username()
|
|
|
|
|
config.get_password()
|
|
|
|
|
password_is_valid = True
|
|
|
|
|
except (InvalidToken, ValueError, RuntimeError):
|
|
|
|
|
username = ''
|
2026-08-14 18:02:25 +02:00
|
|
|
display_config = {
|
|
|
|
|
'base_url': config.base_url,
|
2026-08-14 19:33:05 +02:00
|
|
|
'username': username,
|
2026-08-14 18:02:25 +02:00
|
|
|
'is_active': config.is_active,
|
2026-08-14 19:33:05 +02:00
|
|
|
'updated_at': config.updated_at,
|
|
|
|
|
'password_is_valid': password_is_valid,
|
2026-08-14 18:02:25 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return render_template('yeastar/config.html', config=display_config)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@yeastar_bp.route('/config/test', methods=['POST'])
|
2026-08-14 19:33:05 +02:00
|
|
|
@system_admin_required
|
2026-08-14 18:02:25 +02:00
|
|
|
def test_connection():
|
|
|
|
|
"""Teste la connexion Yeastar avec les identifiants actuels."""
|
|
|
|
|
client, error = get_client()
|
|
|
|
|
|
|
|
|
|
if error:
|
|
|
|
|
return jsonify({"success": False, "error": error})
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
# Test simple : récupérer les extensions
|
|
|
|
|
extensions = client.get_extensions()
|
|
|
|
|
pbx_info = client.get_pbx_info()
|
|
|
|
|
client.logout()
|
|
|
|
|
|
|
|
|
|
return jsonify({
|
|
|
|
|
"success": True,
|
|
|
|
|
"extension_count": len(extensions),
|
|
|
|
|
"pbx_type": pbx_info.get('product_type', 'P520'),
|
|
|
|
|
"version": pbx_info.get('version', 'N/A')
|
|
|
|
|
})
|
|
|
|
|
except Exception as e:
|
|
|
|
|
return jsonify({"success": False, "error": str(e)})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@yeastar_bp.route('/dnd-config', methods=['GET', 'POST'])
|
|
|
|
|
@system_admin_required
|
|
|
|
|
def dnd_config_page():
|
|
|
|
|
"""Page de configuration du watchdog DND."""
|
|
|
|
|
config = YeastarDNDConfig.query.first()
|
|
|
|
|
if not config:
|
|
|
|
|
config = YeastarDNDConfig()
|
|
|
|
|
db.session.add(config)
|
|
|
|
|
db.session.commit()
|
|
|
|
|
|
|
|
|
|
if request.method == 'POST':
|
|
|
|
|
config.is_enabled = request.form.get('is_enabled') == 'on'
|
|
|
|
|
config.refresh_seconds = int(request.form.get('refresh_seconds', 1))
|
|
|
|
|
config.reset_delay_seconds = int(request.form.get('reset_delay_seconds', 3))
|
|
|
|
|
config.excluded_extensions = request.form.get('excluded_extensions', '')
|
|
|
|
|
config.updated_at = datetime.now(timezone.utc)
|
|
|
|
|
|
|
|
|
|
db.session.commit()
|
|
|
|
|
|
|
|
|
|
# Redémarrer le watchdog
|
|
|
|
|
try:
|
|
|
|
|
subprocess.run(['pkill', '-f', 'watchdog_dnd'], capture_output=True)
|
|
|
|
|
except:
|
|
|
|
|
pass
|
|
|
|
|
if config.is_enabled:
|
|
|
|
|
script_path = '/app/watchdog_dnd.py'
|
|
|
|
|
subprocess.Popen(['python3', script_path],
|
|
|
|
|
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
|
|
|
|
|
|
|
|
flash("Configuration DND sauvegardée", "success")
|
|
|
|
|
return redirect(url_for('yeastar.dnd_config_page'))
|
|
|
|
|
|
|
|
|
|
return render_template('yeastar/dnd_config.html', config=config)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@yeastar_bp.route('/api/extensions')
|
|
|
|
|
@login_required
|
|
|
|
|
def api_extensions():
|
|
|
|
|
"""API JSON pour les statuts des extensions."""
|
|
|
|
|
client, error = get_client()
|
|
|
|
|
|
|
|
|
|
if error:
|
|
|
|
|
return jsonify({"error": error}), 500
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
extensions = client.get_extensions()
|
|
|
|
|
pbx_info = client.get_pbx_info()
|
|
|
|
|
client.logout()
|
|
|
|
|
|
|
|
|
|
status_map = {
|
|
|
|
|
'available': 'Disponible',
|
|
|
|
|
'away': 'Absent',
|
|
|
|
|
'business_trip': 'En déplacement',
|
|
|
|
|
'do_not_disturb': 'Ne pas déranger',
|
|
|
|
|
'launch': 'Pause déjeuner',
|
|
|
|
|
'off_work': 'Hors service'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Status de connexion réseau
|
|
|
|
|
conn_map = {
|
|
|
|
|
0: ('Connecté', 'success'),
|
|
|
|
|
4: ('Non connecté', 'warning'),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ext_list = []
|
|
|
|
|
for ext in sorted(extensions, key=lambda x: int(x.get('ext_num', 0))):
|
|
|
|
|
status_code = ext.get('status', -1)
|
|
|
|
|
conn_info = conn_map.get(status_code, ('Inconnu', 'secondary'))
|
|
|
|
|
|
|
|
|
|
ext_list.append({
|
|
|
|
|
'num': ext['ext_num'],
|
|
|
|
|
'name': ext.get('caller_id_name', 'N/A'),
|
|
|
|
|
'status': ext.get('presence_status', 'unknown'),
|
|
|
|
|
'status_fr': status_map.get(ext.get('presence_status', ''), ext.get('presence_status', '')),
|
|
|
|
|
'status_code': status_code,
|
|
|
|
|
'conn_label': conn_info[0],
|
|
|
|
|
'conn_class': conn_info[1],
|
|
|
|
|
'ip': ext.get('ip_address', 'N/A'),
|
|
|
|
|
'device': ext.get('device', 'N/A')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return jsonify({
|
|
|
|
|
"pbx": pbx_info.get('product_type', 'P520'),
|
|
|
|
|
"version": pbx_info.get('version', 'N/A'),
|
|
|
|
|
"extensions": ext_list
|
|
|
|
|
})
|
|
|
|
|
except Exception as e:
|
|
|
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@yeastar_bp.route('/api/watchdog/toggle', methods=['POST'])
|
|
|
|
|
@system_admin_required
|
|
|
|
|
def api_watchdog_toggle():
|
|
|
|
|
"""Active ou désactive le watchdog."""
|
|
|
|
|
config = YeastarDNDConfig.query.first()
|
|
|
|
|
if config:
|
|
|
|
|
config.is_enabled = request.json.get('enabled', False)
|
|
|
|
|
db.session.commit()
|
|
|
|
|
return jsonify({"success": True, "enabled": config.is_enabled})
|
|
|
|
|
return jsonify({"error": "Config non trouvée"}), 404
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@yeastar_bp.route('/api/watchdog/status')
|
|
|
|
|
@login_required
|
|
|
|
|
def api_watchdog_status():
|
|
|
|
|
"""Statut du watchdog."""
|
|
|
|
|
config = YeastarDNDConfig.query.first()
|
|
|
|
|
if config:
|
|
|
|
|
return jsonify({
|
|
|
|
|
"enabled": config.is_enabled,
|
|
|
|
|
"refresh_seconds": config.refresh_seconds,
|
|
|
|
|
"reset_delay_seconds": config.reset_delay_seconds,
|
|
|
|
|
"excluded_extensions": config.excluded_extensions
|
|
|
|
|
})
|
|
|
|
|
return jsonify({"error": "Config non trouvée"}), 404
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@yeastar_bp.route('/api/watchdog/restart', methods=['POST'])
|
|
|
|
|
@system_admin_required
|
|
|
|
|
def api_watchdog_restart():
|
|
|
|
|
"""Redémarre le watchdog (tue et relance)."""
|
|
|
|
|
# Tuer les processus existants
|
|
|
|
|
try:
|
|
|
|
|
subprocess.run(['pkill', '-f', 'watchdog_dnd'], capture_output=True)
|
|
|
|
|
except:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
# Relancer
|
|
|
|
|
script_path = '/app/watchdog_dnd.py'
|
|
|
|
|
subprocess.Popen(['python3', script_path],
|
|
|
|
|
stdout=subprocess.DEVNULL,
|
|
|
|
|
stderr=subprocess.DEVNULL)
|
|
|
|
|
|
|
|
|
|
return jsonify({"success": True, "message": "Watchdog relancé"})
|