""" 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