2025-04-27 19:45:45 +02:00

257 lines
8.6 KiB
Python

"""
Utilitaires partagés pour les scripts de contrôle des appareils Tuya.
Ce module fournit des fonctions communes utilisées par tous les scripts.
"""
import os
import sys
import json
import time
import logging
import tinytuya
from . import config
# Configuration du logging
logging.basicConfig(
level=logging.INFO if config.VERBOSE_MODE else logging.WARNING,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[logging.StreamHandler()]
)
logger = logging.getLogger("TuyaControl")
def load_device_info(device_id):
"""
Charge les informations d'un appareil depuis le fichier devices.json
Args:
device_id (str): ID de l'appareil Tuya
Returns:
dict: Informations de l'appareil ou None si non trouvé
"""
try:
# Chemin vers le répertoire parent du projet
base_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
devices_file = os.path.join(base_dir, 'devices.json')
with open(devices_file, 'r') as f:
devices = json.load(f)
for device in devices:
if device['id'] == device_id:
return device
logger.error(f"Appareil avec ID {device_id} non trouvé dans devices.json")
return None
except Exception as e:
logger.error(f"Erreur lors du chargement des informations de l'appareil: {e}")
return None
def connect_device(device_id, device_ip=None, device_key=None, device_version=3.3):
"""
Se connecte à un appareil Tuya
Args:
device_id (str): ID de l'appareil
device_ip (str, optional): IP de l'appareil (facultatif si mode scan)
device_key (str, optional): Clé de l'appareil (facultatif si chargé depuis devices.json)
device_version (float, optional): Version du protocole
Returns:
tinytuya.Device: Instance de l'appareil connecté ou None en cas d'erreur
"""
try:
# Si la clé n'est pas fournie, charger depuis devices.json
if device_key is None:
device_info = load_device_info(device_id)
if not device_info:
logger.error(f"Impossible de trouver les informations pour l'appareil {device_id}")
return None
device_key = device_info.get('key')
# Créer l'objet appareil
device = tinytuya.Device(
dev_id=device_id,
address=device_ip, # None si on veut que la bibliothèque recherche l'appareil
local_key=device_key,
version=device_version
)
# Configurer le timeout
device.set_socketTimeout(config.TIMEOUT)
# Tester la connexion
status = device.status()
if 'Error' in status:
logger.error(f"Erreur de connexion à l'appareil {device_id}: {status['Error']}")
return None
logger.info(f"Connexion réussie à l'appareil {device_id}")
return device
except Exception as e:
logger.error(f"Erreur lors de la connexion à l'appareil {device_id}: {e}")
return None
def toggle_device(device, switch_id="1", current_state=None):
"""
Bascule l'état d'un appareil (allumé/éteint)
Args:
device (tinytuya.Device): Instance de l'appareil
switch_id (str): ID du commutateur (généralement "1" ou "20" pour les lumières)
current_state (bool, optional): État actuel si connu, sinon sera détecté
Returns:
bool: Nouvel état (True=On, False=Off) ou None en cas d'erreur
"""
try:
# Si l'état actuel n'est pas fourni, l'obtenir
if current_state is None:
status = device.status()
if 'dps' in status and switch_id in status['dps']:
current_state = status['dps'][switch_id]
else:
logger.error(f"Impossible de déterminer l'état actuel du commutateur {switch_id}")
return None
# Basculer l'état (inverse de l'état actuel)
new_state = not current_state
result = device.set_value(switch_id, new_state)
logger.info(f"Appareil basculé de {current_state} à {new_state}")
return new_state
except Exception as e:
logger.error(f"Erreur lors du basculement de l'appareil: {e}")
return None
def control_shutter(device, action="stop"):
"""
Contrôle un volet roulant ou store
Args:
device (tinytuya.Device): Instance de l'appareil
action (str): Action à effectuer ("stop", "up"/"forward", "down"/"back")
Returns:
bool: True si réussi, False sinon
"""
try:
# Convertir les actions en commandes spécifiques au dispositif
command = action
if action == "up":
command = "forward"
elif action == "down":
command = "back"
# Les volets utilisent généralement le point de données "1" pour leurs commandes
result = device.set_value("1", command)
logger.info(f"Volet contrôlé avec commande: {command}")
return True
except Exception as e:
logger.error(f"Erreur lors du contrôle du volet: {e}")
return False
def get_device_status_text(device_id, status=None):
"""
Obtient un texte descriptif de l'état de l'appareil
Args:
device_id (str): ID de l'appareil
status (dict, optional): État actuel si déjà obtenu
Returns:
str: Description textuelle de l'état
"""
try:
device_info = load_device_info(device_id)
if not device_info:
return "Appareil inconnu"
# Si le statut n'est pas fourni, essayer de se connecter et obtenir l'état
if status is None:
device = connect_device(device_id)
if device:
status = device.status()
else:
return "Non connecté"
# Obtenir le nom de l'appareil
device_name = device_info.get('name', 'Appareil')
# Analyser l'état selon le type d'appareil
if 'dps' in status:
dps = status['dps']
# Pour les prises et la plupart des appareils on/off
if '1' in dps and isinstance(dps['1'], bool):
state = "ALLUMÉ" if dps['1'] else "ÉTEINT"
return f"{device_name}: {state}"
# Pour les lumières (généralement dps 20)
elif '20' in dps and isinstance(dps['20'], bool):
state = "ALLUMÉE" if dps['20'] else "ÉTEINTE"
return f"{device_name}: {state}"
# Pour les volets
elif '1' in dps and isinstance(dps['1'], str):
states = {
"stop": "ARRÊTÉ",
"forward": "EN OUVERTURE",
"back": "EN FERMETURE"
}
state = states.get(dps['1'], dps['1'].upper())
return f"{device_name}: {state}"
return f"{device_name}: État inconnu"
except Exception as e:
logger.error(f"Erreur lors de l'obtention de l'état de l'appareil: {e}")
return "Erreur"
def scan_for_devices():
"""
Recherche les appareils Tuya disponibles sur le réseau local
Returns:
list: Liste des appareils trouvés
"""
try:
logger.info("Recherche des appareils Tuya sur le réseau...")
devices = tinytuya.scan()
if devices is None:
devices = []
logger.info(f"Trouvé {len(devices)} appareils")
return devices
except Exception as e:
logger.error(f"Erreur lors de la recherche d'appareils: {e}")
return []
# Fonction pour l'intégration avec StreamDeck
def report_for_streamdeck(result, device_name="Appareil", new_state=None):
"""
Génère un rapport formaté pour l'intégration avec StreamDeck
Args:
result (bool): Résultat de l'opération
device_name (str): Nom de l'appareil
new_state: Nouvel état (True=On, False=Off, str pour d'autres états)
Returns:
dict: Rapport formaté
"""
# Format attendu par StreamDeck
report = {
"success": result is not None and result is not False,
"device": device_name,
"state": str(new_state) if new_state is not None else "unknown",
"timestamp": time.time()
}
# Afficher le rapport au format JSON pour StreamDeck
print(json.dumps(report))
return report