Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- # -*- coding: utf-8 -*-
- import tkinter as tk
- from tkinter import ttk, Scale, Checkbutton
- from PIL import Image, ImageTk
- import plotly.graph_objects as go
- import random
- import io
- import plotly.io as pio
- pio.renderers.default = "svg" # Tilføjet denne linje
- # Define the version number
- VERSION = "0.09.3.1"
- class VAVSimulationApp:
- def __init__(self, root):
- self.root = root
- self.root.title("VAV Damper Simulation")
- self.set_window_size()
- self.voc_ppm_measured = tk.DoubleVar(value=0)
- self.voc_ppm_display = tk.DoubleVar(value=0) # Separate variable for display
- self.temp_celsius_measured = tk.DoubleVar(value=20)
- self.spj_opening = tk.DoubleVar(value=0)
- self.inlet_temp = tk.DoubleVar(value=20)
- self.voc_setpoint = tk.DoubleVar(value=700)
- self.temp_setpoint = tk.DoubleVar(value=22)
- self.simulation_interval = 5
- self.voc_proportional_band = tk.DoubleVar(value=200)
- self.temp_proportional_band = tk.DoubleVar(value=2.0)
- self.integr_time = tk.DoubleVar(value=300)
- self.update_interval_ms = tk.DoubleVar(value=5000) # Default value of 5000 ms (5 seconds)
- self.override_checkbox_var = tk.BooleanVar(value=False)
- # Variables for controlling data display in the graph
- self.show_measured_voc = tk.BooleanVar(value=True)
- self.show_measured_temp = tk.BooleanVar(value=True)
- self.show_spj_opening = tk.BooleanVar(value=True)
- self.update_job = None
- self.time_values = []
- self.voc_values = []
- self.temp_values = []
- self.spj_opening_values = []
- self.setup_gui()
- def set_window_size(self):
- # Set window size based on screen size
- screen_width_mm = 344 # mm
- screen_height_mm = 194 # mm
- screen_resolution = (1920, 1780) # Resolution in pixels (width, height)
- screen_diagonal_inches = 15.6
- screen_diagonal_mm = (screen_width_mm ** 2 + screen_height_mm ** 2) ** 0.5
- screen_ppi = screen_resolution[0] / screen_diagonal_inches
- window_width = int(screen_width_mm * screen_resolution[0] / screen_diagonal_mm)
- window_height = int(screen_height_mm * screen_resolution[1] / screen_diagonal_mm)
- self.root.geometry(f"{window_width}x{window_height}")
- def setup_gui(self):
- # Create frame for input
- input_frame = ttk.Frame(self.root)
- input_frame.place(relx=0.05, rely=0.05, relwidth=0.9, relheight=0.3)
- # Create frame for graph
- self.graph_frame = ttk.Frame(self.root)
- self.graph_frame.place(relx=0.05, rely=0.4, relwidth=0.9, relheight=0.45)
- # Labels and entry for Measured VOC Level
- ttk.Label(input_frame, text="Measured VOC Level (ppm):").grid(row=0, column=0, sticky="w")
- ttk.Entry(input_frame, textvariable=self.voc_ppm_display, state='readonly').grid(row=0, column=1)
- for i, increment in enumerate(["-10", "+10", "-100", "+100"]):
- if increment == "+10":
- ttk.Button(input_frame, text=increment,
- command=lambda inc=10 if i < 2 else 100: self.increment_voc(inc)).grid(row=0, column=i + 2)
- elif increment == "-10":
- ttk.Button(input_frame, text=increment,
- command=lambda inc=-10 if i < 2 else -100: self.increment_voc(inc)).grid(row=0, column=i + 2)
- else:
- ttk.Button(input_frame, text=increment,
- command=lambda inc=100 if increment.startswith('+') else -100: self.increment_voc(inc)).grid(
- row=0, column=i + 2)
- # Labels and entry for Measured Temperature
- ttk.Label(input_frame, text="Measured Temperature (\u00b0C):").grid(row=1, column=0, sticky="w")
- ttk.Entry(input_frame, textvariable=self.temp_celsius_measured).grid(row=1, column=1)
- for i, increment in enumerate(["-0.1", "+0.1", "-1", "+1"]):
- if increment == "+0.1":
- ttk.Button(input_frame, text=increment,
- command=lambda inc=0.1 if i < 2 else 1: self.increment_temp(inc)).grid(row=1, column=i + 2)
- elif increment == "-0.1":
- ttk.Button(input_frame, text=increment,
- command=lambda inc=-0.1 if i < 2 else -1: self.increment_temp(inc)).grid(row=1,
- column=i + 2)
- else:
- ttk.Button(input_frame, text=increment,
- command=lambda inc=1 if increment.startswith('+') else -1: self.increment_temp(
- inc)).grid(
- row=1, column=i + 2)
- # Labels and entry for Inlet Temperature, VOC Setpoint, Temperature Setpoint
- labels_entries = [
- ("Inlet Temperature (\u00b0C):", self.inlet_temp),
- ("VOC Setpoint (ppm):", self.voc_setpoint),
- ("Temperature Setpoint (\u00b0C):", self.temp_setpoint)
- ]
- for i, (label, variable) in enumerate(labels_entries):
- ttk.Label(input_frame, text=label).grid(row=i + 2, column=0, sticky="w")
- ttk.Entry(input_frame, textvariable=variable).grid(row=i + 2, column=1)
- # Buttons for automatic update, damper opening label, settings
- buttons = [
- ("Start Automatic Update", self.toggle_automatic_update),
- ("Settings", self.open_settings_window)
- ]
- for i, (text, command) in enumerate(buttons):
- ttk.Button(input_frame, text=text, command=command).grid(row=i + 5, columnspan=2)
- self.automatic_update_button = ttk.Button(input_frame, text="Start Automatic Update",
- command=self.toggle_automatic_update)
- self.automatic_update_button.grid(row=5, columnspan=2)
- self.spj_opening_label = ttk.Label(input_frame, text="VAV Damper Opening: N/A")
- self.spj_opening_label.grid(row=7, columnspan=2)
- self.settings_button = ttk.Button(input_frame, text="Settings", command=self.open_settings_window)
- self.settings_button.grid(row=7, columnspan=2)
- # Frame for overriding damper opening
- override_frame = ttk.Frame(self.root)
- override_frame.place(relx=0.05, rely=0.9, relwidth=0.9, relheight=0.1)
- # Override slider and button
- ttk.Label(override_frame, text="Override Damper Opening (%):").grid(row=0, column=0, sticky="w")
- self.override_slider = Scale(override_frame, from_=0, to=100, orient=tk.HORIZONTAL,
- length=400) # Adjusted length
- self.override_slider.set(0)
- self.override_slider.grid(row=0, column=1)
- self.override_button = ttk.Button(override_frame, text="Enable Override", command=self.toggle_override)
- self.override_button.grid(row=1, columnspan=2)
- # Checkboxes for displaying data in the graph
- checkboxes = [
- ("Show Measured VOC", self.show_measured_voc, 8),
- ("Show Measured Temperature", self.show_measured_temp, 9),
- ("Show Damper Opening", self.show_spj_opening, 10)
- ]
- for text, var, row in checkboxes:
- ttk.Label(input_frame, text=text).grid(row=row, column=0, sticky="w")
- Checkbutton(input_frame, variable=var, command=self.plot_graph).grid(row=row, column=1)
- def toggle_override(self):
- if self.override_button["text"] == "Enable Override":
- self.override_button.config(text="Disable Override")
- self.override_slider.config(state=tk.NORMAL)
- self.apply_override() # Apply the override
- else:
- self.override_button.config(text="Enable Override")
- self.override_slider.config(state=tk.DISABLED)
- self.calculate_spj_opening() # Calculate damper opening without override
- def apply_override(self):
- overridden_opening = self.override_slider.get()
- self.spj_opening.set(overridden_opening)
- self.spj_opening_label.config(text=f"VAV Damper Opening: {self.spj_opening.get()}%")
- self.override_checkbox_var.set(True) # Update override variable
- self.calculate_spj_opening() # Calculate damper opening based on the new setting
- def calculate_spj_opening(self):
- print("Calculating SPJ opening")
- measured_voc = self.voc_ppm_measured.get()
- voc_setpoint = self.voc_setpoint.get()
- voc_proportional_band = self.voc_proportional_band.get()
- voc_deviation = measured_voc - voc_setpoint
- lower_bound = voc_setpoint - voc_proportional_band / 2
- upper_bound = voc_setpoint + voc_proportional_band / 2
- if lower_bound < measured_voc < upper_bound:
- proportion = (measured_voc - lower_bound) / voc_proportional_band
- self.spj_opening.set(proportion * 100)
- elif measured_voc <= lower_bound:
- self.spj_opening.set(0)
- else:
- self.spj_opening.set(100)
- self.spj_opening_label.config(text=f"VAV Damper Opening: {self.spj_opening.get()}%")
- self.update_measured_values()
- if self.time_values:
- self.time_values.append(self.time_values[-1] + self.simulation_interval)
- else:
- self.time_values.append(self.simulation_interval)
- self.voc_values.append(measured_voc)
- self.temp_values.append(self.temp_celsius_measured.get())
- self.spj_opening_values.append(self.spj_opening.get())
- if len(self.time_values) > 60:
- self.time_values.pop(0)
- self.voc_values.pop(0)
- self.temp_values.pop(0)
- self.spj_opening_values.pop(0)
- self.plot_graph()
- def update_measured_values(self):
- measured_temp = self.temp_celsius_measured.get()
- inlet_temp = self.inlet_temp.get()
- # Simulate a more realistic temperature change
- if measured_temp < inlet_temp:
- # Simulate a gradual rise in temperature
- increment = random.uniform(0, 0.1)
- measured_temp += increment
- if measured_temp > inlet_temp:
- measured_temp = inlet_temp # Ensure measured temperature does not exceed inlet temperature
- elif measured_temp > inlet_temp:
- # Simulate a gradual decrease in temperature
- decrement = random.uniform(0, 0.1)
- measured_temp -= decrement
- if measured_temp < inlet_temp:
- measured_temp = inlet_temp # Ensure measured temperature does not fall below inlet temperature
- # Update measured temperature
- self.temp_celsius_measured.set(round(measured_temp, 1))
- # Update VOC values
- current_voc_level = self.voc_ppm_measured.get()
- voc_reduction_rate = self.spj_opening.get() / 100.0
- new_voc_level = max(current_voc_level - voc_reduction_rate * random.uniform(0, 5),
- 420) # Ensure VOC does not fall below 420 ppm
- self.voc_ppm_measured.set(round(new_voc_level))
- self.voc_ppm_display.set(round(new_voc_level)) # Update the display variable
- def toggle_automatic_update(self):
- if not self.update_job:
- self.automatic_update_button.config(text="Stop Automatic Update")
- self.periodic_update()
- else:
- self.automatic_update_button.config(text="Start Automatic Update")
- self.root.after_cancel(self.update_job)
- self.update_job = None
- def periodic_update(self):
- self.calculate_spj_opening() # Update damper opening regardless of whether override is enabled or not
- self.update_job = self.root.after(int(self.update_interval_ms.get()), self.periodic_update)
- def plot_graph(self):
- if len(self.time_values) > 0:
- time_range = range(max(0, len(self.time_values) - 60), len(self.time_values))
- # Create trace for measured VOC if enabled
- if self.show_measured_voc.get():
- trace_voc = go.Scatter(x=[self.time_values[i] for i in time_range],
- y=[self.voc_values[i] for i in time_range],
- mode='lines',
- name='Measured VOC Level (ppm)')
- else:
- trace_voc = None
- # Create trace for measured temperature if enabled
- if self.show_measured_temp.get():
- trace_temp = go.Scatter(x=[self.time_values[i] for i in time_range],
- y=[self.temp_values[i] for i in time_range],
- mode='lines',
- name='Measured Temperature (\u00b0C)',
- yaxis='y2') # Display on the second y-axis
- else:
- trace_temp = None
- # Create trace for damper opening if enabled
- if self.show_spj_opening.get():
- trace_spj = go.Scatter(x=[self.time_values[i] for i in time_range],
- y=[self.spj_opening_values[i] for i in time_range],
- mode='lines',
- name='Damper Opening (%)',
- yaxis='y3') # Display on a separate y-axis
- else:
- trace_spj = None
- # Create figure object with the enabled traces
- fig = go.Figure(data=[trace for trace in [trace_voc, trace_temp, trace_spj] if trace])
- # Update layout (same as before)
- fig.update_layout(
- title="VAV Damper Simulation",
- xaxis_title="Time",
- yaxis=dict(side='left', title='Measured VOC Level (ppm)', range=[300, 1500]),
- yaxis2=dict(
- side='right',
- overlaying='y',
- showgrid=False,
- range=[0, 100],
- tickfont=dict(size=12),
- title=None,
- tickvals=[0, 20, 40, 60, 80, 100]
- ),
- yaxis3=dict(
- side='right',
- overlaying='y',
- showgrid=False,
- range=[0, 100],
- tickfont=dict(size=12),
- title=None,
- tickvals=[0, 20, 40, 60, 80, 100]
- ),
- margin=dict(l=100, r=200, t=80, b=80),
- width=1200,
- legend=dict(orientation="h", x=0.5, xanchor="center", y=-0.1, yanchor="top")
- )
- # Save the figure as a PNG image in memory (same as before)
- img_bytes = fig.to_image(format="png")
- img = Image.open(io.BytesIO(img_bytes))
- photo = ImageTk.PhotoImage(img)
- if hasattr(self, 'image_label'):
- self.image_label.config(image=photo)
- self.image_label.image = photo
- else:
- self.image_label = tk.Label(self.graph_frame, image=photo)
- self.image_label.image = photo
- self.image_label.pack(fill="both", expand=True)
- def open_settings_window(self):
- settings_window = tk.Toplevel(self.root)
- settings_window.title("Settings")
- settings_window.geometry("300x300")
- ttk.Label(settings_window, text="VOC Proportional Band:").grid(row=0, column=0, sticky="w")
- ttk.Entry(settings_window, textvariable=self.voc_proportional_band).grid(row=0, column=1)
- ttk.Label(settings_window, text="Temperature Proportional Band:").grid(row=1, column=0, sticky="w")
- ttk.Entry(settings_window, textvariable=self.temp_proportional_band).grid(row=1, column=1)
- ttk.Label(settings_window, text="VOC Setpoint (ppm):").grid(row=2, column=0, sticky="w")
- ttk.Entry(settings_window, textvariable=self.voc_setpoint).grid(row=2, column=1)
- ttk.Label(settings_window, text="Temperature Setpoint (\u00b0C):").grid(row=3, column=0, sticky="w")
- ttk.Entry(settings_window, textvariable=self.temp_setpoint).grid(row=3, column=1)
- ttk.Label(settings_window, text="Integration Time (s):").grid(row=4, column=0, sticky="w")
- ttk.Entry(settings_window, textvariable=self.integr_time).grid(row=4, column=1)
- ttk.Label(settings_window, text="Update Interval (ms):").grid(row=5, column=0, sticky="w")
- ttk.Entry(settings_window, textvariable=self.update_interval_ms).grid(row=5, column=1)
- # Display the version number
- ttk.Label(settings_window, text="Version: " + VERSION).grid(row=6, columnspan=2, pady=5)
- def increment_voc(self, value):
- new_voc = self.voc_ppm_measured.get() + value
- self.voc_ppm_measured.set(new_voc)
- self.voc_ppm_display.set(new_voc)
- def decrement_voc(self, value):
- new_voc = self.voc_ppm_measured.get() - value
- self.voc_ppm_measured.set(new_voc)
- self.voc_ppm_display.set(new_voc)
- def increment_temp(self, value):
- new_temp = self.temp_celsius_measured.get() + value
- self.temp_celsius_measured.set(new_temp)
- def decrement_temp(self, value):
- new_temp = self.temp_celsius_measured.get() - value
- self.temp_celsius_measured.set(new_temp)
- if __name__ == "__main__":
- root = tk.Tk()
- app = VAVSimulationApp(root)
- root.mainloop()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement