pyMHCgui/pymhcgui.py

311 lines
12 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- 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 DADÉ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