# -*- coding: utf-8 -*- # # Nom du projet : pyMHCgui # Description : Interface de développement et débogage des contrôleurs de chauffe MHC # # Copyright (C) 2025 Jonathan Roth / 462engineering # # Ce programme est un logiciel libre : vous pouvez le redistribuer et/ou # le modifier selon les termes de la licence GNU Affero General Public License # publiée par la Free Software Foundation, soit la version 3 de la licence, # soit (à votre choix) toute version ultérieure. # # Ce programme est distribué dans l'espoir qu'il sera utile, # mais SANS AUCUNE GARANTIE ; sans même la garantie implicite de # QUALITÉ MARCHANDE ou D’ADÉQUATION À UN USAGE PARTICULIER. # Consultez la Licence GNU Affero General Public License pour plus de détails. # # Vous devriez avoir reçu une copie de la Licence GNU Affero General Public License # avec ce programme. Sinon, consultez . import tkinter as tk from tkinter import messagebox import threading import socket import json import time # Variable pour stocker l'adresse IP server_ip = "192.168.1.111" # Adresse IP par défaut # Fonction pour envoyer la commande de démarrage ou d'arrêt de la chauffe def toggle_heating_state(heating_button): try: global server_ip if server_ip is None: raise ValueError("L'adresse IP du serveur n'est pas définie.") port = 4623 # Port du serveur btcolor = heating_button.cget("bg") current_state = 0 if btcolor == "gray" else 1 # Si la chauffe est active (vert), envoyer la commande "stop" if current_state == 1: # print("stop") command = "stop" heating_button.config(bg="gray", text="Chauffe inactive") # Si la chauffe est inactive (gris), envoyer la commande "start" else: # print("start") command = "start" heating_button.config(bg="green", text="Chauffe active") # Créer la commande à envoyer au serveur with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.connect((server_ip, port)) # Envoi de la commande s.sendall(command.encode('utf-8')) # print(f"Commande envoyée : {command}") except Exception as e: print(f"Erreur lors de l'envoi de la commande de chauffe : {e}") # Fonction pour mettre à jour la couleur du bouton "Chauffe active" def update_heating_button(heating_button, state): if state == "IDLE": heating_button.config(bg="gray", text="Chauffe inactive") return "inactive" elif state == "ERROR": heating_button.config(bg="red", text="Erreur de chauffe") return "error" else: heating_button.config(bg="green", text="Chauffe active") return "active" # Fonction pour récupérer les données depuis la socket def start_socket_listener(update_widgets): try: global server_ip if server_ip is None: raise ValueError("L'adresse IP du serveur n'est pas définie.") port = 4623 # Port du serveur with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.connect((server_ip, port)) while True: try: # Envoi de la commande "get_json" s.sendall(b"get_json") # Lecture des données data = s.recv(1024) # Taille max des données reçues if not data: break # Convertir les données en JSON json_data = json.loads(data.decode('utf-8')) # Mise à jour des widgets update_widgets(json_data) except json.JSONDecodeError: update_widgets({"error": "Données JSON non valides"}) time.sleep(1) except Exception as e: update_widgets({"error": f"Erreur : {e}"}) # Fonction pour mettre à jour l'interface avec les données JSON def update_interface_with_json(json_data, label, controller_buttons, enabled_buttons, temp_frame, heating_button): """ Met à jour les widgets dans l'interface en fonction des données JSON reçues. """ try: # Mise à jour des informations générales status = json_data.get("status", "Indisponible") state = json_data.get("state", "Indisponible") current_temp = json_data.get("t", "Indisponible") target_temp = json_data.get("target", "Indisponible") # Texte principal label.config( text=f"Status : {status}\n" f"État : {state}\n" f"Température actuelle : {current_temp} °C\n" f"Température cible : {target_temp} °C" ) # Mise à jour des boutons pour controllers controllers = json_data.get("controllers", []) for i, state in enumerate(controllers): color = "green" if state else "gray" controller_buttons[i].config(bg=color, text=f"Carte OK {i}") # Mise à jour des boutons pour enabled enabled = json_data.get("enabled", []) for i, state in enumerate(enabled): color = "green" if state else "gray" enabled_buttons[i].config(bg=color, text=f"Zone active {i}") # Mise à jour du tableau des températures temps = json_data.get("temps", []) display_temperatures(temps, temp_frame) # Mise à jour de la couleur du bouton "Chauffe active" heating_state = json_data.get("state", []) update_heating_button(heating_button, heating_state) # Retourner l'état de chauffe actuel return heating_state except Exception as e: label.config(text=f"Erreur d'analyse : {e}") return "error" # Fonction pour quitter proprement def on_close(root): if messagebox.askokcancel("Quitter", "Voulez-vous vraiment quitter ?"): root.destroy() # Fonction pour afficher les températures dans un tableau def display_temperatures(temps, temp_frame): for widget in temp_frame.winfo_children(): widget.destroy() for i in range(0, len(temps), 4): row = tk.Frame(temp_frame) row.pack(pady=5) for j in range(4): if i + j < len(temps): temp_label = tk.Label(row, text=f"{temps[i + j]:.1f} °C", width=10) temp_label.pack(side="left", padx=5) # Fonction pour ouvrir une fenêtre de modification de la température cible def open_target_window(): def update_target(): # Récupérer la température entrée par l'utilisateur try: new_target = float(target_entry.get()) # Envoyer la commande avec la nouvelle température command = f"set_target {new_target}" with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.connect((server_ip, 4623)) s.sendall(command.encode('utf-8')) # Fermer la fenêtre après avoir envoyé la commande target_window.destroy() except ValueError: messagebox.showerror("Erreur", "Veuillez entrer une température valide.") # Création de la fenêtre pour changer la température cible target_window = tk.Toplevel() target_window.title("Modifier Température Cible") target_window.geometry("300x150") # Ajouter un champ de saisie pour la nouvelle température target_label = tk.Label(target_window, text="Nouvelle température cible :") target_label.pack(pady=10) target_entry = tk.Entry(target_window) target_entry.pack(pady=5) # Bouton pour valider la température confirm_button = tk.Button(target_window, text="Valider", command=update_target) confirm_button.pack(pady=10) # Fonction pour activer ou désactiver une zone en envoyant une commande def toggle_zone(zone_number, enabled_buttons): try: global server_ip if server_ip is None: raise ValueError("L'adresse IP du serveur n'est pas définie.") # Vérifier l'état actuel de la zone current_state = enabled_buttons[zone_number].cget("bg") new_state = 1 if current_state == "gray" else 0 # Envoi de la commande "set_zone" command = f"set_zone {zone_number} {new_state}" with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.connect((server_ip, 4623)) s.sendall(command.encode('utf-8')) # Mettre à jour la couleur du bouton en fonction de l'état new_color = "green" if new_state == 1 else "gray" enabled_buttons[zone_number].config(bg=new_color, text=f"Zone active {zone_number}") except Exception as e: print(f"Erreur lors du changement de l'état de la zone {zone_number}: {e}") # Fenêtre de configuration de l'adresse IP def configure_ip(): def submit_ip(): global server_ip ip = ip_entry.get() try: # Vérification simple de l'adresse IP (vous pouvez ajouter une validation plus complète) socket.inet_aton(ip) server_ip = ip ip_window.destroy() create_app() # Lancer l'application principale except socket.error: messagebox.showerror("Erreur", "Adresse IP invalide. Veuillez entrer une adresse IP valide.") ip_window = tk.Tk() ip_window.title("Configuration Adresse IP") ip_window.geometry("400x200") ip_label = tk.Label(ip_window, text="Entrez l'adresse IP du serveur :") ip_label.pack(pady=10) ip_entry = tk.Entry(ip_window) ip_entry.pack(pady=5) ip_entry.insert(0, server_ip) # Remplir avec l'IP par défaut submit_button = tk.Button(ip_window, text="Valider", command=submit_ip) submit_button.pack(pady=20) ip_window.mainloop() # Fonction principale pour créer l'interface def create_app(): # Créer la fenêtre principale root = tk.Tk() root.title("MHC Client") root.geometry("800x600") # Frame pour afficher les informations générales label = tk.Label(root, text="Données non reçues", font=("Arial", 12)) label.pack(pady=20) # Créer des boutons pour les contrôleurs (Carte OK) controller_buttons = [] controller_frame = tk.Frame(root) controller_frame.pack(pady=10) for i in range(2): button = tk.Button(controller_frame, text=f"Carte OK {i}", width=15, height=2) button.pack(side="left", padx=10) controller_buttons.append(button) # Créer des boutons pour les zones enabled_buttons = [] enabled_frame = tk.Frame(root) enabled_frame.pack(pady=10) for i in range(4): button = tk.Button(enabled_frame, text=f"Zone active {i}", width=15, height=2, command=lambda i=i: toggle_zone(i, enabled_buttons)) button.pack(side="left", padx=10) enabled_buttons.append(button) # Frame pour afficher les températures temp_frame = tk.Frame(root) temp_frame.pack(pady=20) # Bouton pour changer la température cible target_button = tk.Button(root, text="Changer la température cible", command=open_target_window) target_button.pack(pady=10) # Bouton pour gérer la chauffe heating_button = tk.Button(root, text="Chauffe inactive", bg="gray", width=15, height=2, command=lambda: toggle_heating_state(heating_button)) heating_button.pack(pady=10) # Fonction de mise à jour des widgets après chaque lecture def update_widgets(json_data): heating_state = update_interface_with_json(json_data, label, controller_buttons, enabled_buttons, temp_frame, heating_button) return heating_state # Démarrer un thread pour écouter les données de la socket threading.Thread(target=start_socket_listener, args=(update_widgets,), daemon=True).start() # Fonction pour quitter proprement root.protocol("WM_DELETE_WINDOW", lambda: on_close(root)) # Boucle principale de l'application root.mainloop() if __name__ == "__main__": configure_ip() # Ouvrir la fenêtre de configuration de l'IP avant de lancer l'application