Advertisement
Guest User

Untitled

a guest
Feb 25th, 2024
29
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 17.52 KB | None | 0 0
  1. # -*- coding: utf-8 -*-
  2.  
  3. import tkinter as tk
  4. from tkinter import ttk, Scale, Checkbutton
  5. from PIL import Image, ImageTk
  6. import plotly.graph_objects as go
  7. import random
  8. import io
  9. import plotly.io as pio
  10. pio.renderers.default = "svg"  # Tilføjet denne linje
  11.  
  12.  
  13. # Define the version number
  14. VERSION = "0.09.3.1"
  15.  
  16.  
  17. class VAVSimulationApp:
  18.     def __init__(self, root):
  19.         self.root = root
  20.         self.root.title("VAV Damper Simulation")
  21.         self.set_window_size()
  22.         self.voc_ppm_measured = tk.DoubleVar(value=0)
  23.         self.voc_ppm_display = tk.DoubleVar(value=0)  # Separate variable for display
  24.         self.temp_celsius_measured = tk.DoubleVar(value=20)
  25.         self.spj_opening = tk.DoubleVar(value=0)
  26.         self.inlet_temp = tk.DoubleVar(value=20)
  27.         self.voc_setpoint = tk.DoubleVar(value=700)
  28.         self.temp_setpoint = tk.DoubleVar(value=22)
  29.         self.simulation_interval = 5
  30.         self.voc_proportional_band = tk.DoubleVar(value=200)
  31.         self.temp_proportional_band = tk.DoubleVar(value=2.0)
  32.         self.integr_time = tk.DoubleVar(value=300)
  33.         self.update_interval_ms = tk.DoubleVar(value=5000)  # Default value of 5000 ms (5 seconds)
  34.         self.override_checkbox_var = tk.BooleanVar(value=False)
  35.  
  36.         # Variables for controlling data display in the graph
  37.         self.show_measured_voc = tk.BooleanVar(value=True)
  38.         self.show_measured_temp = tk.BooleanVar(value=True)
  39.         self.show_spj_opening = tk.BooleanVar(value=True)
  40.  
  41.         self.update_job = None
  42.         self.time_values = []
  43.         self.voc_values = []
  44.         self.temp_values = []
  45.         self.spj_opening_values = []
  46.  
  47.         self.setup_gui()
  48.  
  49.     def set_window_size(self):
  50.         # Set window size based on screen size
  51.         screen_width_mm = 344  # mm
  52.         screen_height_mm = 194  # mm
  53.         screen_resolution = (1920, 1780)  # Resolution in pixels (width, height)
  54.         screen_diagonal_inches = 15.6
  55.         screen_diagonal_mm = (screen_width_mm ** 2 + screen_height_mm ** 2) ** 0.5
  56.         screen_ppi = screen_resolution[0] / screen_diagonal_inches
  57.         window_width = int(screen_width_mm * screen_resolution[0] / screen_diagonal_mm)
  58.         window_height = int(screen_height_mm * screen_resolution[1] / screen_diagonal_mm)
  59.         self.root.geometry(f"{window_width}x{window_height}")
  60.  
  61.     def setup_gui(self):
  62.         # Create frame for input
  63.         input_frame = ttk.Frame(self.root)
  64.         input_frame.place(relx=0.05, rely=0.05, relwidth=0.9, relheight=0.3)
  65.  
  66.         # Create frame for graph
  67.         self.graph_frame = ttk.Frame(self.root)
  68.         self.graph_frame.place(relx=0.05, rely=0.4, relwidth=0.9, relheight=0.45)
  69.  
  70.         # Labels and entry for Measured VOC Level
  71.         ttk.Label(input_frame, text="Measured VOC Level (ppm):").grid(row=0, column=0, sticky="w")
  72.         ttk.Entry(input_frame, textvariable=self.voc_ppm_display, state='readonly').grid(row=0, column=1)
  73.         for i, increment in enumerate(["-10", "+10", "-100", "+100"]):
  74.             if increment == "+10":
  75.                 ttk.Button(input_frame, text=increment,
  76.                            command=lambda inc=10 if i < 2 else 100: self.increment_voc(inc)).grid(row=0, column=i + 2)
  77.             elif increment == "-10":
  78.                 ttk.Button(input_frame, text=increment,
  79.                            command=lambda inc=-10 if i < 2 else -100: self.increment_voc(inc)).grid(row=0, column=i + 2)
  80.             else:
  81.                 ttk.Button(input_frame, text=increment,
  82.                            command=lambda inc=100 if increment.startswith('+') else -100: self.increment_voc(inc)).grid(
  83.                     row=0, column=i + 2)
  84.  
  85.         # Labels and entry for Measured Temperature
  86.         ttk.Label(input_frame, text="Measured Temperature (\u00b0C):").grid(row=1, column=0, sticky="w")
  87.         ttk.Entry(input_frame, textvariable=self.temp_celsius_measured).grid(row=1, column=1)
  88.         for i, increment in enumerate(["-0.1", "+0.1", "-1", "+1"]):
  89.             if increment == "+0.1":
  90.                 ttk.Button(input_frame, text=increment,
  91.                            command=lambda inc=0.1 if i < 2 else 1: self.increment_temp(inc)).grid(row=1, column=i + 2)
  92.             elif increment == "-0.1":
  93.                 ttk.Button(input_frame, text=increment,
  94.                            command=lambda inc=-0.1 if i < 2 else -1: self.increment_temp(inc)).grid(row=1,
  95.                                                                                                     column=i + 2)
  96.             else:
  97.                 ttk.Button(input_frame, text=increment,
  98.                            command=lambda inc=1 if increment.startswith('+') else -1: self.increment_temp(
  99.                                inc)).grid(
  100.                     row=1, column=i + 2)
  101.  
  102.         # Labels and entry for Inlet Temperature, VOC Setpoint, Temperature Setpoint
  103.         labels_entries = [
  104.             ("Inlet Temperature (\u00b0C):", self.inlet_temp),
  105.             ("VOC Setpoint (ppm):", self.voc_setpoint),
  106.             ("Temperature Setpoint (\u00b0C):", self.temp_setpoint)
  107.         ]
  108.         for i, (label, variable) in enumerate(labels_entries):
  109.             ttk.Label(input_frame, text=label).grid(row=i + 2, column=0, sticky="w")
  110.             ttk.Entry(input_frame, textvariable=variable).grid(row=i + 2, column=1)
  111.  
  112.         # Buttons for automatic update, damper opening label, settings
  113.         buttons = [
  114.             ("Start Automatic Update", self.toggle_automatic_update),
  115.             ("Settings", self.open_settings_window)
  116.         ]
  117.         for i, (text, command) in enumerate(buttons):
  118.             ttk.Button(input_frame, text=text, command=command).grid(row=i + 5, columnspan=2)
  119.  
  120.         self.automatic_update_button = ttk.Button(input_frame, text="Start Automatic Update",
  121.                                                   command=self.toggle_automatic_update)
  122.         self.automatic_update_button.grid(row=5, columnspan=2)
  123.  
  124.         self.spj_opening_label = ttk.Label(input_frame, text="VAV Damper Opening: N/A")
  125.         self.spj_opening_label.grid(row=7, columnspan=2)
  126.  
  127.         self.settings_button = ttk.Button(input_frame, text="Settings", command=self.open_settings_window)
  128.         self.settings_button.grid(row=7, columnspan=2)
  129.  
  130.         # Frame for overriding damper opening
  131.         override_frame = ttk.Frame(self.root)
  132.         override_frame.place(relx=0.05, rely=0.9, relwidth=0.9, relheight=0.1)
  133.  
  134.         # Override slider and button
  135.         ttk.Label(override_frame, text="Override Damper Opening (%):").grid(row=0, column=0, sticky="w")
  136.         self.override_slider = Scale(override_frame, from_=0, to=100, orient=tk.HORIZONTAL,
  137.                                      length=400)  # Adjusted length
  138.         self.override_slider.set(0)
  139.         self.override_slider.grid(row=0, column=1)
  140.  
  141.         self.override_button = ttk.Button(override_frame, text="Enable Override", command=self.toggle_override)
  142.         self.override_button.grid(row=1, columnspan=2)
  143.  
  144.         # Checkboxes for displaying data in the graph
  145.         checkboxes = [
  146.             ("Show Measured VOC", self.show_measured_voc, 8),
  147.             ("Show Measured Temperature", self.show_measured_temp, 9),
  148.             ("Show Damper Opening", self.show_spj_opening, 10)
  149.         ]
  150.         for text, var, row in checkboxes:
  151.             ttk.Label(input_frame, text=text).grid(row=row, column=0, sticky="w")
  152.             Checkbutton(input_frame, variable=var, command=self.plot_graph).grid(row=row, column=1)
  153.  
  154.     def toggle_override(self):
  155.         if self.override_button["text"] == "Enable Override":
  156.             self.override_button.config(text="Disable Override")
  157.             self.override_slider.config(state=tk.NORMAL)
  158.             self.apply_override()  # Apply the override
  159.         else:
  160.             self.override_button.config(text="Enable Override")
  161.             self.override_slider.config(state=tk.DISABLED)
  162.             self.calculate_spj_opening()  # Calculate damper opening without override
  163.  
  164.     def apply_override(self):
  165.         overridden_opening = self.override_slider.get()
  166.         self.spj_opening.set(overridden_opening)
  167.         self.spj_opening_label.config(text=f"VAV Damper Opening: {self.spj_opening.get()}%")
  168.         self.override_checkbox_var.set(True)  # Update override variable
  169.         self.calculate_spj_opening()  # Calculate damper opening based on the new setting
  170.  
  171.     def calculate_spj_opening(self):
  172.         print("Calculating SPJ opening")
  173.         measured_voc = self.voc_ppm_measured.get()
  174.         voc_setpoint = self.voc_setpoint.get()
  175.         voc_proportional_band = self.voc_proportional_band.get()
  176.  
  177.         voc_deviation = measured_voc - voc_setpoint
  178.         lower_bound = voc_setpoint - voc_proportional_band / 2
  179.         upper_bound = voc_setpoint + voc_proportional_band / 2
  180.  
  181.         if lower_bound < measured_voc < upper_bound:
  182.             proportion = (measured_voc - lower_bound) / voc_proportional_band
  183.             self.spj_opening.set(proportion * 100)
  184.         elif measured_voc <= lower_bound:
  185.             self.spj_opening.set(0)
  186.         else:
  187.             self.spj_opening.set(100)
  188.  
  189.         self.spj_opening_label.config(text=f"VAV Damper Opening: {self.spj_opening.get()}%")
  190.  
  191.         self.update_measured_values()
  192.  
  193.         if self.time_values:
  194.             self.time_values.append(self.time_values[-1] + self.simulation_interval)
  195.         else:
  196.             self.time_values.append(self.simulation_interval)
  197.  
  198.         self.voc_values.append(measured_voc)
  199.         self.temp_values.append(self.temp_celsius_measured.get())
  200.         self.spj_opening_values.append(self.spj_opening.get())
  201.  
  202.         if len(self.time_values) > 60:
  203.             self.time_values.pop(0)
  204.             self.voc_values.pop(0)
  205.             self.temp_values.pop(0)
  206.             self.spj_opening_values.pop(0)
  207.  
  208.         self.plot_graph()
  209.  
  210.     def update_measured_values(self):
  211.         measured_temp = self.temp_celsius_measured.get()
  212.         inlet_temp = self.inlet_temp.get()
  213.  
  214.         # Simulate a more realistic temperature change
  215.         if measured_temp < inlet_temp:
  216.             # Simulate a gradual rise in temperature
  217.             increment = random.uniform(0, 0.1)
  218.             measured_temp += increment
  219.             if measured_temp > inlet_temp:
  220.                 measured_temp = inlet_temp  # Ensure measured temperature does not exceed inlet temperature
  221.         elif measured_temp > inlet_temp:
  222.             # Simulate a gradual decrease in temperature
  223.             decrement = random.uniform(0, 0.1)
  224.             measured_temp -= decrement
  225.             if measured_temp < inlet_temp:
  226.                 measured_temp = inlet_temp  # Ensure measured temperature does not fall below inlet temperature
  227.  
  228.         # Update measured temperature
  229.         self.temp_celsius_measured.set(round(measured_temp, 1))
  230.  
  231.         # Update VOC values
  232.         current_voc_level = self.voc_ppm_measured.get()
  233.         voc_reduction_rate = self.spj_opening.get() / 100.0
  234.         new_voc_level = max(current_voc_level - voc_reduction_rate * random.uniform(0, 5),
  235.                             420)  # Ensure VOC does not fall below 420 ppm
  236.         self.voc_ppm_measured.set(round(new_voc_level))
  237.         self.voc_ppm_display.set(round(new_voc_level))  # Update the display variable
  238.  
  239.     def toggle_automatic_update(self):
  240.         if not self.update_job:
  241.             self.automatic_update_button.config(text="Stop Automatic Update")
  242.             self.periodic_update()
  243.         else:
  244.             self.automatic_update_button.config(text="Start Automatic Update")
  245.             self.root.after_cancel(self.update_job)
  246.             self.update_job = None
  247.  
  248.     def periodic_update(self):
  249.         self.calculate_spj_opening()  # Update damper opening regardless of whether override is enabled or not
  250.         self.update_job = self.root.after(int(self.update_interval_ms.get()), self.periodic_update)
  251.  
  252.     def plot_graph(self):
  253.         if len(self.time_values) > 0:
  254.             time_range = range(max(0, len(self.time_values) - 60), len(self.time_values))
  255.  
  256.             # Create trace for measured VOC if enabled
  257.             if self.show_measured_voc.get():
  258.                 trace_voc = go.Scatter(x=[self.time_values[i] for i in time_range],
  259.                                        y=[self.voc_values[i] for i in time_range],
  260.                                        mode='lines',
  261.                                        name='Measured VOC Level (ppm)')
  262.             else:
  263.                 trace_voc = None
  264.  
  265.             # Create trace for measured temperature if enabled
  266.             if self.show_measured_temp.get():
  267.                 trace_temp = go.Scatter(x=[self.time_values[i] for i in time_range],
  268.                                         y=[self.temp_values[i] for i in time_range],
  269.                                         mode='lines',
  270.                                         name='Measured Temperature (\u00b0C)',
  271.                                         yaxis='y2')  # Display on the second y-axis
  272.             else:
  273.                 trace_temp = None
  274.  
  275.             # Create trace for damper opening if enabled
  276.             if self.show_spj_opening.get():
  277.                 trace_spj = go.Scatter(x=[self.time_values[i] for i in time_range],
  278.                                        y=[self.spj_opening_values[i] for i in time_range],
  279.                                        mode='lines',
  280.                                        name='Damper Opening (%)',
  281.                                        yaxis='y3')  # Display on a separate y-axis
  282.             else:
  283.                 trace_spj = None
  284.  
  285.             # Create figure object with the enabled traces
  286.             fig = go.Figure(data=[trace for trace in [trace_voc, trace_temp, trace_spj] if trace])
  287.  
  288.             # Update layout (same as before)
  289.             fig.update_layout(
  290.                 title="VAV Damper Simulation",
  291.                 xaxis_title="Time",
  292.                 yaxis=dict(side='left', title='Measured VOC Level (ppm)', range=[300, 1500]),
  293.                 yaxis2=dict(
  294.                     side='right',
  295.                     overlaying='y',
  296.                     showgrid=False,
  297.                     range=[0, 100],
  298.                     tickfont=dict(size=12),
  299.                     title=None,
  300.                     tickvals=[0, 20, 40, 60, 80, 100]
  301.                 ),
  302.                 yaxis3=dict(
  303.                     side='right',
  304.                     overlaying='y',
  305.                     showgrid=False,
  306.                     range=[0, 100],
  307.                     tickfont=dict(size=12),
  308.                     title=None,
  309.                     tickvals=[0, 20, 40, 60, 80, 100]
  310.                 ),
  311.                 margin=dict(l=100, r=200, t=80, b=80),
  312.                 width=1200,
  313.                 legend=dict(orientation="h", x=0.5, xanchor="center", y=-0.1, yanchor="top")
  314.             )
  315.  
  316.             # Save the figure as a PNG image in memory (same as before)
  317.             img_bytes = fig.to_image(format="png")
  318.             img = Image.open(io.BytesIO(img_bytes))
  319.             photo = ImageTk.PhotoImage(img)
  320.  
  321.             if hasattr(self, 'image_label'):
  322.                 self.image_label.config(image=photo)
  323.                 self.image_label.image = photo
  324.             else:
  325.                 self.image_label = tk.Label(self.graph_frame, image=photo)
  326.                 self.image_label.image = photo
  327.                 self.image_label.pack(fill="both", expand=True)
  328.  
  329.     def open_settings_window(self):
  330.         settings_window = tk.Toplevel(self.root)
  331.         settings_window.title("Settings")
  332.         settings_window.geometry("300x300")
  333.  
  334.         ttk.Label(settings_window, text="VOC Proportional Band:").grid(row=0, column=0, sticky="w")
  335.         ttk.Entry(settings_window, textvariable=self.voc_proportional_band).grid(row=0, column=1)
  336.  
  337.         ttk.Label(settings_window, text="Temperature Proportional Band:").grid(row=1, column=0, sticky="w")
  338.         ttk.Entry(settings_window, textvariable=self.temp_proportional_band).grid(row=1, column=1)
  339.  
  340.         ttk.Label(settings_window, text="VOC Setpoint (ppm):").grid(row=2, column=0, sticky="w")
  341.         ttk.Entry(settings_window, textvariable=self.voc_setpoint).grid(row=2, column=1)
  342.  
  343.         ttk.Label(settings_window, text="Temperature Setpoint (\u00b0C):").grid(row=3, column=0, sticky="w")
  344.         ttk.Entry(settings_window, textvariable=self.temp_setpoint).grid(row=3, column=1)
  345.  
  346.         ttk.Label(settings_window, text="Integration Time (s):").grid(row=4, column=0, sticky="w")
  347.         ttk.Entry(settings_window, textvariable=self.integr_time).grid(row=4, column=1)
  348.  
  349.         ttk.Label(settings_window, text="Update Interval (ms):").grid(row=5, column=0, sticky="w")
  350.         ttk.Entry(settings_window, textvariable=self.update_interval_ms).grid(row=5, column=1)
  351.  
  352.         # Display the version number
  353.         ttk.Label(settings_window, text="Version: " + VERSION).grid(row=6, columnspan=2, pady=5)
  354.  
  355.     def increment_voc(self, value):
  356.         new_voc = self.voc_ppm_measured.get() + value
  357.         self.voc_ppm_measured.set(new_voc)
  358.         self.voc_ppm_display.set(new_voc)
  359.  
  360.     def decrement_voc(self, value):
  361.         new_voc = self.voc_ppm_measured.get() - value
  362.         self.voc_ppm_measured.set(new_voc)
  363.         self.voc_ppm_display.set(new_voc)
  364.  
  365.     def increment_temp(self, value):
  366.         new_temp = self.temp_celsius_measured.get() + value
  367.         self.temp_celsius_measured.set(new_temp)
  368.  
  369.     def decrement_temp(self, value):
  370.         new_temp = self.temp_celsius_measured.get() - value
  371.         self.temp_celsius_measured.set(new_temp)
  372.  
  373.  
  374. if __name__ == "__main__":
  375.     root = tk.Tk()
  376.     app = VAVSimulationApp(root)
  377.     root.mainloop()
  378.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement