"""
Building data management and heat demand calculation module.
Provides MVP architecture for building heat requirement calculations using
BDEW profiles and Test Reference Year (TRY) climate data.
:author: Dipl.-Ing. (FH) Jonas Pfeiffer
"""
import json
import os
import traceback
from collections import namedtuple
import matplotlib.gridspec as gridspec
import matplotlib.pyplot as plt
import pandas as pd
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qtagg import NavigationToolbar2QT as NavigationToolbar
from matplotlib.figure import Figure
from PyQt6.QtCore import Qt, pyqtSignal
from PyQt6.QtGui import QAction
from PyQt6.QtWidgets import (
QComboBox,
QFileDialog,
QGroupBox,
QHBoxLayout,
QLabel,
QLineEdit,
QMenuBar,
QMessageBox,
QSizePolicy,
QTableWidget,
QTableWidgetItem,
QVBoxLayout,
QWidget,
)
from districtheatingsim.gui.utilities import CheckableComboBox, convert_to_serializable
from districtheatingsim.heat_requirement.heat_requirement_calculation_csv import generate_profiles_from_csv
from districtheatingsim.utilities.schema import add_meta, check_version
[docs]
class BuildingModel:
"""
Data model for building information and heat demand calculations.
Manages CSV input data, JSON results, and heat profile generation.
"""
[docs]
def __init__(self):
self.base_path = None
self.csv_path = ""
self.json_path = ""
self.data = None
self.results = None
[docs]
def load_csv(self):
"""
Load CSV data into DataFrame.
:raises Exception: If CSV loading fails
"""
try:
self.data = pd.read_csv(self.csv_path, delimiter=";", dtype={"Subtyp": str})
except Exception as e:
raise Exception(f"Fehler beim Laden der CSV-Datei: {e}") from e
[docs]
def save_csv(self):
"""
Save DataFrame to CSV file.
:raises Exception: If CSV saving fails
"""
if self.data is not None:
try:
self.data.to_csv(self.csv_path, index=False, sep=";", encoding="utf-8-sig")
except Exception as e:
raise Exception(f"Fehler beim Speichern der CSV-Datei: {e}") from e
[docs]
def load_json(self):
"""
Load results from JSON file.
:raises Exception: If JSON loading fails
"""
try:
with open(self.json_path, encoding="utf-8") as f:
loaded_data = json.load(f)
check_version(loaded_data, "building_data")
# Building entries are keyed by index; the _meta block (and any other
# non-building key) is naturally skipped by the 'wärme' filter.
self.results = {k: v for k, v in loaded_data.items() if isinstance(v, dict) and "wärme" in v}
except Exception as e:
raise Exception(f"Fehler beim Laden der JSON-Datei: {e}") from e
[docs]
def save_json(self, combined_data):
"""
Save results to JSON file.
:param combined_data: Data to save
:type combined_data: dict
:raises Exception: If JSON saving fails
"""
try:
with open(self.json_path, "w", encoding="utf-8") as f:
json.dump(add_meta(combined_data, "building_data"), f, indent=4)
except Exception as e:
raise Exception(f"Fehler beim Speichern der Ergebnisse: {e}") from e
HeatDemandResult = namedtuple(
"HeatDemandResult",
["time_steps", "total_kw", "heating_kw", "warmwater_kw", "max_kw", "supply_temp", "return_temp", "air_temp"],
)
[docs]
def calculate_heat_demand(self, data, try_filename, year: int = 2023):
"""
Calculate heat demand profiles from building data.
:param data: Building input data
:type data: pd.DataFrame
:param try_filename: Climate data filename
:type try_filename: str
:param year: Calculation year for profile generation (BDEW/VDI 4655), defaults to 2023
:type year: int
:return: Calculated heat demand profiles in kW
:rtype: HeatDemandResult
"""
(
yearly_time_steps,
total_heat_W,
heating_heat_W,
warmwater_heat_W,
max_heat_requirement_W,
supply_temperature_curve,
return_temperature_curve,
hourly_air_temperatures,
) = generate_profiles_from_csv(data=data, TRY=try_filename, calc_method="Datensatz", year=year)
# Convert from W to kW
return self.HeatDemandResult(
time_steps=yearly_time_steps,
total_kw=total_heat_W / 1000,
heating_kw=heating_heat_W / 1000,
warmwater_kw=warmwater_heat_W / 1000,
max_kw=max_heat_requirement_W / 1000,
supply_temp=supply_temperature_curve,
return_temp=return_temperature_curve,
air_temp=hourly_air_temperatures,
)
[docs]
class BuildingPresenter:
"""
Presenter managing interaction between BuildingModel and BuildingTabView.
Coordinates building data operations, heat demand calculations, and UI updates.
"""
[docs]
def __init__(self, model, view, folder_manager, data_manager, config_manager):
"""
Initialize building presenter.
:param model: Data model
:type model: BuildingModel
:param view: View component
:type view: BuildingTabView
:param folder_manager: Folder manager
:type folder_manager: ProjectFolderManager
:param data_manager: Data manager
:type data_manager: DataManager
:param config_manager: Configuration manager
:type config_manager: ProjectConfigManager
"""
self.model = model
self.view = view
self.folder_manager = folder_manager
self.data_manager = data_manager
self.config_manager = config_manager
self.combined_data = None
# Connect signals
self.folder_manager.project_folder_changed.connect(self.standard_path)
if self.folder_manager.variant_folder:
self.standard_path(self.folder_manager.variant_folder)
self.view.create_csv_template_signal.connect(self.create_csv_template)
self.view.load_csv_signal.connect(self.load_csv)
self.view.save_csv_signal.connect(self.save_csv)
self.view.load_json_signal.connect(self.load_json)
self.view.save_json_signal.connect(self.save_json)
self.view.calculate_heat_demand_signal.connect(self.calculate_heat_demand)
self.view.data_type_combobox.view().pressed.connect(self.on_combobox_selection_changed)
self.view.building_combobox.view().pressed.connect(self.on_combobox_selection_changed)
self.view.plot(self.model.results) # Initial plot
[docs]
def standard_path(self, path):
"""
Update default file paths.
:param path: New base path
:type path: str
"""
if path:
self.model.base_path = path
self.model.csv_path = os.path.join(
self.model.base_path, self.config_manager.get_relative_path("current_building_data_path")
)
self.model.json_path = os.path.join(
self.model.base_path, self.config_manager.get_relative_path("building_load_profile_path")
)
# Column order must match what geocoding, heat-profile calc, and net generation expect.
# Heizgrenztemperatur / Heizexponent / P_max are optional BDEW parameters; leave blank to use defaults.
_TEMPLATE_COLUMNS = [
"Land",
"Bundesland",
"Stadt",
"Adresse",
"Wärmebedarf",
"Gebäudetyp",
"Subtyp",
"WW_Anteil",
"Typ_Heizflächen",
"VLT_max",
"Steigung_Heizkurve",
"RLT_max",
"Normaußentemperatur",
"Heizgrenztemperatur",
"Heizexponent",
"P_max",
"UTM_X",
"UTM_Y",
]
_TEMPLATE_EXAMPLE = [
"Deutschland",
"Sachsen",
"Leipzig",
"Musterstraße 1",
"50000",
"MFH",
"",
"0.15",
"HK",
"70",
"1.5",
"50",
"-15",
"",
"",
"",
"",
"",
]
[docs]
def create_csv_template(self):
"""
Save an empty CSV template with the required column headers and one
example row. The user picks the save location via file dialog.
"""
default_path = os.path.join(self.model.base_path or "", "Quartier.csv")
fname, _ = QFileDialog.getSaveFileName(self.view, "CSV-Vorlage speichern", default_path, "CSV Files (*.csv)")
if not fname:
return
try:
template_df = pd.DataFrame([self._TEMPLATE_EXAMPLE], columns=self._TEMPLATE_COLUMNS)
template_df.to_csv(fname, index=False, sep=";", encoding="utf-8-sig")
self.view.populate_table(template_df)
self.model.csv_path = fname
self.model.data = template_df
self.view.show_message(
"Vorlage erstellt",
f"CSV-Vorlage wurde gespeichert und geladen:\n{fname}\n\n"
"Pflichtfelder: Land, Bundesland, Stadt, Adresse, Wärmebedarf, Gebäudetyp,\n"
"WW_Anteil, VLT_max, Steigung_Heizkurve, RLT_max, Normaußentemperatur.\n\n"
"Optionale BDEW-Felder (leer lassen = pyslpheat-Standard):\n"
" Heizgrenztemperatur – Temperatur ab der geheizt wird (Standard ~15 °C)\n"
" Heizexponent – Formparameter der Heizkurve (Standard 1.0)\n"
" P_max – maximale Wärmeleistung in kW (begrenzt Lastspitzen)\n\n"
"UTM_X/UTM_Y werden beim Geocoding automatisch befüllt.",
)
except Exception as e:
self.view.show_error_message("Fehler", f"Vorlage konnte nicht gespeichert werden: {e}")
[docs]
def load_csv(self, fname=None, show_dialog=True):
"""
Load CSV file with file dialog.
Parameters
----------
fname : str, optional
Filename to load.
show_dialog : bool, optional
Whether to show success/error dialogs. Default is True.
"""
if fname is None or fname == "":
fname, _ = QFileDialog.getOpenFileName(
self.view, "Select CSV File", self.model.csv_path, "CSV Files (*.csv);;All Files (*)"
)
if fname:
try:
self.model.csv_path = fname
self.model.load_csv()
self.view.populate_table(self.model.data)
if show_dialog:
self.view.show_message("Erfolg", f"CSV-Datei {fname} wurde geladen.")
except Exception as e:
if show_dialog:
self.view.show_error_message("Fehler", str(e))
[docs]
def save_csv(self, fname=None, show_dialog=True):
"""
Save CSV file with file dialog.
Parameters
----------
fname : str, optional
Filename to save.
"""
if fname is None or fname == "":
if show_dialog:
fname, _ = QFileDialog.getSaveFileName(
self.view, "Save CSV File", self.model.csv_path, "CSV Files (*.csv);;All Files (*)"
)
else:
fname = self.model.csv_path
if fname:
try:
self.model.csv_path = fname
self.model.save_csv()
if show_dialog:
self.view.show_message("Erfolg", f"CSV-Datei wurde in {fname} gespeichert.")
except Exception as e:
if show_dialog:
self.view.show_error_message("Fehler", str(e))
[docs]
def load_json(self, fname=None, show_dialog=True):
"""
Load JSON results with optional file dialog.
Parameters
----------
fname : str, optional
Filename to load.
show_dialog : bool, optional
Whether to show file dialog if no filename provided. Default is True.
"""
if fname is None or fname == "":
if show_dialog:
fname, _ = QFileDialog.getOpenFileName(
self.view, "Select JSON File", self.model.json_path, "JSON Files (*.json);;All Files (*)"
)
else:
return
if fname:
try:
self.model.json_path = fname
self.model.load_json()
self.view.populate_building_combobox(self.model.results)
self.view.plot(self.model.results)
except Exception as e:
self.view.show_error_message("Fehler", str(e))
[docs]
def save_json(self, fname=None, show_dialog=True):
"""
Save JSON results with file dialog.
Parameters
----------
fname : str, optional
Filename to save.
"""
if self.combined_data is None:
self.view.show_error_message("Fehler", "Es sind keine Daten zum Speichern vorhanden.")
return
if fname is None or fname == "":
if show_dialog:
fname, _ = QFileDialog.getSaveFileName(
self.view, "Save JSON File", self.model.json_path, "JSON Files (*.json);;All Files (*)"
)
else:
fname = self.model.json_path
if fname:
try:
self.model.json_path = fname
self.model.save_json(self.combined_data)
if show_dialog:
self.view.show_message("Erfolg", f"Ergebnisse wurden in {fname} gespeichert.")
except Exception as e:
if show_dialog:
self.view.show_error_message("Fehler", str(e))
[docs]
def calculate_heat_demand(self, _=None):
"""Calculate heat demand profiles and save results."""
self.model.data = self.view.get_table_data()
if self.model.data.empty:
self.view.show_error_message("Fehler", "Die Tabelle enthält keine Daten.")
return
try:
try_filename = self.folder_manager.try_filename
year = getattr(self.folder_manager, "calculation_year", 2023)
results = self.model.calculate_heat_demand(self.model.data, try_filename, year=year)
self.model.results = self.format_results(results, self.model.data)
self.view.populate_building_combobox(self.model.results)
self.view.plot(self.model.results)
self.combined_data = self.combine_data_with_results(self.model.data, self.model.results)
self.model.save_json(self.combined_data)
self.view.show_message(
"Erfolg", f"Berechnung der Gebäudelastgänge abgeschlossen und in {self.model.json_path} gespeichert."
)
except Exception as e:
tb_str = "".join(traceback.format_exception(type(e), e, e.__traceback__))
self.view.show_error_message("Fehler", f"Es ist ein Fehler aufgetreten: {str(e)}\n\nDetails:\n{tb_str}")
[docs]
def combine_data_with_results(self, data, results):
"""
Combine input data with calculation results.
:param data: Input data
:type data: pd.DataFrame
:param results: Calculation results
:type results: dict
:return: Combined data dictionary
:rtype: dict
"""
data.reset_index(drop=True, inplace=True)
data_dict = data.map(convert_to_serializable).to_dict(orient="index")
combined_data = {str(idx): {**data_dict[idx], **results[str(idx)]} for idx in range(len(data))}
return combined_data
[docs]
def on_combobox_selection_changed(self):
"""Update plot when combobox selection changes."""
self.view.plot(self.model.results)
[docs]
class BuildingTabView(QWidget):
"""
View component for building tab UI.
Provides table for building data input and interactive plotting
of heat demand profiles.
"""
create_csv_template_signal = pyqtSignal()
load_csv_signal = pyqtSignal()
save_csv_signal = pyqtSignal()
load_json_signal = pyqtSignal()
save_json_signal = pyqtSignal()
calculate_heat_demand_signal = pyqtSignal()
[docs]
def __init__(self, parent=None):
"""
Initialize building tab view.
:param parent: Parent widget (optional)
:type parent: QWidget
"""
super().__init__(parent)
self.initUI()
[docs]
def initUI(self):
"""Initialize UI components."""
self.main_layout = QVBoxLayout(self)
self.initMenuBar()
self.initDataTable()
self.initPlotAndComboboxes()
self.setLayout(self.main_layout)
[docs]
def initDataTable(self):
"""Initialize data table widget."""
self.table_widget = QTableWidget(self)
self.table_widget.setMinimumSize(1200, 300)
self.table_widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
self.main_layout.addWidget(self.table_widget)
[docs]
def initPlotAndComboboxes(self):
"""Initialize plot area and data selection controls."""
plt.style.use("seaborn-v0_8-darkgrid")
# Plot area oben
plot_layout = QVBoxLayout()
self.figure = Figure(constrained_layout=True)
self.canvas = FigureCanvas(self.figure)
self.canvas.setMinimumSize(1200, 500)
self.canvas.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
self.toolbar = NavigationToolbar(self.canvas, self)
plot_layout.addWidget(self.canvas)
toolbar_layout = QHBoxLayout()
toolbar_layout.addStretch(1)
toolbar_layout.addWidget(self.toolbar)
toolbar_layout.addStretch(1)
plot_layout.addLayout(toolbar_layout)
self.main_layout.addLayout(plot_layout)
combobox_group = QGroupBox()
combobox_group.setTitle("Auswahl")
combobox_group_layout = QHBoxLayout()
# Daten auswählen
data_label = QLabel("<b>Daten auswählen:</b>")
data_label.setStyleSheet("font-size: 15px; margin-right: 10px;")
self.data_type_combobox = CheckableComboBox(self)
for data_type in [
"Wärmebedarf",
"Heizwärmebedarf",
"Warmwasserbedarf",
"Vorlauftemperatur",
"Rücklauftemperatur",
]:
self.data_type_combobox.addItem(data_type)
self.data_type_combobox.model().item(0).setCheckState(Qt.CheckState.Checked)
combobox_group_layout.addWidget(data_label)
combobox_group_layout.addWidget(self.data_type_combobox)
# Gebäude auswählen
building_label = QLabel("<b>Gebäude auswählen:</b>")
building_label.setStyleSheet("font-size: 15px; margin-left: 30px; margin-right: 10px;")
self.building_combobox = CheckableComboBox(self)
combobox_group_layout.addWidget(building_label)
combobox_group_layout.addWidget(self.building_combobox)
combobox_group.setLayout(combobox_group_layout)
self.main_layout.addWidget(combobox_group)
[docs]
def createCsvTemplate(self):
"""Emit signal to create a new CSV template."""
self.create_csv_template_signal.emit()
[docs]
def loadCsvFile(self):
"""Emit signal to load CSV file."""
self.load_csv_signal.emit()
[docs]
def saveCsvFile(self):
"""Emit signal to save CSV file."""
self.save_csv_signal.emit()
[docs]
def loadJsonFile(self):
"""Emit signal to load JSON file."""
self.load_json_signal.emit()
[docs]
def saveJsonFile(self):
"""Emit signal to save JSON file."""
self.save_json_signal.emit()
[docs]
def calculateHeatDemand(self):
"""Emit signal to calculate heat demand."""
self.calculate_heat_demand_signal.emit()
[docs]
def populate_table(self, data):
"""
Populate table with DataFrame data.
:param data: Data to display in table
:type data: pd.DataFrame
"""
self.table_widget.setColumnCount(len(data.columns))
self.table_widget.setRowCount(len(data.index))
self.table_widget.setHorizontalHeaderLabels(data.columns)
for i in range(len(data.index)):
for j in range(len(data.columns)):
item = QTableWidgetItem(str(data.iat[i, j]))
self.table_widget.setItem(i, j, item)
self.table_widget.resizeColumnsToContents()
[docs]
def get_table_data(self):
"""
Extract data from table widget.
:return: Table data as DataFrame
:rtype: pd.DataFrame
"""
rows = self.table_widget.rowCount()
columns = self.table_widget.columnCount()
data = []
for row in range(rows):
row_data = []
for column in range(columns):
widget = self.table_widget.cellWidget(row, column)
if widget:
if isinstance(widget, QComboBox):
row_data.append(widget.currentText())
elif isinstance(widget, QLineEdit):
row_data.append(widget.text())
else:
item = self.table_widget.item(row, column)
if item and item.text():
row_data.append(item.text())
else:
row_data.append(None)
data.append(row_data)
df = pd.DataFrame(data, columns=[self.table_widget.horizontalHeaderItem(i).text() for i in range(columns)])
if "Subtyp" in df.columns:
df["Subtyp"] = df["Subtyp"].astype(str)
return df
[docs]
def populate_building_combobox(self, results):
"""
Populate building selection combobox.
:param results: Results data for building selection
:type results: dict
"""
self.building_combobox.clear()
for key in results.keys():
self.building_combobox.addItem(f"Gebäude {key}")
item = self.building_combobox.model().item(self.building_combobox.count() - 1, 0)
item.setCheckState(Qt.CheckState.Checked)
[docs]
def plot(self, results=None):
"""
Plot heat demand profiles for selected buildings and data types.
:param results: Heat demand calculation results
:type results: dict
"""
if results is None:
return
self.figure.clear()
gs = gridspec.GridSpec(1, 3, width_ratios=[0.18, 0.64, 0.18], figure=self.figure)
ax_legend_left = self.figure.add_subplot(gs[0, 0])
ax_main = self.figure.add_subplot(gs[0, 1])
ax_legend_right = self.figure.add_subplot(gs[0, 2])
ax2 = ax_main.twinx()
selected_data_types = self.data_type_combobox.checkedItems()
selected_buildings = self.building_combobox.checkedItems()
label_fontsize = 16
legend_fontsize = 12
line_width = 2
color_map = plt.get_cmap("tab10")
temp_color_map = plt.get_cmap("Set2")
color_idx = 0
temp_color_idx = 0
lines_ax1 = []
labels_ax1 = []
lines_ax2 = []
labels_ax2 = []
for building in selected_buildings:
key = building.split()[-1]
value = results[key]
x = list(range(len(value["wärme"])))
if "Wärmebedarf" in selected_data_types:
(line,) = ax_main.plot(
x,
value["wärme"],
label=f"Wärmebedarf Gebäude {key}",
color=color_map(color_idx % 10),
linewidth=line_width,
)
lines_ax1.append(line)
labels_ax1.append(f"Wärmebedarf Gebäude {key}")
color_idx += 1
if "Heizwärmebedarf" in selected_data_types:
(line,) = ax_main.plot(
x,
value["heizwärme"],
label=f"Heizwärmebedarf Gebäude {key}",
color=color_map(color_idx % 10),
linestyle="--",
linewidth=line_width,
)
lines_ax1.append(line)
labels_ax1.append(f"Heizwärmebedarf Gebäude {key}")
color_idx += 1
if "Warmwasserbedarf" in selected_data_types:
(line,) = ax_main.plot(
x,
value["warmwasserwärme"],
label=f"Warmwasserbedarf Gebäude {key}",
color=color_map(color_idx % 10),
linestyle=":",
linewidth=line_width,
)
lines_ax1.append(line)
labels_ax1.append(f"Warmwasserbedarf Gebäude {key}")
color_idx += 1
if "Vorlauftemperatur" in selected_data_types:
(line,) = ax2.plot(
x,
value["vorlauftemperatur"],
label=f"Vorlauftemperatur Gebäude {key}",
color=temp_color_map(temp_color_idx % 8),
linestyle="-.",
linewidth=line_width,
)
lines_ax2.append(line)
labels_ax2.append(f"Vorlauftemperatur Gebäude {key}")
temp_color_idx += 1
if "Rücklauftemperatur" in selected_data_types:
(line,) = ax2.plot(
x,
value["rücklauftemperatur"],
label=f"Rücklauftemperatur Gebäude {key}",
color=temp_color_map(temp_color_idx % 8),
linestyle="-.",
linewidth=line_width,
)
lines_ax2.append(line)
labels_ax2.append(f"Rücklauftemperatur Gebäude {key}")
temp_color_idx += 1
ax_main.set_xlabel("Jahresstunden", fontsize=label_fontsize)
ax_main.set_ylabel("Wärmebedarf (kW)", fontsize=label_fontsize)
ax2.set_ylabel("Temperatur (°C)", fontsize=label_fontsize)
ax_main.tick_params(axis="both", labelsize=14)
ax2.tick_params(axis="y", labelsize=14)
# Legenden als eigene Achsen
ax_legend_left.axis("off")
ax_legend_right.axis("off")
if lines_ax1:
ncol_left = 2 if len(lines_ax1) > 18 else 1
ax_legend_left.legend(
lines_ax1, labels_ax1, loc="center", fontsize=legend_fontsize, frameon=False, ncol=ncol_left
)
if lines_ax2:
ncol_right = 2 if len(lines_ax2) > 18 else 1
ax_legend_right.legend(
lines_ax2, labels_ax2, loc="center", fontsize=legend_fontsize, frameon=False, ncol=ncol_right
)
self.figure.suptitle("Gebäude Wärmebedarf & Temperaturen", fontsize=18)
ax_main.grid(True, alpha=0.3)
self.canvas.draw()
[docs]
def show_error_message(self, title, message):
"""
Display error message dialog.
:param title: Dialog title
:type title: str
:param message: Error message
:type message: str
"""
QMessageBox.critical(self, title, message)
[docs]
def show_message(self, title, message):
"""
Display information message dialog.
:param title: Dialog title
:type title: str
:param message: Information message
:type message: str
"""
QMessageBox.information(self, title, message)
[docs]
class BuildingTab(QWidget):
"""
Main building tab widget integrating MVP components.
Central interface for building data management and heat demand analysis.
"""
[docs]
def __init__(self, folder_manager, data_manager, config_manager, parent=None):
"""
Initialize building tab with MVP architecture.
:param folder_manager: Folder manager
:type folder_manager: ProjectFolderManager
:param data_manager: Data manager
:type data_manager: DataManager
:param config_manager: Configuration manager
:type config_manager: ProjectConfigManager
:param parent: Parent widget (optional)
:type parent: QWidget
"""
super().__init__(parent)
self.model = BuildingModel()
self.view = BuildingTabView()
self.presenter = BuildingPresenter(self.model, self.view, folder_manager, data_manager, config_manager)
layout = QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(self.view)
if __name__ == "__main__":
import sys
from PyQt6.QtWidgets import QApplication
app = QApplication(sys.argv)
window = BuildingTabView()
window.resize(1400, 900)
window.show()
# Simuliere das Laden einer JSON wie im Model/Presenter
json_path = os.path.join(
os.path.dirname(__file__),
"..",
"..",
"project_data",
"Görlitz",
"Variante 1",
"Lastgang",
"Gebäude Lastgang.json",
)
json_path = os.path.abspath(json_path)
import json
with open(json_path, encoding="utf-8") as f:
loaded_data = json.load(f)
# Filter wie im Model: nur dicts mit 'wärme'
results = {k: v for k, v in loaded_data.items() if isinstance(v, dict) and "wärme" in v}
# Simuliere Presenter: populate_building_combobox und plot
window.populate_building_combobox(results)
window.plot(results)
sys.exit(app.exec())