311 lines
12 KiB
Python
311 lines
12 KiB
Python
# -*- 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 <https://www.gnu.org/licenses/>.
|
||
|
||
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
|